Import Cobalt 24.master.0.310483
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 12bbd12..b7b0dee 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -58,10 +58,11 @@
         always_run: true
     -   id: clang-format
         name: clang-format
-        entry: python ./precommit_hooks/clang_format_wrapper.py
+        entry: clang-format
         language: python
         types: [c++]
         args: [-i, -style=file]
+        additional_dependencies: ['clang-format']
     -   id: cpplint
         name: cpplint
         entry: cpplint
@@ -147,14 +148,6 @@
         stages: [push]
         always_run: true
         pass_filenames: false
-    -   id: run-py2-tests
-        name: Run Python 2 Tests
-        description: Run Python 2 unittests
-        entry: python precommit_hooks/run_python2_unittests.py
-        language: python
-        language_version: python2.7
-        additional_dependencies: ['mock']
-        types: [python]
     -   id: osslint
         name: osslint
         entry: python precommit_hooks/osslint_wrapper.py
diff --git a/.pylintrc b/.pylintrc
index 80f56fe..6892a44 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -5,16 +5,15 @@
 # Its canonical open-source location is:
 #   https://google.github.io/styleguide/pylintrc
 #
-# Last updated for Cobalt (YYYY-MM-DD): 2020-11-04
+# Last updated for Cobalt (YYYY-MM-DD): 2022-12-29
 
 [MASTER]
 
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
+# Files or directories to be skipped. They should be base names, not paths.
 ignore=third_party
 
-# Add files or directories matching the regex patterns to the blacklist. The
-# regex matches against base names, not paths.
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
 ignore-patterns=
 
 # Pickle collected data for later comparisons.
@@ -31,11 +30,6 @@
 # active Python interpreter and may run arbitrary code.
 unsafe-load-any-extension=no
 
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code
-extension-pkg-whitelist=
-
 
 [MESSAGES CONTROL]
 
@@ -72,7 +66,6 @@
         cmp-method,
         coerce-builtin,
         coerce-method,
-        consider-using-f-string,
         delslice-method,
         div-method,
         duplicate-code,
@@ -85,7 +78,7 @@
         global-statement,
         hex-method,
         idiv-method,
-        implicit-str-concat-in-sequence,
+        implicit-str-concat,
         import-error,
         import-self,
         import-star-module-level,
@@ -119,20 +112,17 @@
         old-raise-syntax,
         parameter-unpacking,
         print-statement,
-        raise-missing-from,
         raising-string,
         range-builtin-not-iterating,
         raw_input-builtin,
         rdiv-method,
         reduce-builtin,
-        redundant-u-string-prefix,
         relative-import,
         reload-builtin,
         round-builtin,
         setslice-method,
         signature-differs,
         standarderror-builtin,
-        super-with-arguments,
         suppressed-message,
         sys-max-int,
         too-few-public-methods,
@@ -151,8 +141,6 @@
         unicode-builtin,
         unnecessary-pass,
         unpacking-in-except,
-        unspecified-encoding,
-        use-maxsplit-arg,
         useless-else-on-loop,
         useless-object-inheritance,
         useless-suppression,
@@ -169,12 +157,6 @@
 # mypackage.mymodule.MyReporterClass.
 output-format=text
 
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]". This option is deprecated
-# and it will be removed in Pylint 2.0.
-files-output=no
-
 # Tells whether to display a full report or only the messages
 reports=no
 
@@ -293,12 +275,6 @@
 # else.
 single-line-if-stmt=yes
 
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=
-
 # Maximum number of lines in a module
 max-module-lines=99999
 
diff --git a/base/values.cc b/base/values.cc
index ade6b6f..476c94c 100644
--- a/base/values.cc
+++ b/base/values.cc
@@ -415,7 +415,6 @@
 }
 
 bool Value::RemovePath(std::initializer_list<StringPiece> path) {
-  DCHECK_GE(path.size(), 2u) << "Use RemoveKey() for a path of length 1.";
   return RemovePath(make_span(path.begin(), path.size()));
 }
 
diff --git a/build/config/compiler/BUILD.gn b/build/config/compiler/BUILD.gn
index 6260b81..157c841 100644
--- a/build/config/compiler/BUILD.gn
+++ b/build/config/compiler/BUILD.gn
@@ -1292,7 +1292,7 @@
   # android:runtime_library.  This is to ensure libc++ appears before
   # libandroid_support in the -isystem include order.  Otherwise, there will be
   # build errors related to symbols declared in math.h.
-  if (use_custom_libcxx) {
+  if (!is_starboard && use_custom_libcxx) {
     configs += [ "//build/config/c++:runtime_library" ]
   }
 
diff --git a/build/config/win/BUILD.gn b/build/config/win/BUILD.gn
index 3996b5a..5003502 100644
--- a/build/config/win/BUILD.gn
+++ b/build/config/win/BUILD.gn
@@ -590,3 +590,19 @@
 config("nominmax") {
   defines = [ "NOMINMAX" ]
 }
+
+# Visual Studio 2022 Upgrade --------------------------------------------------
+
+# TODO(b/210151198) Visual Studio 2022 upgrade compatibility changes.
+# To be deleted after the upgrade completes and 2022 is the default.
+
+if (is_starboard) {
+  config("visual_studio_version_compat") {
+    if (use_visual_studio_2022) {
+      cflags = [
+        "/wd4800",
+        "/wd4834",
+      ]
+    }
+  }
+}
diff --git a/build/config/win/visual_studio_version.gni b/build/config/win/visual_studio_version.gni
index 47f16c4..74828ec 100644
--- a/build/config/win/visual_studio_version.gni
+++ b/build/config/win/visual_studio_version.gni
@@ -4,7 +4,6 @@
 
 if (is_starboard) {
   declare_args() {
-    # Version of Visual Studio.
     visual_studio_version = "14.15.26726"
 
     # Full path to the Windows SDK, not including a backslash at the end.
@@ -20,6 +19,20 @@
     }
   }
 
+  # TODO(b/210151198) Visual Studio 2022 upgrade compatibility changes.
+  # This argument should be removed, and replace the defaults for the arguments
+  # that are being overriden correspondingly.
+  declare_args() {
+    use_visual_studio_2022 = getenv("VISUAL_STUDIO_VERSION") == "2022"
+  }
+
+  if (use_visual_studio_2022) {
+    visual_studio_version = "14.34.31933"
+    if (current_os != "winuwp") {
+      wdk_version = "10.0.18362.0"
+    }
+  }
+
   if (is_docker_build) {
     _default_visual_studio_path = "C:/BuildTools"
   } else {
diff --git a/build/toolchain/cc_wrapper.gni b/build/toolchain/cc_wrapper.gni
index f2d368d..bdb8cea 100644
--- a/build/toolchain/cc_wrapper.gni
+++ b/build/toolchain/cc_wrapper.gni
@@ -41,6 +41,15 @@
   }
 }
 
+if (is_starboard) {
+  declare_args() {
+    # Set to false to completely ignore the cc_wrapper.
+    enable_cc_wrapper = true
+  }
+  assert(enable_cc_wrapper || cc_wrapper == "",
+         "Do not set `cc_wrapper` if you set `enable_cc_wrapper` to false.")
+}
+
 if (is_starboard && getenv("SCCACHE") == "") {
   enable_sccache = host_os == "win" && cobalt_fastbuild
 }
@@ -51,13 +60,14 @@
   _set_sccache_gcs_oauth_url = getenv("SCCACHE_GCS_OAUTH_URL") != ""
   _set_sccache_gcs_rw_mode = getenv("SCCACHE_GCS_RW_MODE") != ""
   _set_sccache_dir = getenv("SCCACHE_DIR") != ""
-  assert((_set_sccache_dir) || (_set_sccache_gcs_bucket &&
-             (_set_sccache_gcs_key_path || _set_sccache_gcs_oauth_url) &&
-             _set_sccache_gcs_rw_mode),
+  assert(_set_sccache_dir ||
+             (_set_sccache_gcs_bucket &&
+                  (_set_sccache_gcs_key_path || _set_sccache_gcs_oauth_url) &&
+                  _set_sccache_gcs_rw_mode),
          "Set Sccache environment variables before use.")
 }
 
-if (is_starboard && cc_wrapper == "") {
+if (is_starboard && cc_wrapper == "" && enable_cc_wrapper) {
   # TODO(https://crbug.com/gn/273): Use sccache locally as well.
   if (enable_sccache) {
     cc_wrapper = "sccache"
diff --git a/build/toolchain/win/msvc_toolchain.gni b/build/toolchain/win/msvc_toolchain.gni
index 52225b0..92c98d4 100644
--- a/build/toolchain/win/msvc_toolchain.gni
+++ b/build/toolchain/win/msvc_toolchain.gni
@@ -141,6 +141,9 @@
       rspfile_content = "{{inputs_newline}}"
     }
 
+    # TODO(b/217794556): All following linker tools should list the PDB file in
+    # their outputs. It has been removed as it is not always generated, and
+    # ninja will treat the missing file as a dirty edge.
     tool("solink") {
       # E.g. "foo.dll":
       dllname = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
@@ -157,13 +160,11 @@
       outputs = [
         dllname,
         libname,
-        pdbname,
       ]
       link_output = libname
       depend_output = libname
       runtime_outputs = [
         dllname,
-        pdbname,
       ]
 
       # Since the above commands only updates the .lib file when it changes, ask
@@ -190,7 +191,6 @@
       description = "LINK_MODULE(DLL) {{output}}"
       outputs = [
         dllname,
-        pdbname,
       ]
       runtime_outputs = outputs
 
@@ -212,7 +212,6 @@
       description = "LINK {{output}}"
       outputs = [
         exename,
-        pdbname,
       ]
       runtime_outputs = outputs
 
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
deleted file mode 100644
index afaa3ca..0000000
--- a/buildtools/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*.pyc
-linux64/clang-format
-mac/clang-format
-win/clang-format.exe
diff --git a/buildtools/LICENSE b/buildtools/LICENSE
deleted file mode 100644
index 972bb2e..0000000
--- a/buildtools/LICENSE
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//    * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//    * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//    * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/buildtools/METADATA b/buildtools/METADATA
deleted file mode 100644
index 33b683b..0000000
--- a/buildtools/METADATA
+++ /dev/null
@@ -1,20 +0,0 @@
-name: "buildtools"
-description:
-  "Subtree at buildtools."
-
-third_party {
-  url {
-    type: LOCAL_SOURCE
-    value: "/buildtools_mirror"
-  }
-  url {
-    type: GIT
-    value: "https://chromium.googlesource.com/chromium/buildtools"
-  }
-  version: "5af0a3a8b89827a8634132080a39ab4b63dee489"
-  last_upgrade_date {
-    year: 2017
-    month: 8
-    day: 18
-  }
-}
diff --git a/buildtools/README.txt b/buildtools/README.txt
deleted file mode 100644
index 1db97b4..0000000
--- a/buildtools/README.txt
+++ /dev/null
@@ -1,56 +0,0 @@
-This repository contains hashes of build tools used by Chromium and related
-projects. The actual binaries are pulled from Google Storage, normally as part
-of a gclient hook.
-
-The repository is separate so that the shared build tools can be shared between
-the various Chromium-related projects without each one needing to maintain
-their own versionining of each binary.
-
-To update the GN binary, run (from the Chromium repo) tools/gn/bin/roll_gn.py
-which will automatically upload the binaries and roll build tools.
-
-________________________________________
-UPDATING AND ROLLING BUILDTOOLS MANUALLY
-
-When you update buildtools, you should roll the new version into the Chromium
-repository right away. Otherwise, the next person who makes a change will end
-up rolling (and testing) your change. If there are any unresolved problems with
-your change, the next person will be blocked.
-
-  - From the buildtools directory, make a branch, edit and upload normally.
-
-  - Get your change reviewed and landed. There are no trybots so landing will
-    be very fast.
-
-  - Get the hash for the commit that commit-bot made. Make a new branch in
-    the Chromium repository and paste the hash into the line in //DEPS
-    labeled "buildtools_revision".
-
-  - You can TBR changes to the DEPS file since the git hashes can't be reviewed
-    in any practical way. Submit that patch to the commit queue.
-
-  - If this roll identifies a problem with your patch, fix it promptly. If you
-    are unable to fix it promptly, it's best to revert your buildtools patch
-    to avoid blocking other people that want to make changes.
-
-________________________
-ADDING BINARIES MANUALLY
-
-One uploads new versions of the tools using the 'gsutil' binary from the
-Google Storage SDK:
-
-  https://developers.google.com/storage/docs/gsutil
-
-There is a checked-in version of gsutil as part of depot_tools.
-
-To initialize gsutil's credentials:
-
-  python ~/depot_tools/third_party/gsutil/gsutil config
-
-  That will give a URL which you should log into with your web browser. For
-  rolling GN, the username should be the one that is on the ACL for the
-  "chromium-gn" bucket (probably your @google.com address). Contact the build
-  team for help getting access if necessary.
-
-  Copy the code back to the command line util. Ignore the project ID (it's OK
-  to just leave blank when prompted).
diff --git a/buildtools/deps_revisions.gni b/buildtools/deps_revisions.gni
deleted file mode 100644
index d66c2a9..0000000
--- a/buildtools/deps_revisions.gni
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-declare_args() {
-  # Used to cause full rebuilds on libc++ rolls. This should be kept in sync
-  # with the libcxx_revision vars in //DEPS.
-  libcxx_revision = "8fa87946779682841e21e2da977eccfb6cb3bded"
-}
diff --git a/buildtools/linux64/clang-format.sha1 b/buildtools/linux64/clang-format.sha1
deleted file mode 100644
index dcf7df3..0000000
--- a/buildtools/linux64/clang-format.sha1
+++ /dev/null
@@ -1 +0,0 @@
-5349d1954e17f6ccafb6e6663b0f13cdb2bb33c8
\ No newline at end of file
diff --git a/buildtools/mac/clang-format.sha1 b/buildtools/mac/clang-format.sha1
deleted file mode 100644
index 8a00b61..0000000
--- a/buildtools/mac/clang-format.sha1
+++ /dev/null
@@ -1 +0,0 @@
-0679b295e2ce2fce7919d1e8d003e497475f24a3
diff --git a/buildtools/win/clang-format.exe.sha1 b/buildtools/win/clang-format.exe.sha1
deleted file mode 100644
index fbb0ae5..0000000
--- a/buildtools/win/clang-format.exe.sha1
+++ /dev/null
@@ -1 +0,0 @@
-c8455d43d052eb79f65d046c6b02c169857b963b
diff --git a/cobalt/BUILD.gn b/cobalt/BUILD.gn
index c268919..23b7aca 100644
--- a/cobalt/BUILD.gn
+++ b/cobalt/BUILD.gn
@@ -23,7 +23,6 @@
     "//cobalt/dom:dom_test",
     "//cobalt/dom_parser:dom_parser_test",
     "//cobalt/encoding:text_encoding_test",
-    "//cobalt/extension:extension_test",
     "//cobalt/layout:layout_test",
     "//cobalt/layout_tests",
     "//cobalt/layout_tests:web_platform_tests",
diff --git a/cobalt/base/deep_link_event.h b/cobalt/base/deep_link_event.h
index c2d3aaf..e878511 100644
--- a/cobalt/base/deep_link_event.h
+++ b/cobalt/base/deep_link_event.h
@@ -16,20 +16,28 @@
 #define COBALT_BASE_DEEP_LINK_EVENT_H_
 
 #include <string>
+#include <utility>
 
 #include "base/callback.h"
 #include "base/compiler_specific.h"
 #include "base/strings/string_util.h"
 #include "cobalt/base/event.h"
+#include "cobalt/base/polymorphic_downcast.h"
 
 namespace base {
 
 class DeepLinkEvent : public Event {
  public:
   DeepLinkEvent(const std::string& link, const base::Closure& consumed_callback)
-      : link_(link), consumed_callback_(consumed_callback) {}
+      : link_(link), consumed_callback_(std::move(consumed_callback)) {}
+  explicit DeepLinkEvent(const Event* event) {
+    const base::DeepLinkEvent* deep_link_event =
+        base::polymorphic_downcast<const base::DeepLinkEvent*>(event);
+    link_ = deep_link_event->link();
+    consumed_callback_ = deep_link_event->callback();
+  }
   const std::string& link() const { return link_; }
-  const base::Closure& callback() const { return consumed_callback_; }
+  base::Closure callback() const { return consumed_callback_; }
 
   BASE_EVENT_SUBCLASS(DeepLinkEvent);
 
diff --git a/cobalt/base/source_location.h b/cobalt/base/source_location.h
index e4a5e5c..dfedda2 100644
--- a/cobalt/base/source_location.h
+++ b/cobalt/base/source_location.h
@@ -34,6 +34,7 @@
 //
 // Line and column numbers are 1-based.
 struct SourceLocation {
+  SourceLocation() = default;
   SourceLocation(const std::string& file_path, int line_number,
                  int column_number)
       : file_path(file_path),
diff --git a/cobalt/base/tokens.h b/cobalt/base/tokens.h
index 5629893..0b82b64 100644
--- a/cobalt/base/tokens.h
+++ b/cobalt/base/tokens.h
@@ -92,6 +92,7 @@
     MacroOpWithNameOnly(offline)                                     \
     MacroOpWithNameOnly(online)                                      \
     MacroOpWithNameOnly(open)                                        \
+    MacroOpWithNameOnly(pointercancel)                               \
     MacroOpWithNameOnly(pointerdown)                                 \
     MacroOpWithNameOnly(pointerenter)                                \
     MacroOpWithNameOnly(pointerleave)                                \
diff --git a/cobalt/bindings/contexts.py b/cobalt/bindings/contexts.py
index fb03640..10b02da 100644
--- a/cobalt/bindings/contexts.py
+++ b/cobalt/bindings/contexts.py
@@ -345,7 +345,9 @@
       return base_type + '*'
     if is_any_type(idl_type) or is_array_buffer_or_view_type(idl_type):
       return 'const ::cobalt::script::ScriptValue<%s>*' % base_type
-    elif idl_type.is_string_type or idl_type.is_interface_type:
+    elif cobalt_type_is_optional(idl_type) or is_sequence_type(idl_type) or (
+        idl_type.is_string_type or idl_type.is_interface_type or
+        idl_type.is_union_type):
       return 'const %s&' % base_type
     return base_type
 
diff --git a/cobalt/bindings/flatten_idls_test.py b/cobalt/bindings/flatten_idls_test.py
index 2eef9f9..0401c72 100644
--- a/cobalt/bindings/flatten_idls_test.py
+++ b/cobalt/bindings/flatten_idls_test.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python3
 #
 # Copyright 2016 The Cobalt Authors. All Rights Reserved.
 #
@@ -19,7 +20,7 @@
 import platform
 import unittest
 
-import _env  # pylint: disable=unused-import
+from . import _env  # pylint: disable=unused-import
 from cobalt.bindings import flatten_idls
 
 
@@ -85,6 +86,7 @@
     self.assertItemsEqual(['Tomato', 'Onion'], result[0].operations)
 
 
+@unittest.skip('Another test here has bitrotted.')
 class FlattenedInterfaceDifferenceTest(unittest.TestCase):
 
   def testAssertsOnDifferentInterfaces(self):
diff --git a/cobalt/bindings/path_generator_test.py b/cobalt/bindings/path_generator_test.py
index a819b45..a6f2ce0 100644
--- a/cobalt/bindings/path_generator_test.py
+++ b/cobalt/bindings/path_generator_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright 2017 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,7 @@
 
 import unittest
 
+from . import _env  # pylint: disable=unused-import
 from cobalt.bindings.path_generator import PathBuilder
 
 
diff --git a/cobalt/bindings/shared/idl_conditional_macros.h b/cobalt/bindings/shared/idl_conditional_macros.h
index 3dd9d74..a34191f 100644
--- a/cobalt/bindings/shared/idl_conditional_macros.h
+++ b/cobalt/bindings/shared/idl_conditional_macros.h
@@ -27,8 +27,4 @@
 // attribute.
 #define COBALT_ENABLE_ON_SCREEN_KEYBOARD
 
-// This is used to conditionally define setMaxVideoCapabilities() in
-// HTMLVideoElement.
-#define COBALT_ENABLE_SET_MAX_VIDEO_CAPABILITIES
-
 #endif  // COBALT_BINDINGS_SHARED_IDL_CONDITIONAL_MACROS_H_
diff --git a/cobalt/bindings/testing/array_buffers_test.cc b/cobalt/bindings/testing/array_buffers_test.cc
index e14289c..bf4139e 100644
--- a/cobalt/bindings/testing/array_buffers_test.cc
+++ b/cobalt/bindings/testing/array_buffers_test.cc
@@ -61,24 +61,23 @@
   }
 
   {
-    std::unique_ptr<script::PreallocatedArrayBufferData> preallocated_data(
-        new script::PreallocatedArrayBufferData(256));
-    EXPECT_EQ(256, preallocated_data->byte_length());
-    for (int i = 0; i < preallocated_data->byte_length(); i++) {
-      reinterpret_cast<uint8_t*>(preallocated_data->data())[i] = i;
+    script::PreallocatedArrayBufferData preallocated_data(256);
+    EXPECT_EQ(256, preallocated_data.byte_length());
+    for (int i = 0; i < preallocated_data.byte_length(); i++) {
+      preallocated_data.data()[i] = i;
     }
 
-    void* data_pointer = preallocated_data->data();
-
+    void* data_pointer = preallocated_data.data();
     auto array_buffer = script::ArrayBuffer::New(global_environment_,
                                                  std::move(preallocated_data));
+
+    EXPECT_EQ(nullptr, preallocated_data.data());
     EXPECT_EQ(256, array_buffer->ByteLength());
     EXPECT_EQ(data_pointer, array_buffer->Data());
+
     for (int i = 0; i < 256; i++) {
       EXPECT_EQ(i, reinterpret_cast<uint8_t*>(array_buffer->Data())[i]);
     }
-
-    EXPECT_EQ(nullptr, preallocated_data.get());
   }
 }
 
diff --git a/cobalt/black_box_tests/black_box_cobalt_runner.py b/cobalt/black_box_tests/black_box_cobalt_runner.py
index f9ec2df..1f333e3 100644
--- a/cobalt/black_box_tests/black_box_cobalt_runner.py
+++ b/cobalt/black_box_tests/black_box_cobalt_runner.py
@@ -17,19 +17,32 @@
 from __future__ import division
 from __future__ import print_function
 
+import logging
+import time
+
 from cobalt.tools.automated_testing import cobalt_runner
+from cobalt.tools.automated_testing import webdriver_utils
+
+selenium_exceptions = webdriver_utils.import_selenium_module(
+    'common.exceptions')
 
 # The following constants and logics are shared between this module and
 # the JavaScript test environment. Anyone making changes here should also
 # ensure necessary changes are made to testdata/black_box_js_test_utils.js
 _TEST_STATUS_ELEMENT_NAME = 'black_box_test_status'
 _JS_TEST_SUCCESS_MESSAGE = 'JavaScript_test_succeeded'
+_JS_TEST_FAILURE_MESSAGE = 'JavaScript_test_failed'
 _JS_TEST_SETUP_DONE_MESSAGE = 'JavaScript_setup_done'
 
+POLL_UNTIL_WAIT_SECONDS = 30
+
 
 class BlackBoxCobaltRunner(cobalt_runner.CobaltRunner):
   """Custom CobaltRunner made for BlackBoxTests' need."""
 
+  class AssertException(Exception):
+    """Raised when assert condition fails."""
+
   def __init__(self,
                launcher_params,
                url,
@@ -43,11 +56,37 @@
       target_params = []
     target_params.append('--silence_inline_script_warnings')
 
-    super(BlackBoxCobaltRunner, self).__init__(launcher_params, url, log_file,
-                                               target_params, success_message)
+    super().__init__(launcher_params, url, log_file, target_params,
+                     success_message)
+
+  def PollUntilFoundOrTestsFailedWithReconnects(self, css_selector):
+    """Polls until an element is found.
+
+    Args:
+      css_selector: A CSS selector
+    """
+    start_time = time.time()
+    while time.time() - start_time < POLL_UNTIL_WAIT_SECONDS:
+      is_failed = False
+      try:
+        if self.FindElements(css_selector):
+          break
+        is_failed = self.JSTestsFailed()
+      except (cobalt_runner.CobaltRunner.AssertException,
+              selenium_exceptions.NoSuchElementException,
+              selenium_exceptions.NoSuchWindowException,
+              selenium_exceptions.WebDriverException) as e:
+        # If the page
+        logging.warning(e)
+        self.ReconnectWebDriver()
+        continue
+      if is_failed:
+        raise BlackBoxCobaltRunner.AssertException(
+            'JS Test failed while waiting for ' + css_selector)
+      time.sleep(0.25)
 
   def JSTestsSucceeded(self):
-    """Check test assertions in HTML page."""
+    """Check succeeded test assertion in HTML page."""
 
     # Call onTestEnd() in black_box_js_test_utils.js to unblock the waiting for
     # JavaScript test logic completion.
@@ -56,9 +95,18 @@
     return body_element.get_attribute(
         _TEST_STATUS_ELEMENT_NAME) == _JS_TEST_SUCCESS_MESSAGE
 
+  def JSTestsFailed(self):
+    """Check failed test assertion in HTML page."""
+
+    # Call onTestEnd() in black_box_js_test_utils.js to unblock the waiting for
+    # JavaScript test logic completion.
+    body_element = self.UniqueFind('body')
+    return body_element and body_element.get_attribute(
+        _TEST_STATUS_ELEMENT_NAME) == _JS_TEST_FAILURE_MESSAGE
+
   def WaitForJSTestsSetup(self):
     """Poll setup status until JavaScript gives green light."""
 
     # Calling setupFinished() in black_box_js_test_utils.js to unblock the
     # waiting logic here.
-    self.PollUntilFound('#{}'.format(_JS_TEST_SETUP_DONE_MESSAGE))
+    self.PollUntilFound(f'#{_JS_TEST_SETUP_DONE_MESSAGE}')
diff --git a/cobalt/black_box_tests/black_box_tests.py b/cobalt/black_box_tests/black_box_tests.py
index 2ede496..129c8a8 100644
--- a/cobalt/black_box_tests/black_box_tests.py
+++ b/cobalt/black_box_tests/black_box_tests.py
@@ -64,18 +64,25 @@
     'compression_test',
     'default_site_can_load',
     'disable_eval_with_csp',
+    'h5vcc_storage_write_verify_test',
     'http_cache',
     'persistent_cookie',
+    'service_worker_cache_keys_test',
+    'service_worker_controller_activation_test',
+    'service_worker_get_registrations_test',
+    'service_worker_fetch_test',
+    'service_worker_message_test',
+    'service_worker_test',
+    # TODO(b/259731731) disable for now until persisted registrations
+    # activate correctly.
+    'service_worker_persist_test',
     'soft_mic_platform_service_test',
     'text_encoding_test',
     'web_debugger',
     'web_platform_tests',
     'web_worker_test',
     'worker_csp_test',
-    'service_worker_get_registrations_test',
-    'service_worker_fetch_test',
-    'service_worker_message_test',
-    'service_worker_test',
+    'worker_load_test',
 ]
 # These tests can only be run on platforms whose app launcher can send deep
 # links.
@@ -96,7 +103,7 @@
   """Base class for Cobalt black box test cases."""
 
   def __init__(self, *args, **kwargs):
-    super(BlackBoxTestCase, self).__init__(*args, **kwargs)
+    super().__init__(*args, **kwargs)
     self.launcher_params = _launcher_params
     self.platform_config = build.GetPlatformConfig(_launcher_params.platform)
     self.cobalt_config = self.platform_config.GetApplicationConfiguration(
@@ -185,13 +192,12 @@
     # be able to bind correctly with incomplete support of IPv6
     if device_id and IsValidIpAddress(device_id):
       _launcher_params.target_params.append(
-          '--dev_servers_listen_ip={}'.format(device_id))
+          f'--dev_servers_listen_ip={device_id}')
     elif IsValidIpAddress(server_binding_address):
       _launcher_params.target_params.append(
-          '--dev_servers_listen_ip={}'.format(server_binding_address))
+          f'--dev_servers_listen_ip={server_binding_address}')
     _launcher_params.target_params.append(
-        '--web-platform-test-server=http://web-platform.test:{}'.format(
-            wpt_http_port))
+        f'--web-platform-test-server=http://web-platform.test:{wpt_http_port}')
 
     # Port used to create the proxy server. If not specified, a random free
     # port is used.
@@ -199,8 +205,8 @@
       proxy_port = str(self.GetUnusedPort([server_binding_address]))
     if proxy_address is None:
       proxy_address = server_binding_address
-    _launcher_params.target_params.append('--proxy=%s:%s' %
-                                          (proxy_address, proxy_port))
+    _launcher_params.target_params.append(
+        f'--proxy={proxy_address}:{proxy_port}')
 
     self.proxy_port = proxy_port
     self.test_name = test_name
@@ -224,10 +230,10 @@
     out_dir = _launcher_params.out_directory
     is_ci = out_dir and 'mh_lab' in out_dir  # pylint: disable=unsupported-membership-test
 
-    target = (_launcher_params.platform, _launcher_params.config)
-    if is_ci and '{}/{}'.format(*target) in _DISABLED_BLACKBOXTEST_CONFIGS:
+    if is_ci and (f'{_launcher_params.platform}/{_launcher_params.config}'
+                  in _DISABLED_BLACKBOXTEST_CONFIGS):
       logging.warning('Blackbox tests disabled for platform:%s config:%s',
-                      *target)
+                      _launcher_params.platform, _launcher_params.config)
       return 0
 
     logging.info('Using proxy port: %s', self.proxy_port)
@@ -237,12 +243,13 @@
         host_resolve_map=self.host_resolve_map,
         client_ips=self.device_ips):
       if self.test_name:
-        suite = unittest.TestLoader().loadTestsFromModule(
-            importlib.import_module(_TEST_DIR_PATH + self.test_name))
+        suite = unittest.TestLoader().loadTestsFromName(_TEST_DIR_PATH +
+                                                        self.test_name)
       else:
         suite = LoadTests(_launcher_params)
+      # Using verbosity=2 to log individual test function names and results.
       return_code = not unittest.TextTestRunner(
-          verbosity=0, stream=sys.stdout).run(suite).wasSuccessful()
+          verbosity=2, stream=sys.stdout).run(suite).wasSuccessful()
       return return_code
 
   def GetUnusedPort(self, addresses):
diff --git a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
index 2227ff5..e4dfc8a 100644
--- a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
+++ b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
@@ -52,6 +52,9 @@
     console.error('Already failed.');
     return;
   }
+  if (!error) {
+    error = Error('');
+  }
   Promise.resolve(tearDown()).then(() => {
     printError(error);
     document.body.setAttribute(TEST_STATUS_ELEMENT_NAME, FAILURE_MESSAGE);
diff --git a/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html
new file mode 100644
index 0000000..25c748b
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<html>
+  <head></head>
+  <body>
+    <script src='black_box_js_test_utils.js'></script>
+    <script src='h5vcc_storage_write_verify_test.js'></script>
+  </body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js
new file mode 100644
index 0000000..3bd4a20
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js
@@ -0,0 +1,84 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+'use strict';
+
+function failTest() {
+  notReached();
+  onEndTest();
+}
+
+function assertWrite(bytes, test_string) {
+  let response = h5vcc.storage.writeTest(bytes, test_string);
+  if (response.error != "") {
+    console.log(`assertWrite(${bytes}, ${test_string}), error: ${response.error}`);
+    failTest();
+    return false;
+  }
+  if (response.bytes_written != bytes) {
+    console.log(`assertWrite(${bytes}, ${test_string}), bytes_written (${response.bytes_written}) not equal to bytes`);
+    failTest();
+    return false;
+  }
+  return true;
+}
+
+function assertVerify(bytes, test_string) {
+  let response = h5vcc.storage.verifyTest(bytes, test_string);
+  if (response.error != "") {
+    console.log(`assertVerify(${bytes}, ${test_string}), error: ${response.error}`);
+    failTest();
+    return false;
+  }
+  if (response.bytes_read != bytes) {
+    console.log(`assertVerify(${bytes}, ${test_string}), bytes_read (${response.bytes_read}) not equal to bytes`);
+    failTest();
+    return false;
+  }
+  if (!response.verified) {
+    console.log(`assertVerify(${bytes}, ${test_string}), response not verified`);
+    failTest();
+    return false;
+  }
+  return true;
+}
+
+function h5vccStorageWriteVerifyTest() {
+  if (!h5vcc.storage) {
+    console.log("h5vcc.storage does not exist");
+    return;
+  }
+
+  if (!h5vcc.storage.writeTest || !h5vcc.storage.verifyTest) {
+    console.log("writeTest and verifyTest do not exist");
+    return;
+  }
+
+  if (!assertWrite(1, "a"))
+    return;
+  if (!assertVerify(1, "a"))
+    return;
+
+  if (!assertWrite(100, "a"))
+    return;
+  if (!assertVerify(100, "a"))
+    return;
+
+  if (!assertWrite(24 * 1024 * 1024, "foobar"))
+    return;
+  if (!assertVerify(24 * 1024 * 1024, "foobar"))
+    return;
+}
+
+h5vccStorageWriteVerifyTest();
+onEndTest();
diff --git a/cobalt/black_box_tests/testdata/http_cache.html b/cobalt/black_box_tests/testdata/http_cache.html
index 71dda62..edabfb3 100644
--- a/cobalt/black_box_tests/testdata/http_cache.html
+++ b/cobalt/black_box_tests/testdata/http_cache.html
@@ -53,6 +53,24 @@
     // Ensure that the cache is enabled.
     h5vcc.storage.enableCache();
 
+    // Make sure caching quotas are configured for required resources
+    const quota = 1024 * 1024 * 1; // set 1MB for each
+    let quotas = h5vcc.storage.getQuota();
+    // required by test
+    quotas.uncompiled_js = quota;
+    quotas.image = quota;
+    quotas.css = quota;
+    // Adjust "other" to match the total
+    quotas.other = quotas.total -
+      quotas.html -  quotas.css -  quotas.image - quotas.font - quotas.splash -
+      quotas.uncompiled_js -  quotas.compiled_js - quotas.cache_api -
+      quotas.service_worker_js;
+    const response = h5vcc.storage.setQuota(quotas);
+    if(!response.success) {
+      console.log("Failed to set cache quotas:" + response.error)
+      notReached();
+    }
+
     // Validate the tranferSize attribute of each performance entry specified
     // in CACHED_ELEMENT_LOCATIONS and UNCACHED_ELEMENT_LOCATIONS.
     function checkTransferSizes() {
diff --git a/cobalt/black_box_tests/testdata/preload_visibility.html b/cobalt/black_box_tests/testdata/preload_visibility.html
index 1c1eab8..6063d37 100644
--- a/cobalt/black_box_tests/testdata/preload_visibility.html
+++ b/cobalt/black_box_tests/testdata/preload_visibility.html
@@ -15,13 +15,20 @@
     assertEqual("hidden", document.visibilityState);
     assertFalse(document.hasFocus());
 
+    var visibilityStateVisible = false;
     // Wait for visibility change to verify visibilityState and having focus.
     function handleVisibilityChange() {
-      assertEqual("visible", document.visibilityState);
       assertFalse(document.hasFocus());
-      onEndTest();
+      if (visibilityStateVisible) {
+        assertEqual("hidden", document.visibilityState);
+        // Allow the app to become visible and then hidden at most once.
+      } else {
+        assertEqual("visible", document.visibilityState);
+        visibilityStateVisible = true;
+        onEndTest();
+      }
     }
     document.addEventListener("visibilitychange", handleVisibilityChange);
     setupFinished();
   </script>
-</body>
\ No newline at end of file
+</body>
diff --git a/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html
new file mode 100644
index 0000000..c5a57e9
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  This is a basic test of the cache.keys() for service workers.
+-->
+
+<html>
+<head>
+  <title>Cobalt Service Worker cache.keys() Test</title>
+  <script src='black_box_js_test_utils.js'></script>
+</head>
+<body>
+  <script>
+    const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+        Promise.all(registrations.map(r => r.unregister())));
+    const fail = msg => {
+      if (msg) {
+        console.error(msg);
+      }
+      unregisterAll().then(notReached);
+    };
+    const timeoutId = window.setTimeout(fail, 10000);
+    const success = () => unregisterAll().then(() => {
+      clearTimeout(timeoutId);
+      onEndTest();
+    });
+    const assertEquals = (expected, actual, msg) => {
+      if (expected !== actual) {
+        const errorMessage = `Expected: '${expected}', but was '${actual}'`;
+        fail(msg ? `${msg}(${errorMessage})` : errorMessage);
+      }
+    };
+    const workerPostMessage = message => navigator.serviceWorker.getRegistrations()
+      .then(registrations => {
+        let sent = false;
+        registrations.forEach(registration => {
+          if (registration.active) {
+            registration.active.postMessage(message);
+            sent = true;
+          }
+        });
+        if (!sent) {
+          fail('Unable to post message to active service worker.');
+        }
+      });
+    let activeServiceWorker = null;
+    navigator.serviceWorker.onmessage = event => {
+      if (event.data === 'end-test') {
+        success();
+        return;
+      }
+      fail(event.data);
+    };
+
+    unregisterAll()
+      .then(() => navigator.serviceWorker.register('service_worker_cache_keys_test.js'))
+      .catch(fail);
+
+    navigator.serviceWorker.ready.then(() => {
+      workerPostMessage('start-test');
+    });
+
+    setupFinished();
+  </script>
+</body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js
new file mode 100644
index 0000000..a80fe9d
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js
@@ -0,0 +1,70 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const postMessage = message => {
+  const options = {
+    includeUncontrolled: false, type: 'window'
+  };
+  self.clients.matchAll(options).then(clients => {
+    clients.forEach(c => {
+      c.postMessage(message);
+    });
+  });
+};
+
+const postError = message => {
+  postMessage(new Error(message).stack);
+};
+
+const assertEquals = (expected, actual, message) => {
+  if (expected !== actual) {
+    postError(`\nExpected: ${expected}\nActual:   ${actual}\n${message || ''}`);
+    throw new Error();
+  }
+};
+
+const assertTrue = (actual, message) => {
+  assertEquals(true, actual, message);
+};
+
+self.addEventListener("install", event => {
+  self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+  self.clients.claim();
+});
+
+self.addEventListener('message', async event => {
+  if (event.data === 'start-test') {
+    const cache = await caches.open('test-cache');
+    if ((await cache.keys()).length !== 0) {
+      const keys = await cache.keys();
+      await Promise.all(keys.map(k => cache.delete(k.url)));
+    }
+    assertEquals(0, (await cache.keys()).length);
+    await cache.put('http://www.example.com/a', new Response('a'));
+    await cache.put('http://www.example.com/b', new Response('b'));
+    await cache.put('http://www.example.com/c', new Response('c'));
+    const keys = await cache.keys();
+    assertEquals(3, keys.length);
+    const urls = new Set(keys.map(k => k.url));
+    assertTrue(urls.has('http://www.example.com/a'));
+    assertTrue(urls.has('http://www.example.com/b'));
+    assertTrue(urls.has('http://www.example.com/c'));
+    postMessage('end-test');
+    return;
+  }
+  postMessage(`${JSON.stringify(event.data)}-unexpected-message`);
+});
diff --git a/cobalt/black_box_tests/testdata/service_worker_controller_activation_test.html b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test.html
new file mode 100644
index 0000000..56321a8
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  Test for service worker controller activation.
+-->
+
+<html>
+
+<head>
+    <title>Cobalt Service Worker Controller Activation Test</title>
+    <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+    <script src='service_worker_controller_activation_test_script.js'></script>
+</body>
+
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_script.js b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_script.js
new file mode 100644
index 0000000..d373dba
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_script.js
@@ -0,0 +1,130 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+* @param {KeyboardEvent} event
+*/
+
+const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+  Promise.all(registrations.map(r => r.unregister())));
+setTearDown(unregisterAll);
+
+function TEST(test) {
+  console.log('[ RUN', test.name, ']');
+  test();
+}
+
+window.onkeydown = function (event) {
+  assertEqual(undefined, self.clients);
+
+  if (!('serviceWorker' in navigator)) {
+    console.log("serviceWorker not in navigator, ending test");
+    onEndTest();
+  }
+
+  navigator.serviceWorker.ready.then(function (registration) {
+    assertNotEqual(null, registration);
+    console.log('(Expected) Registration ready promise',
+      registration, 'with active worker ', registration.active);
+    assertNotEqual(null, registration.active);
+  });
+
+  navigator.serviceWorker.oncontrollerchange = function (event) {
+    console.log('Got oncontrollerchange event', event.target);
+  }
+
+  navigator.serviceWorker.onmessage = function (event) {
+    console.log('Got onmessage event', event.target, event.data);
+  }
+
+  if (event.key == 0) {
+    console.log('Keydown 0');
+    TEST(SuccessfulRegistration);
+  } else if (event.key == 1) {
+    console.log('Keydown 1');
+    TEST(SuccessfulActivation);
+  } else if (event.key == 2) {
+    console.log('Keydown 2');
+    unregisterAll().then(() => onEndTest());
+ }
+}
+
+function SuccessfulRegistration() {
+  navigator.serviceWorker.register('service_worker_controller_activation_test_worker.js', {
+    scope: '/testdata',
+  }).then(function (registration) {
+    console.log('(Expected) Registration Succeeded:',
+      registration);
+    assertNotEqual(null, registration);
+    // The default value for RegistrationOptions.type is
+    // 'imports'.
+    assertEqual('imports', registration.updateViaCache);
+    assertTrue(registration.scope.endsWith('/testdata'));
+
+    // Check that the registration has an activated worker after
+    // some time. The delay used here should be long enough for
+    // the service worker to complete activating and have the
+    // state 'activated'. It has to be longer than the combined
+    // delays in the install or activate event handlers.
+    window.setTimeout(function () {
+      // Since these events are asynchronous, the service
+      // worker can be either of these states.
+      assertNotEqual(null, registration.active);
+      registration.active.postMessage('Registration is active after waiting.');
+      assertEqual('activated',
+        registration.active.state);
+      assertNotEqual(null, navigator.serviceWorker.controller);
+      assertEqual('activated',
+        navigator.serviceWorker.controller.state);
+      if (failed()) window.close();
+      console.log('Reload page');
+      window.location.href =
+        window.location.origin + window.location.pathname +
+        '?result=SuccessfulRegistration';
+    }, 1000);
+  }, function (error) {
+    console.log(`(Unexpected): ${error}`, error);
+    notReached();
+    onEndTest();
+  });
+}
+
+function SuccessfulActivation() {
+  navigator.serviceWorker.getRegistration().then(registration => {
+    assertTrue(!!registration);
+    assertEqual('imports', registration.updateViaCache);
+    assertNotEqual(null, registration.active);
+    assertEqual('activated', registration.active.state);
+
+    if (failed()) window.close();
+    console.log('Reload page');
+    window.location.href =
+      window.location.origin + window.location.pathname +
+      '?result=Success';
+  })
+    .catch(error => {
+      console.log(`(Unexpected): ${error}`, error);
+      notReached();
+      onEndTest();
+    });
+  assertNotEqual(null, navigator.serviceWorker.controller);
+  assertEqual('activated',
+    navigator.serviceWorker.controller.state);
+}
+
+var search = document.createElement('div');
+search.id = window.location.search.replace(/\?result=/, 'Result');
+document.body.appendChild(search);
+
+setupFinished();
diff --git a/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_worker.js b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_worker.js
new file mode 100644
index 0000000..4bda3e0
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_controller_activation_test_worker.js
@@ -0,0 +1,26 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+console.log('Service Worker Script Started');
+self.onmessage = function (event) {
+  console.log('Got onmessage event', event.source, event.target, event.data);
+}
+
+self.oninstall = function (e) {
+  console.log('oninstall event received', e);
+}
+
+self.onactivate = function (e) {
+  console.log('onactivate event received', e);
+}
diff --git a/cobalt/black_box_tests/testdata/service_worker_csp_test.html b/cobalt/black_box_tests/testdata/service_worker_csp_test.html
new file mode 100644
index 0000000..ff058e3
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_csp_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<head>
+  <title>Verify that service worker registration correctly follow CSP</title>
+  <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+  <script>
+  var window_onerror_count = 0;
+  window.onerror = (message, filename, lineno, colno, error) => {
+    ++window_onerror_count;
+    // Note: Worker execution errors currently don't pass line or column
+    // number in the error message.
+    assertIncludes('SecurityError', message);
+    assertIncludes('worker_csp_test.js', filename);
+  }
+
+  // This worker attempts to do an XHR request that is blocked by CSP.
+  navigator.serviceWorker.register(
+    'worker_csp_test.js', { scope: './' })
+    .then(registration => {
+      notReached();
+    })
+    .catch(error => {
+      console.log(`Registration failed: ${error}`);
+      assertIncludes('TypeError', error);
+    });
+
+  window.setTimeout(
+    () => {
+      assertEqual(0, window_onerror_count);
+      onEndTest();
+    }, 250);
+  </script>
+</body>
diff --git a/cobalt/black_box_tests/testdata/service_worker_fetch_test.js b/cobalt/black_box_tests/testdata/service_worker_fetch_test.js
index 22da79e..6b9ca7d 100644
--- a/cobalt/black_box_tests/testdata/service_worker_fetch_test.js
+++ b/cobalt/black_box_tests/testdata/service_worker_fetch_test.js
@@ -89,6 +89,8 @@
 self.addEventListener('fetch', event => {
   fetchEventCount++;
   if (shouldIntercept) {
-    event.respondWith(Promise.resolve(new Response(interceptedBody)));
+    event.respondWith(new Promise(resolve => {
+      setTimeout(() => resolve(new Response(interceptedBody)), 100);
+    }));
   }
 });
diff --git a/cobalt/black_box_tests/testdata/service_worker_persist_test.html b/cobalt/black_box_tests/testdata/service_worker_persist_test.html
new file mode 100644
index 0000000..6d6e629
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_persist_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  Note: This is a test of the Service Worker Registration Lifetime.
+-->
+
+<html>
+
+<head>
+    <title>Cobalt Service Worker Test</title>
+    <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+    <script src='service_worker_persist_test_script.js'></script>
+</body>
+
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js b/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js
new file mode 100644
index 0000000..82d19b4
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js
@@ -0,0 +1,162 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+* @param {KeyboardEvent} event
+*/
+window.onkeydown = function(event) {
+    assertEqual(undefined, self.clients);
+
+    if (!('serviceWorker' in navigator)) {
+        console.log("serviceWorker not in navigator, ending test");
+        onEndTest();
+    }
+
+    navigator.serviceWorker.ready.then(function (registration) {
+        assertNotEqual(null, registration);
+        console.log('(Expected) Registration ready promise',
+            registration, 'with active worker ', registration.active);
+        assertNotEqual(null, registration.active);
+    });
+
+    navigator.serviceWorker.oncontrollerchange = function (event) {
+        console.log('Got oncontrollerchange event', event.target);
+    }
+
+    navigator.serviceWorker.onmessage = function (event) {
+        console.log('Got onmessage event', event.target, event.data);
+    }
+
+    if (event.key == 0) {
+        h5vcc.storage.enableCache();
+        test_successful_registration();
+    } else if (event.key == 1) {
+        test_persistent_registration();
+    } else if (event.key == 2) {
+        test_unregistered_persistent_registration_does_not_exist();
+    }
+}
+
+function test_successful_registration() {
+    console.log('test_successful_registration()');
+    navigator.serviceWorker.register('service_worker_test_persisted_worker.js', {
+        scope: '/bar/registration/scope',
+    }).then(function (registration) {
+        console.log('(Expected) Registration Succeeded:',
+            registration);
+        assertNotEqual(null, registration);
+        // The default value for RegistrationOptions.type is
+        // 'imports'.
+        assertEqual('imports', registration.updateViaCache);
+        assertTrue(registration.scope.endsWith(
+            'bar/registration/scope'));
+
+        // Check that the registration has an activated worker after
+        // some time. The delay used here should be long enough for
+        // the service worker to complete activating and have the
+        // state 'activated'. It has to be longer than the combined
+        // delays in the install or activate event handlers.
+        window.setTimeout(function () {
+            // Since these events are asynchronous, the service
+            // worker can be either of these states.
+            console.log(
+                'Registration active check after timeout',
+                registration);
+            assertNotEqual(null, registration.active);
+            console.log('Registration active',
+                registration.active, 'state:',
+                registration.active.state);
+            assertEqual('activated',
+                registration.active.state);
+            onEndTest();
+        }, 1000);
+    }, function (error) {
+        console.log(`(Unexpected): ${error}`, error);
+        notReached();
+        onEndTest();
+    });
+}
+
+function test_persistent_registration() {
+    console.log("test_persistent_registration()");
+    navigator.serviceWorker.getRegistration(
+        '/bar/registration/scope')
+        .then(function (registration) {
+            console.log('(Expected) getRegistration Succeeded:',
+                registration);
+            assertNotEqual(null, registration);
+            assertEqual('imports', registration.updateViaCache);
+            assertTrue(registration.scope.endsWith(
+                'bar/registration/scope'));
+
+            window.setTimeout(function () {
+                console.log(
+                    'Registration active check after timeout',
+                    registration);
+                assertNotEqual(null, registration.active);
+                console.log('Registration active',
+                    registration.active, 'state:',
+                    registration.active.state);
+                assertEqual('activated',
+                    registration.active.state);
+
+                registration.unregister()
+                .then(function (success) {
+                    console.log('(Expected) unregister success :',
+                        success);
+                    // Finally, test getRegistration for the
+                    // unregistered scope.
+                    navigator.serviceWorker.getRegistration(
+                        'bar/registration/scope')
+                        .then(function (registration) {
+                            assertTrue(null == registration ||
+                                undefined == registration);
+                            onEndTest();
+                        }, function (error) {
+                            console.log(`(Unexpected): ${error}`,
+                                error);
+                            notReached();
+                            onEndTest();
+                        });
+                }, function (error) {
+                    console.log('(Unexpected) unregister ' +
+                        `${error}`, error);
+                    assertIncludes('SecurityError: ', `${error}`);
+                    notReached();
+                    onEndTest();
+                });
+            }, 1000);
+        }, function (error) {
+            console.log(`(Unexpected): ${error}`, error);
+            notReached();
+            onEndTest();
+        });
+}
+
+function test_unregistered_persistent_registration_does_not_exist() {
+    console.log("test_unregistered_persistent_registration_does_not_exist()");
+    navigator.serviceWorker.getRegistration(
+        '/bar/registration/scope')
+        .then(function (registration) {
+            console.log('(Expected) getRegistration Succeeded:',
+                registration);
+            assertEqual(null, registration);
+            onEndTest();
+        }, function (error) {
+            console.log(`(Unexpected): ${error}`, error);
+            notReached();
+            onEndTest();
+        });
+}
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_persisted_worker.js b/cobalt/black_box_tests/testdata/service_worker_test_persisted_worker.js
new file mode 100644
index 0000000..003deda
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_test_persisted_worker.js
@@ -0,0 +1,15 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+console.log('Service Worker Script Started');
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_script.js b/cobalt/black_box_tests/testdata/service_worker_test_script.js
index 20ce1f7..55d40eb 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test_script.js
+++ b/cobalt/black_box_tests/testdata/service_worker_test_script.js
@@ -258,6 +258,11 @@
             'Registration successful, current worker state is ' +
             'active.');
       }
+      // TODO (b/259734597) : This registration is persisting since it is
+      // not unregistered and h5vcc storage clearCache does not correctly clear
+      // registrations.
+      // TODO (b/259731731) : Adding registration.unregister() causes memory
+      // leaks.
     })
     .catch(fail),
 ]);
diff --git a/cobalt/black_box_tests/testdata/worker_csp_test.html b/cobalt/black_box_tests/testdata/worker_csp_test.html
index fdb1cea..ba09f97 100644
--- a/cobalt/black_box_tests/testdata/worker_csp_test.html
+++ b/cobalt/black_box_tests/testdata/worker_csp_test.html
@@ -20,20 +20,33 @@
   <script src='black_box_js_test_utils.js'></script>
 </head>
 
-<body style="background-color: white;">
-  <h1>
-    <span>ID element</span>
-  </h1>
+<body>
   <script>
-  var worker = new Worker('worker_csp_test.js');
+  var worker;
+  var window_onerror_count = 0;
+  window.onerror = (message, filename, lineno, colno, error) => {
+    ++window_onerror_count;
+    // Note: Worker execution errors currently don't pass line or column
+    // number in the error message.
+    assertIncludes('SecurityError', message);
+    assertIncludes('worker_csp_test.js', filename);
+    assertEqual(1, window_onerror_count);
+    window.setTimeout(
+      () => {
+        worker.terminate();
+        onEndTest();
+      }, 250);
+  }
+
+  // This worker attempts to do an XHR request that is blocked by CSP.
+  worker = new Worker('worker_csp_test.js');
   worker.onmessage = function (event) {
     notReached();
   };
+  worker.onerror = function (event) {
+    // Note: The Worker's onerror handler (incorrectly) isn't called.
+    notReached();
+  };
 
-  window.setTimeout(
-    () => {
-      worker.terminate();
-      onEndTest();
-    }, 250);
   </script>
 </body>
diff --git a/cobalt/black_box_tests/testdata/worker_load_csp_test.html b/cobalt/black_box_tests/testdata/worker_load_csp_test.html
new file mode 100644
index 0000000..f265f05
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_load_csp_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<head>
+  <title>Verify that worker loading correctly follows CSP</title>
+  <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+  <script>
+  var window_onerror_count = 0;
+  window.onerror = (message, filename, lineno, colno, error) => {
+    ++window_onerror_count;
+    if (message.includes('blocked_worker.js')) {
+      assertIncludes('rejected by security policy', message);
+      assertIncludes('worker_load_csp_test.html', filename);
+    } else {
+      notReached();
+    }
+    if (window_onerror_count == 1) {
+      window.setTimeout(
+        () => {
+          assertEqual(1, window_onerror_count);
+          onEndTest();
+        }, 250);
+
+    }
+  }
+
+  // This worker is blocked because the URL isn't allowed by CSP.
+  try {
+    var blocked_worker = new Worker('https://www.google.com/blocked_worker.js');
+    blocked_worker.onerror = function (event) {
+      // Note: The Worker's onerror handler (incorrectly) isn't called.
+      notReached();
+    };
+  } catch (error) {
+    // The error is thrown asynchronously after the Worker constructor.
+    notReached();
+  }
+  </script>
+</body>
diff --git a/cobalt/black_box_tests/testdata/worker_load_test.html b/cobalt/black_box_tests/testdata/worker_load_test.html
new file mode 100644
index 0000000..7f13191
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_load_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<head>
+  <title>Verify that worker loading is blocked and reported as expected</title>
+  <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+  <script>
+  var window_onerror_count = 0;
+  window.onerror = (message, filename, lineno, colno, error) => {
+    ++window_onerror_count;
+    console.log(message);
+    if (message.includes('nonexisting_worker.js')) {
+      assertIncludes('aborted or failed with code 404', message);
+      assertIncludes('worker_load_test.html', filename);
+    } else {
+      notReached();
+    }
+    if (window_onerror_count == 1) {
+      window.setTimeout(
+        () => {
+          assertEqual(1, window_onerror_count);
+          onEndTest();
+        }, 250);
+
+    }
+  }
+
+  // This worker is blocked because the URL can't resolve.
+  try {
+    var blocked_worker = new Worker('..:/blocked_worker.js');
+    notReached();
+  } catch(error) {
+    console.log(error);
+    assertEqual('SyntaxError', error.name);
+  }
+
+  // This worker is blocked because the script does not exist.
+  try {
+    var nonexisting_worker = new Worker('nonexisting_worker.js');
+    nonexisting_worker.onerror = function (event) {
+      // Note: The Worker's onerror handler (incorrectly) isn't called.
+      notReached();
+    };
+  } catch (error) {
+    // The error is thrown asynchronously after the Worker constructor.
+    notReached();
+  }
+  </script>
+</body>
diff --git a/cobalt/black_box_tests/tests/deep_links.py b/cobalt/black_box_tests/tests/deep_links.py
index 4a69ffe..211e867 100644
--- a/cobalt/black_box_tests/tests/deep_links.py
+++ b/cobalt/black_box_tests/tests/deep_links.py
@@ -78,6 +78,14 @@
 class DeepLink(black_box_tests.BlackBoxTestCase):
   """Tests firing deep links before web module is loaded."""
 
+  def setUp(self) -> None:
+    _script_loading_signal.clear()
+    return super().setUp()
+
+  def tearDown(self) -> None:
+    _script_loading_signal.clear()
+    return super().tearDown()
+
   def _load_page(self, webdriver, url):
     """Instructs webdriver to navigate to url."""
     try:
diff --git a/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py b/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py
new file mode 100644
index 0000000..4727fde
--- /dev/null
+++ b/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py
@@ -0,0 +1,32 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+"""Test that H5vccStorage::WriteTest and VerifyTest work on all platforms"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class H5vccStorageWriteVerifyTest(black_box_tests.BlackBoxTestCase):
+
+  def test_h5vcc_storage_write_verify_test(self):
+    with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(
+          file_name='testdata/h5vcc_storage_write_verify_test.html')
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForActiveElement()
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py b/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py
new file mode 100644
index 0000000..996e3ad
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py
@@ -0,0 +1,28 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker getRegistrations() functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class ServiceWorkerCacheKeysTest(black_box_tests.BlackBoxTestCase):
+
+  def test_service_worker_cache_keys(self):
+    with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(
+          file_name='testdata/service_worker_cache_keys_test.html')
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForJSTestsSetup()
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_controller_activation_test.py b/cobalt/black_box_tests/tests/service_worker_controller_activation_test.py
new file mode 100644
index 0000000..cf79f71
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_controller_activation_test.py
@@ -0,0 +1,80 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker controller activation functionality."""
+
+import logging
+import os
+from six.moves import SimpleHTTPServer
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import MakeRequestHandlerClass
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+from cobalt.tools.automated_testing import webdriver_utils
+
+# The base path of the requested assets is the parent directory.
+_SERVER_ROOT_PATH = os.path.join(os.path.dirname(__file__), os.pardir)
+
+keys = webdriver_utils.import_selenium_module('webdriver.common.keys')
+
+
+class ServiceWorkerRequestDetector(MakeRequestHandlerClass(_SERVER_ROOT_PATH)):
+  """Proxies everything to SimpleHTTPRequestHandler, except some paths."""
+
+  def end_headers(self):
+    self.send_my_headers()
+    SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self)
+
+  def send_header(self, header, value):
+    # Ensure that the Content-Type for paths ending in '.js' are always
+    # 'text/javascript'.
+    if header == 'Content-Type' and self.path.endswith('.js'):
+      SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(
+          self, header, 'text/javascript')
+    else:
+      SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self, header, value)
+
+  def send_my_headers(self):
+    # Add 'Service-Worker-Allowed' header for the main service worker scripts.
+    if self.path.endswith(
+        '/service_worker_controller_activation_test_worker.js'):
+      self.send_header('Service-Worker-Allowed', '/testdata')
+
+
+class ServiceWorkerControllerActivationTest(black_box_tests.BlackBoxTestCase):
+  """Test basic Service Worker functionality."""
+
+  def test_service_worker_controller_activation(self):
+
+    with ThreadedWebServer(
+        ServiceWorkerRequestDetector,
+        binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(
+          file_name='testdata/service_worker_controller_activation_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForJSTestsSetup()
+
+        logging.info('SendKeys NUMPAD0.')
+        runner.SendKeys(keys.Keys.NUMPAD0)
+        logging.info('Wait.')
+        runner.PollUntilFoundOrTestsFailedWithReconnects(
+            '#ResultSuccessfulRegistration')
+
+        logging.info('SendKeys NUMPAD1.')
+        runner.SendKeys(keys.Keys.NUMPAD1)
+        runner.PollUntilFoundOrTestsFailedWithReconnects('#ResultSuccess')
+
+        logging.info('SendKeys NUMPAD2.')
+        runner.SendKeys(keys.Keys.NUMPAD2)
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_persist_test.py b/cobalt/black_box_tests/tests/service_worker_persist_test.py
new file mode 100644
index 0000000..6505b0c
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_persist_test.py
@@ -0,0 +1,48 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker Persistence functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+from cobalt.tools.automated_testing import webdriver_utils
+from cobalt.black_box_tests.tests.service_worker_test import ServiceWorkerRequestDetector
+
+keys = webdriver_utils.import_selenium_module('webdriver.common.keys')
+
+
+class ServiceWorkerPersistTest(black_box_tests.BlackBoxTestCase):
+  """Test basic Service Worker functionality."""
+
+  def test_service_worker_persist(self):
+
+    with ThreadedWebServer(
+        ServiceWorkerRequestDetector,
+        binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(file_name='testdata/service_worker_persist_test.html')
+
+      # NUMPAD0 calls test_successful_registration()
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForActiveElement()
+        runner.SendKeys(keys.Keys.NUMPAD0)
+        self.assertTrue(runner.JSTestsSucceeded())
+      # NUMPAD1 calls test_persistent_registration()
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForActiveElement()
+        runner.SendKeys(keys.Keys.NUMPAD1)
+        self.assertTrue(runner.JSTestsSucceeded())
+      # NUMPAD2 calls test_persistent_registration_does_not_exist()
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForActiveElement()
+        runner.SendKeys(keys.Keys.NUMPAD2)
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_test.py b/cobalt/black_box_tests/tests/service_worker_test.py
index f66b2dc..d06fecb 100644
--- a/cobalt/black_box_tests/tests/service_worker_test.py
+++ b/cobalt/black_box_tests/tests/service_worker_test.py
@@ -29,7 +29,6 @@
 
   def end_headers(self):
     self.send_my_headers()
-
     SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self)
 
   def send_header(self, header, value):
@@ -43,7 +42,9 @@
 
   def send_my_headers(self):
     # Add 'Service-Worker-Allowed' header for the main service worker scripts.
-    if self.path.endswith('/service_worker_test_worker.js'):
+    if self.path.endswith(
+        '/service_worker_test_worker.js') or self.path.endswith(
+            '/service_worker_test_persisted_worker.js'):
       self.send_header('Service-Worker-Allowed', '/bar')
 
   def do_GET(self):  # pylint: disable=invalid-name
@@ -58,12 +59,11 @@
 
       if not (service_worker_request_header
               == expected_service_worker_request_header):
-        raise ValueError('Service-Worker HTTP request header value does not '
-                         'match with expected value.\n'
-                         'Header value:%s\n'
-                         'Expected value:%s' %
-                         (service_worker_request_header,
-                          expected_service_worker_request_header))
+        raise ValueError(
+            'Service-Worker HTTP request header value does not '
+            'match with expected value.\n'
+            f'Header value:{service_worker_request_header}\n'
+            f'Expected value:{expected_service_worker_request_header}')
 
     return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
 
diff --git a/cobalt/black_box_tests/tests/web_platform_tests.py b/cobalt/black_box_tests/tests/web_platform_tests.py
index 4b64734..d5e1933 100644
--- a/cobalt/black_box_tests/tests/web_platform_tests.py
+++ b/cobalt/black_box_tests/tests/web_platform_tests.py
@@ -69,8 +69,9 @@
           used_filters.append(filter_)
 
       if used_filters:
-        target_params.append('--gtest_filter=-{}'.format(
-            ':'.join(used_filters)))
+        if 'gtest_filter' not in ' '.join(self.launcher_params.target_params):
+          target_params.append('--gtest_filter=-{}'.format(
+              ':'.join(used_filters)))
 
       if self.launcher_params.target_params:
         target_params += self.launcher_params.target_params
diff --git a/cobalt/black_box_tests/tests/worker_csp_test.py b/cobalt/black_box_tests/tests/worker_csp_test.py
index 18b6511..9c988e1 100644
--- a/cobalt/black_box_tests/tests/worker_csp_test.py
+++ b/cobalt/black_box_tests/tests/worker_csp_test.py
@@ -19,9 +19,13 @@
 from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer, MakeCustomHeaderRequestHandlerClass
 
 paths_to_headers = {
+    'worker_load_csp_test.html': {
+        'Content-Security-Policy':
+            "script-src 'unsafe-inline' 'self'; worker-src 'self'"
+    },
     'worker_csp_test.html': {
         'Content-Security-Policy':
-            "script-src 'unsafe-inline' 'self'; connect-src 'self';"
+            "script-src 'unsafe-inline' 'self'; connect-src 'self'"
     },
     'worker_csp_test.js': {
         'Content-Security-Policy': "connect-src 'self';"
@@ -32,7 +36,7 @@
 class WorkerCspTest(black_box_tests.BlackBoxTestCase):
   """Verify correct correct CSP behavior."""
 
-  def test_worker_csp(self):
+  def test_1_worker_csp(self):
     path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
     with ThreadedWebServer(
         binding_address=self.GetBindingAddress(),
@@ -42,3 +46,25 @@
 
       with self.CreateCobaltRunner(url=url) as runner:
         self.assertTrue(runner.JSTestsSucceeded())
+
+  def test_2_worker_load_csp(self):
+    path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+    with ThreadedWebServer(
+        binding_address=self.GetBindingAddress(),
+        handler=MakeCustomHeaderRequestHandlerClass(
+            path, paths_to_headers)) as server:
+      url = server.GetURL(file_name='testdata/worker_load_csp_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        self.assertTrue(runner.JSTestsSucceeded())
+
+  def test_3_service_worker_csp(self):
+    path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+    with ThreadedWebServer(
+        binding_address=self.GetBindingAddress(),
+        handler=MakeCustomHeaderRequestHandlerClass(
+            path, paths_to_headers)) as server:
+      url = server.GetURL(file_name='testdata/service_worker_csp_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/worker_load_test.py b/cobalt/black_box_tests/tests/worker_load_test.py
new file mode 100644
index 0000000..3a337b6
--- /dev/null
+++ b/cobalt/black_box_tests/tests/worker_load_test.py
@@ -0,0 +1,28 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests worker loading violations."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class WorkerLoadTest(black_box_tests.BlackBoxTestCase):
+  """Verify correct correct worker loading behavior."""
+
+  def test_worker_load(self):
+    with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(file_name='testdata/worker_load_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/threaded_web_server.py b/cobalt/black_box_tests/threaded_web_server.py
index 1e173e3..d305ea3 100644
--- a/cobalt/black_box_tests/threaded_web_server.py
+++ b/cobalt/black_box_tests/threaded_web_server.py
@@ -93,7 +93,8 @@
 
     def do_GET(self):  # pylint: disable=invalid-name
       content = self.get_content()
-      self.wfile.write(content)
+      if content:
+        self.wfile.write(content)
 
     def do_HEAD(self):  # pylint: disable=invalid-name
       # Run get_content to send the headers, but ignore the returned results
@@ -172,8 +173,7 @@
     Returns:
       A string containing a HTTP URI.
     """
-    return 'http://{}:{}/{}'.format(self._bound_host, self._bound_port,
-                                    file_name)
+    return f'http://{self._bound_host}:{self._bound_port}/{file_name}'
 
   def __enter__(self):
     self._server_thread = threading.Thread(target=self._server.serve_forever)
diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn
index 4c30f02..e8cf8d3 100644
--- a/cobalt/browser/BUILD.gn
+++ b/cobalt/browser/BUILD.gn
@@ -179,12 +179,12 @@
     "//cobalt/script",
     "//cobalt/script:engine",
     "//cobalt/speech",
-    "//cobalt/sso",
     "//cobalt/storage",
     "//cobalt/subtlecrypto",
     "//cobalt/system_window",
     "//cobalt/trace_event",
     "//cobalt/ui_navigation",
+    "//cobalt/ui_navigation/scroll_engine",
     "//cobalt/watchdog",
     "//cobalt/web",
     "//cobalt/webdriver",
diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc
index c64b761..37c304c 100644
--- a/cobalt/browser/application.cc
+++ b/cobalt/browser/application.cc
@@ -63,8 +63,6 @@
 #include "cobalt/browser/user_agent_string.h"
 #include "cobalt/cache/cache.h"
 #include "cobalt/configuration/configuration.h"
-#include "cobalt/extension/crash_handler.h"
-#include "cobalt/extension/installation_manager.h"
 #include "cobalt/loader/image/image_decoder.h"
 #include "cobalt/math/size.h"
 #include "cobalt/script/javascript_engine.h"
@@ -76,6 +74,8 @@
 #include "cobalt/watchdog/watchdog.h"
 #include "starboard/configuration.h"
 #include "starboard/event.h"
+#include "starboard/extension/crash_handler.h"
+#include "starboard/extension/installation_manager.h"
 #include "starboard/system.h"
 #include "starboard/time.h"
 #include "url/gurl.h"
@@ -572,6 +572,7 @@
                                             user_agent.c_str())) {
       result = false;
     }
+    // TODO(b/265339522): move crashpad prod and ver setter to starboard.
     if (!crash_handler_extension->SetString("prod", product.c_str())) {
       result = false;
     }
diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc
index 0873200..f694893 100644
--- a/cobalt/browser/browser_module.cc
+++ b/cobalt/browser/browser_module.cc
@@ -47,18 +47,19 @@
 #include "cobalt/dom/mutation_observer_task_manager.h"
 #include "cobalt/dom/navigator.h"
 #include "cobalt/dom/window.h"
-#include "cobalt/extension/graphics.h"
 #include "cobalt/h5vcc/h5vcc.h"
 #include "cobalt/input/input_device_manager_fuzzer.h"
 #include "cobalt/math/matrix3_f.h"
 #include "cobalt/overlay_info/overlay_info_registry.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
 #include "cobalt/web/csp_delegate_factory.h"
 #include "cobalt/web/navigator_ua_data.h"
 #include "nb/memory_scope.h"
 #include "starboard/atomic.h"
 #include "starboard/common/string.h"
 #include "starboard/configuration.h"
+#include "starboard/extension/graphics.h"
 #include "starboard/system.h"
 #include "starboard/time.h"
 #include "third_party/icu/source/i18n/unicode/timezone.h"
@@ -227,6 +228,7 @@
       event_dispatcher_(event_dispatcher),
       account_manager_(account_manager),
       is_rendered_(false),
+      is_web_module_rendered_(false),
       can_play_type_handler_(media::MediaModule::CreateCanPlayTypeHandler()),
       network_module_(network_module),
 #if SB_IS(EVERGREEN)
@@ -283,14 +285,15 @@
       main_web_module_generation_(0),
       next_timeline_id_(1),
       current_splash_screen_timeline_id_(-1),
-      current_main_web_module_timeline_id_(-1),
-      service_worker_registry_(network_module) {
+      current_main_web_module_timeline_id_(-1) {
   TRACE_EVENT0("cobalt::browser", "BrowserModule::BrowserModule()");
 
   // Apply platform memory setting adjustments and defaults.
   ApplyAutoMemSettings();
 
   platform_info_.reset(new browser::UserAgentPlatformInfo());
+  service_worker_registry_.reset(new ServiceWorkerRegistry(
+      &web_settings_, network_module, platform_info_.get()));
 
 #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
   SbCoreDumpRegisterHandler(BrowserModule::CoreDumpHandler, this);
@@ -400,7 +403,7 @@
       platform_info_.get(), application_state_,
       base::Bind(&BrowserModule::QueueOnDebugConsoleRenderTreeProduced,
                  base::Unretained(this)),
-      network_module_, GetViewportSize(), GetResourceProvider(),
+      &web_settings_, network_module_, GetViewportSize(), GetResourceProvider(),
       kLayoutMaxRefreshFrequencyInHz,
       base::Bind(&BrowserModule::CreateDebugClient, base::Unretained(this)),
       base::Bind(&BrowserModule::OnMaybeFreeze, base::Unretained(this))));
@@ -545,7 +548,7 @@
           platform_info_.get(), application_state_,
           base::Bind(&BrowserModule::QueueOnSplashScreenRenderTreeProduced,
                      base::Unretained(this)),
-          network_module_, viewport_size, GetResourceProvider(),
+          &web_settings_, network_module_, viewport_size, GetResourceProvider(),
           kLayoutMaxRefreshFrequencyInHz, fallback_splash_screen_url_,
           splash_screen_cache_.get(),
           base::Bind(&BrowserModule::DestroySplashScreen, weak_this_),
@@ -554,6 +557,9 @@
     }
   }
 
+  scroll_engine_.reset(new ui_navigation::scroll_engine::ScrollEngine());
+  scroll_engine_->thread()->Start();
+
 // Create new WebModule.
 #if !defined(COBALT_FORCE_CSP)
   options_.web_module_options.csp_insecure_allowed_token =
@@ -605,13 +611,14 @@
   options.maybe_freeze_callback =
       base::Bind(&BrowserModule::OnMaybeFreeze, base::Unretained(this));
 
+  options.web_options.web_settings = &web_settings_;
   options.web_options.network_module = network_module_;
   options.web_options.service_worker_jobs =
-      service_worker_registry_.service_worker_jobs();
+      service_worker_registry_->service_worker_jobs();
   options.web_options.platform_info = platform_info_.get();
   web_module_.reset(new WebModule("MainWebModule"));
   web_module_->Run(
-      url, application_state_,
+      url, application_state_, scroll_engine_.get(),
       base::Bind(&BrowserModule::QueueOnRenderTreeProduced,
                  base::Unretained(this), main_web_module_generation_),
       base::Bind(&BrowserModule::OnError, base::Unretained(this)),
@@ -792,8 +799,9 @@
   // explicitly.
   renderer_submission.timeline_info.id = current_main_web_module_timeline_id_;
 
-  renderer_submission.on_rasterized_callbacks.push_back(base::Bind(
-      &BrowserModule::OnRendererSubmissionRasterized, base::Unretained(this)));
+  renderer_submission.on_rasterized_callbacks.push_back(
+      base::Bind(&BrowserModule::OnWebModuleRendererSubmissionRasterized,
+                 base::Unretained(this)));
 
   if (!splash_screen_) {
     render_tree_combiner_.SetTimelineLayer(main_web_module_layer_.get());
@@ -1104,6 +1112,12 @@
   }
 #endif  // defined(ENABLE_DEBUGGER)
 
+  scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::Bind(
+          &ui_navigation::scroll_engine::ScrollEngine::HandlePointerEvent,
+          base::Unretained(scroll_engine_.get()), type, event));
+
   DCHECK(web_module_);
   web_module_->InjectPointerEvent(type, event);
 }
@@ -1505,6 +1519,23 @@
   }
 }
 
+void BrowserModule::OnWebModuleRendererSubmissionRasterized() {
+  TRACE_EVENT0("cobalt::browser",
+               "BrowserModule::OnFirstWebModuleSubmissionRasterized()");
+  OnRendererSubmissionRasterized();
+  if (!is_web_module_rendered_) {
+    is_web_module_rendered_ = true;
+    const CobaltExtensionGraphicsApi* graphics_extension =
+        static_cast<const CobaltExtensionGraphicsApi*>(
+            SbSystemGetExtension(kCobaltExtensionGraphicsName));
+    if (graphics_extension &&
+        strcmp(graphics_extension->name, kCobaltExtensionGraphicsName) == 0 &&
+        graphics_extension->version >= 6) {
+      graphics_extension->ReportFullyDrawn();
+    }
+  }
+}
+
 #if defined(COBALT_CHECK_RENDER_TIMEOUT)
 void BrowserModule::OnPollForRenderTimeout(const GURL& url) {
   SbTime last_render_timestamp = static_cast<SbTime>(SbAtomicAcquire_Load64(
@@ -2037,8 +2068,9 @@
   // The web_module_ member can not be safely used in this function.
 
   h5vcc::H5vcc::Settings h5vcc_settings;
-  h5vcc_settings.set_media_source_setting_func = base::Bind(
-      &WebModule::SetMediaSourceSetting, base::Unretained(web_module));
+  h5vcc_settings.set_web_setting_func =
+      base::Bind(&web::WebSettingsImpl::Set, base::Unretained(&web_settings_));
+  h5vcc_settings.media_module = media_module_.get();
   h5vcc_settings.network_module = network_module_;
 #if SB_IS(EVERGREEN)
   h5vcc_settings.updater_module = updater_module_;
diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h
index 646b750..05aece7 100644
--- a/cobalt/browser/browser_module.h
+++ b/cobalt/browser/browser_module.h
@@ -68,6 +68,8 @@
 #include "cobalt/script/array_buffer.h"
 #include "cobalt/storage/storage_manager.h"
 #include "cobalt/system_window/system_window.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
+#include "cobalt/web/web_settings.h"
 #include "cobalt/webdriver/session_driver.h"
 #include "starboard/configuration.h"
 #include "starboard/time.h"
@@ -381,6 +383,10 @@
   // system splash screen after the first render has completed.
   void OnRendererSubmissionRasterized();
 
+  // Called when a renderer submission of the (main) web module has been
+  // rasterized. This should also call OnRendererSubmissionRasterized().
+  void OnWebModuleRendererSubmissionRasterized();
+
   // Process all messages queued into the |render_tree_submission_queue_|.
   void ProcessRenderTreeSubmissionQueue();
 
@@ -490,6 +496,11 @@
   // |weak_ptr_factory_.GetWeakPtr() which is not).
   base::WeakPtr<BrowserModule> weak_this_;
 
+  // Holds browser wide web settings accessible from both the main web thread
+  // and all Workers.  It can only be set on the main web module via h5vcc but
+  // any setting changes affect all web modules.
+  web::WebSettingsImpl web_settings_;
+
   // Memory configuration tool.
   memory_settings::AutoMem auto_mem_;
 
@@ -514,6 +525,9 @@
   // render, we hide the system splash screen.
   bool is_rendered_;
 
+  // Whether the (main) web module has yet rendered anything.
+  bool is_web_module_rendered_;
+
   // The main system window for our application. This routes input event
   // callbacks, and provides a native window handle on desktop systems.
   std::unique_ptr<system_window::SystemWindow> system_window_;
@@ -569,6 +583,9 @@
   std::unique_ptr<dom::OnScreenKeyboardBridge> on_screen_keyboard_bridge_;
   bool on_screen_keyboard_show_called_ = false;
 
+  // Handles pointer events for scroll containers.
+  std::unique_ptr<ui_navigation::scroll_engine::ScrollEngine> scroll_engine_;
+
   // Sets up everything to do with web page management, from loading and
   // parsing the web page and all referenced files to laying it out.  The
   // web module will ultimately produce a render tree that can be passed
@@ -717,7 +734,7 @@
   std::unique_ptr<browser::UserAgentPlatformInfo> platform_info_;
 
   // Manages the Service Workers.
-  ServiceWorkerRegistry service_worker_registry_;
+  std::unique_ptr<ServiceWorkerRegistry> service_worker_registry_;
 };
 
 }  // namespace browser
diff --git a/cobalt/browser/debug_console.cc b/cobalt/browser/debug_console.cc
index 1f0bba5..197233b 100644
--- a/cobalt/browser/debug_console.cc
+++ b/cobalt/browser/debug_console.cc
@@ -109,7 +109,7 @@
     base::ApplicationState initial_application_state,
     const WebModule::OnRenderTreeProducedCallback&
         render_tree_produced_callback,
-    network::NetworkModule* network_module,
+    web::WebSettings* web_settings, network::NetworkModule* network_module,
     const cssom::ViewportSize& window_dimensions,
     render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
     const debug::CreateDebugClientCallback& create_debug_client_callback,
@@ -140,12 +140,13 @@
   // Pass down this callback from Browser module to Web module eventually.
   web_module_options.maybe_freeze_callback = maybe_freeze_callback;
 
+  web_module_options.web_options.web_settings = web_settings;
   web_module_options.web_options.network_module = network_module;
   web_module_options.web_options.platform_info = platform_info;
 
   web_module_.reset(new WebModule("DebugConsoleWebModule"));
   web_module_->Run(GURL(kInitialDebugConsoleUrl), initial_application_state,
-                   render_tree_produced_callback,
+                   nullptr /* scroll_engine */, render_tree_produced_callback,
                    base::Bind(&DebugConsole::OnError, base::Unretained(this)),
                    WebModule::CloseCallback(), /* window_close_callback */
                    base::Closure(),            /* window_minimize_callback */
diff --git a/cobalt/browser/debug_console.h b/cobalt/browser/debug_console.h
index 0ed23a9..c77e1ec 100644
--- a/cobalt/browser/debug_console.h
+++ b/cobalt/browser/debug_console.h
@@ -35,6 +35,7 @@
 #include "cobalt/dom/wheel_event_init.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -49,7 +50,7 @@
       base::ApplicationState initial_application_state,
       const WebModule::OnRenderTreeProducedCallback&
           render_tree_produced_callback,
-      network::NetworkModule* network_module,
+      web::WebSettings* web_settings, network::NetworkModule* network_module,
       const cssom::ViewportSize& window_dimensions,
       render_tree::ResourceProvider* resource_provider,
       float layout_refresh_rate,
diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni
index e9dcf26..b64086d 100644
--- a/cobalt/browser/idl_files.gni
+++ b/cobalt/browser/idl_files.gni
@@ -166,7 +166,6 @@
   "//cobalt/h5vcc/h5vcc_runtime.idl",
   "//cobalt/h5vcc/h5vcc_runtime_event_target.idl",
   "//cobalt/h5vcc/h5vcc_settings.idl",
-  "//cobalt/h5vcc/h5vcc_sso.idl",
   "//cobalt/h5vcc/h5vcc_storage.idl",
   "//cobalt/h5vcc/h5vcc_screen.idl",
   "//cobalt/h5vcc/h5vcc_system.idl",
diff --git a/cobalt/browser/on_screen_keyboard_starboard_bridge.h b/cobalt/browser/on_screen_keyboard_starboard_bridge.h
index 682f2f4..0a68c30 100644
--- a/cobalt/browser/on_screen_keyboard_starboard_bridge.h
+++ b/cobalt/browser/on_screen_keyboard_starboard_bridge.h
@@ -19,7 +19,7 @@
 
 #include "base/callback.h"
 #include "cobalt/dom/on_screen_keyboard_bridge.h"
-#include "cobalt/extension/on_screen_keyboard.h"
+#include "starboard/extension/on_screen_keyboard.h"
 #include "starboard/window.h"
 
 namespace cobalt {
diff --git a/cobalt/browser/service_worker_registry.cc b/cobalt/browser/service_worker_registry.cc
index 44027a4..2fdd068 100644
--- a/cobalt/browser/service_worker_registry.cc
+++ b/cobalt/browser/service_worker_registry.cc
@@ -33,19 +33,21 @@
 }  // namespace
 
 void ServiceWorkerRegistry::WillDestroyCurrentMessageLoop() {
-  // Clear all member variables allocated form the thread.
+  // Clear all member variables allocated from the thread.
   service_worker_jobs_.reset();
 }
 
 ServiceWorkerRegistry::ServiceWorkerRegistry(
-    network::NetworkModule* network_module)
+    web::WebSettings* web_settings, network::NetworkModule* network_module,
+    web::UserAgentPlatformInfo* platform_info)
     : thread_("ServiceWorkerRegistry") {
   if (!thread_.Start()) return;
   DCHECK(message_loop());
 
   message_loop()->task_runner()->PostTask(
-      FROM_HERE, base::Bind(&ServiceWorkerRegistry::Initialize,
-                            base::Unretained(this), network_module));
+      FROM_HERE,
+      base::Bind(&ServiceWorkerRegistry::Initialize, base::Unretained(this),
+                 web_settings, network_module, platform_info));
 
   // Register as a destruction observer to shut down the Web Agent once all
   // pending tasks have been executed and the message loop is about to be
@@ -81,11 +83,13 @@
   return service_worker_jobs_.get();
 }
 
-void ServiceWorkerRegistry::Initialize(network::NetworkModule* network_module) {
+void ServiceWorkerRegistry::Initialize(
+    web::WebSettings* web_settings, network::NetworkModule* network_module,
+    web::UserAgentPlatformInfo* platform_info) {
   TRACE_EVENT0("cobalt::browser", "ServiceWorkerRegistry::Initialize()");
   DCHECK_EQ(base::MessageLoop::current(), message_loop());
-  service_worker_jobs_.reset(
-      new worker::ServiceWorkerJobs(network_module, message_loop()));
+  service_worker_jobs_.reset(new worker::ServiceWorkerJobs(
+      web_settings, network_module, platform_info, message_loop()));
 }
 
 }  // namespace browser
diff --git a/cobalt/browser/service_worker_registry.h b/cobalt/browser/service_worker_registry.h
index 4b4239b..14abedd 100644
--- a/cobalt/browser/service_worker_registry.h
+++ b/cobalt/browser/service_worker_registry.h
@@ -21,6 +21,7 @@
 #include "base/synchronization/waitable_event.h"
 #include "base/threading/thread.h"
 #include "cobalt/network/network_module.h"
+#include "cobalt/web/web_settings.h"
 #include "cobalt/worker/service_worker_jobs.h"
 
 namespace cobalt {
@@ -31,7 +32,9 @@
 // metadata are stored persistently on disk.
 class ServiceWorkerRegistry : public base::MessageLoop::DestructionObserver {
  public:
-  explicit ServiceWorkerRegistry(network::NetworkModule* network_module);
+  ServiceWorkerRegistry(web::WebSettings* web_settings,
+                        network::NetworkModule* network_module,
+                        web::UserAgentPlatformInfo* platform_info);
   ~ServiceWorkerRegistry();
 
   // The message loop this object is running on.
@@ -45,7 +48,9 @@
  private:
   // Called by the constructor to perform any other initialization required on
   // the dedicated thread.
-  void Initialize(network::NetworkModule* network_module);
+  void Initialize(web::WebSettings* web_settings,
+                  network::NetworkModule* network_module,
+                  web::UserAgentPlatformInfo* platform_info);
 
   // The thread created and owned by the Service Worker Registry.
   // All registry mutations occur on this thread. The thread has to outlive all
diff --git a/cobalt/browser/splash_screen.cc b/cobalt/browser/splash_screen.cc
index 952f790..ea764a6 100644
--- a/cobalt/browser/splash_screen.cc
+++ b/cobalt/browser/splash_screen.cc
@@ -55,7 +55,7 @@
     base::ApplicationState initial_application_state,
     const WebModule::OnRenderTreeProducedCallback&
         render_tree_produced_callback,
-    network::NetworkModule* network_module,
+    web::WebSettings* web_settings, network::NetworkModule* network_module,
     const cssom::ViewportSize& window_dimensions,
     render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
     const base::Optional<GURL>& fallback_splash_screen_url,
@@ -102,14 +102,15 @@
   // Pass down this callback from Browser module to Web module eventually.
   web_module_options.maybe_freeze_callback = maybe_freeze_callback;
 
+  web_module_options.web_options.web_settings = web_settings;
   web_module_options.web_options.network_module = network_module;
   web_module_options.web_options.platform_info = platform_info;
 
   DCHECK(url_to_pass);
   web_module_.reset(new WebModule("SplashScreenWebModule"));
   web_module_->Run(*url_to_pass, initial_application_state,
-                   render_tree_produced_callback_, base::Bind(&OnError),
-                   on_window_close,
+                   nullptr /* scroll_engine */, render_tree_produced_callback_,
+                   base::Bind(&OnError), on_window_close,
                    base::Closure(),  // window_minimize_callback
                    NULL /* can_play_type_handler */, NULL /* media_module */,
                    window_dimensions, resource_provider, layout_refresh_rate,
diff --git a/cobalt/browser/splash_screen.h b/cobalt/browser/splash_screen.h
index 6453862..f50c2e4 100644
--- a/cobalt/browser/splash_screen.h
+++ b/cobalt/browser/splash_screen.h
@@ -26,6 +26,7 @@
 #include "cobalt/cssom/viewport_size.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -39,6 +40,7 @@
                base::ApplicationState initial_application_state,
                const WebModule::OnRenderTreeProducedCallback&
                    render_tree_produced_callback,
+               web::WebSettings* web_settings,
                network::NetworkModule* network_module,
                const cssom::ViewportSize& window_dimensions,
                render_tree::ResourceProvider* resource_provider,
diff --git a/cobalt/browser/user_agent_platform_info.cc b/cobalt/browser/user_agent_platform_info.cc
index fe9f3e1..e89f79e 100644
--- a/cobalt/browser/user_agent_platform_info.cc
+++ b/cobalt/browser/user_agent_platform_info.cc
@@ -21,15 +21,15 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "cobalt/browser/switches.h"
-#if SB_IS(EVERGREEN)
-#include "cobalt/extension/installation_manager.h"
-#endif  // SB_IS(EVERGREEN)
 #include "cobalt/renderer/get_default_rasterizer_for_platform.h"
 #include "cobalt/script/javascript_engine.h"
 #include "cobalt/version.h"
 #include "cobalt_build_id.h"  // NOLINT(build/include_subdir)
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
+#if SB_IS(EVERGREEN)
+#include "starboard/extension/installation_manager.h"
+#endif  // SB_IS(EVERGREEN)
 #include "starboard/system.h"
 #if SB_IS(EVERGREEN)
 #include "cobalt/updater/utils.h"
diff --git a/cobalt/browser/web_module.cc b/cobalt/browser/web_module.cc
index 6cb0ea4..dc7ae32 100644
--- a/cobalt/browser/web_module.cc
+++ b/cobalt/browser/web_module.cc
@@ -48,7 +48,6 @@
 #include "cobalt/dom/keyboard_event.h"
 #include "cobalt/dom/keyboard_event_init.h"
 #include "cobalt/dom/local_storage_database.h"
-#include "cobalt/dom/media_source_settings.h"
 #include "cobalt/dom/mutation_observer_task_manager.h"
 #include "cobalt/dom/navigation_type.h"
 #include "cobalt/dom/navigator.h"
@@ -67,6 +66,7 @@
 #include "cobalt/media/media_module.h"
 #include "cobalt/media_session/media_session_client.h"
 #include "cobalt/storage/storage_manager.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
 #include "cobalt/web/blob.h"
 #include "cobalt/web/context.h"
 #include "cobalt/web/csp_delegate_factory.h"
@@ -216,8 +216,6 @@
   void SetSize(cssom::ViewportSize viewport_size);
   void UpdateCamera3D(const scoped_refptr<input::Camera3D>& camera_3d);
   void SetMediaModule(media::MediaModule* media_module);
-  void SetMediaSourceSetting(const std::string& name, int value,
-                             bool* succeeded);
   void SetImageCacheCapacity(int64_t bytes);
   void SetRemoteTypefaceCacheCapacity(int64_t bytes);
 
@@ -343,6 +341,8 @@
 
   web::Context* web_context_;
 
+  ui_navigation::scroll_engine::ScrollEngine* scroll_engine_;
+
   // Simple flag used for basic error checking.
   bool is_running_;
 
@@ -399,9 +399,6 @@
   // Object to register and retrieve MediaSource object with a string key.
   std::unique_ptr<dom::MediaSource::Registry> media_source_registry_;
 
-  // Object to hold WebModule wide settings for MediaSource related objects.
-  dom::MediaSourceSettingsImpl media_source_settings_;
-
   // The Window object wraps all DOM-related components.
   scoped_refptr<dom::Window> window_;
 
@@ -488,6 +485,7 @@
 
 WebModule::Impl::Impl(web::Context* web_context, const ConstructionData& data)
     : web_context_(web_context),
+      scroll_engine_(data.scroll_engine),
       is_running_(false),
       is_render_tree_rasterization_pending_(
           base::StringPrintf("%s.IsRenderTreeRasterizationPending",
@@ -505,7 +503,6 @@
       base::Bind(&WebModule::Impl::LogScriptError, base::Unretained(this));
   web_context_->javascript_engine()->RegisterErrorHandler(error_handler);
 #endif
-
   css_parser::Parser::SupportsMapToMeshFlag supports_map_to_mesh =
       data.options.enable_map_to_mesh
           ? css_parser::Parser::kSupportsMapToMesh
@@ -586,8 +583,8 @@
 
   web_context_->setup_environment_settings(new dom::DOMSettings(
       debugger_hooks_, kDOMMaxElementDepth, media_source_registry_.get(),
-      &media_source_settings_, data.can_play_type_handler, memory_info,
-      &mutation_observer_task_manager_, data.options.dom_settings_options));
+      data.can_play_type_handler, memory_info, &mutation_observer_task_manager_,
+      data.options.dom_settings_options));
   DCHECK(web_context_->environment_settings());
   // From algorithm to setup up a window environment settings object:
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-window-environment-settings-object
@@ -1103,12 +1100,6 @@
   window_->set_web_media_player_factory(media_module);
 }
 
-void WebModule::Impl::SetMediaSourceSetting(const std::string& name, int value,
-                                            bool* succeeded) {
-  DCHECK(succeeded);
-  *succeeded = media_source_settings_.Set(name, value);
-}
-
 void WebModule::Impl::SetApplicationState(base::ApplicationState state,
                                           SbTimeMonotonic timestamp) {
   window_->SetApplicationState(state, timestamp);
@@ -1316,7 +1307,8 @@
           base::polymorphic_downcast<const dom::UIEvent* const>(event.get())
               ->view());
       if (!topmost_event_target_) {
-        topmost_event_target_.reset(new layout::TopmostEventTarget());
+        topmost_event_target_.reset(
+            new layout::TopmostEventTarget(scroll_engine_));
       }
       topmost_event_target_->MaybeSendPointerEvents(event);
     }
@@ -1350,6 +1342,7 @@
 
 void WebModule::Run(
     const GURL& initial_url, base::ApplicationState initial_application_state,
+    ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
     const OnRenderTreeProducedCallback& render_tree_produced_callback,
     OnErrorCallback error_callback, const CloseCallback& window_close_callback,
     const base::Closure& window_minimize_callback,
@@ -1358,10 +1351,11 @@
     render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
     const Options& options) {
   ConstructionData construction_data(
-      initial_url, initial_application_state, render_tree_produced_callback,
-      error_callback, window_close_callback, window_minimize_callback,
-      can_play_type_handler, media_module, window_dimensions, resource_provider,
-      kDOMMaxElementDepth, layout_refresh_rate, ui_nav_root_,
+      initial_url, initial_application_state, scroll_engine,
+      render_tree_produced_callback, error_callback, window_close_callback,
+      window_minimize_callback, can_play_type_handler, media_module,
+      window_dimensions, resource_provider, kDOMMaxElementDepth,
+      layout_refresh_rate, ui_nav_root_,
 #if defined(ENABLE_DEBUGGER)
       &waiting_for_web_debugger_,
 #endif  // defined(ENABLE_DEBUGGER)
@@ -1605,12 +1599,6 @@
   impl_->SetMediaModule(media_module);
 }
 
-bool WebModule::SetMediaSourceSetting(const std::string& name, int value) {
-  bool succeeded = false;
-  SetMediaSourceSettingInternal(name, value, &succeeded);
-  return succeeded;
-}
-
 void WebModule::SetImageCacheCapacity(int64_t bytes) {
   POST_TO_ENSURE_IMPL_ON_THREAD(SetImageCacheCapacity, bytes);
   impl_->SetImageCacheCapacity(bytes);
@@ -1744,12 +1732,5 @@
   impl_->SetUnloadEventTimingInfo(start_time, end_time);
 }
 
-void WebModule::SetMediaSourceSettingInternal(const std::string& name,
-                                              int value, bool* succeeded) {
-  POST_AND_BLOCK_TO_ENSURE_IMPL_ON_THREAD(SetMediaSourceSettingInternal, name,
-                                          value, succeeded);
-  impl_->SetMediaSourceSetting(name, value, succeeded);
-}
-
 }  // namespace browser
 }  // namespace cobalt
diff --git a/cobalt/browser/web_module.h b/cobalt/browser/web_module.h
index a29c1bd..d3fa41c 100644
--- a/cobalt/browser/web_module.h
+++ b/cobalt/browser/web_module.h
@@ -51,6 +51,7 @@
 #include "cobalt/render_tree/node.h"
 #include "cobalt/render_tree/resource_provider.h"
 #include "cobalt/ui_navigation/nav_item.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
 #include "cobalt/web/agent.h"
 #include "cobalt/web/blob.h"
 #include "cobalt/web/context.h"
@@ -264,6 +265,7 @@
   ~WebModule();
   void Run(const GURL& initial_url,
            base::ApplicationState initial_application_state,
+           ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
            const OnRenderTreeProducedCallback& render_tree_produced_callback,
            OnErrorCallback error_callback,
            const CloseCallback& window_close_callback,
@@ -351,7 +353,6 @@
 
   void UpdateCamera3D(const scoped_refptr<input::Camera3D>& camera_3d);
   void SetMediaModule(media::MediaModule* media_module);
-  bool SetMediaSourceSetting(const std::string& name, int value);
   void SetImageCacheCapacity(int64_t bytes);
   void SetRemoteTypefaceCacheCapacity(int64_t bytes);
 
@@ -408,6 +409,7 @@
   struct ConstructionData {
     ConstructionData(const GURL& initial_url,
                      base::ApplicationState initial_application_state,
+                     ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
                      OnRenderTreeProducedCallback render_tree_produced_callback,
                      const OnErrorCallback& error_callback,
                      CloseCallback window_close_callback,
@@ -425,6 +427,7 @@
                      const Options& options)
         : initial_url(initial_url),
           initial_application_state(initial_application_state),
+          scroll_engine(scroll_engine),
           render_tree_produced_callback(render_tree_produced_callback),
           error_callback(error_callback),
           window_close_callback(window_close_callback),
@@ -445,6 +448,7 @@
 
     GURL initial_url;
     base::ApplicationState initial_application_state;
+    ui_navigation::scroll_engine::ScrollEngine* scroll_engine;
     OnRenderTreeProducedCallback render_tree_produced_callback;
     OnErrorCallback error_callback;
     CloseCallback window_close_callback;
@@ -475,9 +479,6 @@
 
   void GetIsReadyToFreeze(volatile bool* is_ready_to_freeze);
 
-  void SetMediaSourceSettingInternal(const std::string& name, int value,
-                                     bool* succeeded);
-
   // The message loop this object is running on.
   base::MessageLoop* message_loop() const {
     DCHECK(web_agent_);
diff --git a/cobalt/build/cobalt_configuration.py b/cobalt/build/cobalt_configuration.py
index 342f025..a8c09b1 100644
--- a/cobalt/build/cobalt_configuration.py
+++ b/cobalt/build/cobalt_configuration.py
@@ -74,6 +74,10 @@
     # the proxy has problems sending and terminating a single complete
     # response. It may end up sending multiple empty responses.
     filters = [
+        # CORS - 304 checks
+        # Disabled because of: Flaky on buildbot, proxy unreliability
+        'cors/WebPlatformTest.Run/cors_304_htm',
+
         # Late listeners: Preflight.
         # Disabled because of: Flaky. Buildbot only failure.
         'cors/WebPlatformTest.Run/cors_late_upload_events_htm',
diff --git a/cobalt/build/gn.py b/cobalt/build/gn.py
index 88fa813..973ec4c 100755
--- a/cobalt/build/gn.py
+++ b/cobalt/build/gn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright 2021 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +18,7 @@
 import os
 import shutil
 import subprocess
+import sys
 from pathlib import Path
 from typing import List
 
@@ -42,7 +43,10 @@
     print(f'{dst_args_gn_file} already exists.' +
           ' Running ninja will regenerate build files automatically.')
 
-  gn_command = ['gn', 'gen', out_directory] + gn_gen_args
+  gn_command = [
+      'gn', '--script-executable={}'.format(sys.executable), 'gen',
+      out_directory
+  ] + gn_gen_args
   print(' '.join(gn_command))
   subprocess.check_call(gn_command)
 
diff --git a/cobalt/build/path_conversion_test.py b/cobalt/build/path_conversion_test.py
index e0245b1..2438707 100644
--- a/cobalt/build/path_conversion_test.py
+++ b/cobalt/build/path_conversion_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright 2017 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc
index aeb1132..f275abb 100644
--- a/cobalt/cache/cache.cc
+++ b/cobalt/cache/cache.cc
@@ -24,11 +24,10 @@
 #include "base/optional.h"
 #include "base/values.h"
 #include "cobalt/configuration/configuration.h"
-#include "cobalt/extension/javascript_cache.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/disk_cache/cobalt/cobalt_backend_impl.h"
-#include "starboard/common/murmurhash2.h"
 #include "starboard/configuration_constants.h"
+#include "starboard/extension/javascript_cache.h"
 #include "starboard/system.h"
 
 namespace {
@@ -38,6 +37,8 @@
   switch (resource_type) {
     case disk_cache::ResourceType::kCompiledScript:
       return 4096u;
+    case disk_cache::ResourceType::kServiceWorkerScript:
+      return 1u;
     default:
       return base::nullopt;
   }
@@ -50,6 +51,8 @@
       return "cache_api";
     case disk_cache::ResourceType::kCompiledScript:
       return "compiled_js";
+    case disk_cache::ResourceType::kServiceWorkerScript:
+      return "service_worker_js";
     default:
       return base::nullopt;
   }
@@ -92,11 +95,6 @@
   return base::Singleton<Cache, base::LeakySingletonTraits<Cache>>::get();
 }
 
-// static
-uint32_t Cache::CreateKey(const std::string& s) {
-  return starboard::MurmurHash2_32(s.c_str(), s.size());
-}
-
 bool Cache::Delete(disk_cache::ResourceType resource_type, uint32_t key) {
   auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
   if (memory_capped_directory) {
@@ -113,6 +111,7 @@
 }
 
 void Cache::DeleteAll() {
+  Delete(disk_cache::ResourceType::kServiceWorkerScript);
   Delete(disk_cache::ResourceType::kCompiledScript);
   Delete(disk_cache::ResourceType::kCacheApi);
 }
@@ -126,13 +125,13 @@
   return std::vector<uint32_t>();
 }
 
-std::unique_ptr<base::Value> Cache::Metadata(
+base::Optional<base::Value> Cache::Metadata(
     disk_cache::ResourceType resource_type, uint32_t key) {
   auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
   if (memory_capped_directory) {
     return memory_capped_directory->Metadata(key);
   }
-  return nullptr;
+  return base::nullopt;
 }
 
 std::unique_ptr<std::vector<uint8_t>> Cache::Retrieve(
@@ -271,6 +270,7 @@
   switch (resource_type) {
     case disk_cache::ResourceType::kCacheApi:
     case disk_cache::ResourceType::kCompiledScript:
+    case disk_cache::ResourceType::kServiceWorkerScript:
       return disk_cache::kTypeMetadata[resource_type].max_size_bytes;
     default:
       return base::nullopt;
diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h
index 5b57be6..40bd225 100644
--- a/cobalt/cache/cache.h
+++ b/cobalt/cache/cache.h
@@ -45,15 +45,14 @@
 class Cache {
  public:
   static Cache* GetInstance();
-  static uint32_t CreateKey(const std::string& s);
 
   bool Delete(disk_cache::ResourceType resource_type, uint32_t key);
   void Delete(disk_cache::ResourceType resource_type);
   void DeleteAll();
   std::vector<uint32_t> KeysWithMetadata(
       disk_cache::ResourceType resource_type);
-  std::unique_ptr<base::Value> Metadata(disk_cache::ResourceType resource_type,
-                                        uint32_t key);
+  base::Optional<base::Value> Metadata(disk_cache::ResourceType resource_type,
+                                       uint32_t key);
   std::unique_ptr<std::vector<uint8_t>> Retrieve(
       disk_cache::ResourceType resource_type, uint32_t key,
       std::function<std::pair<std::unique_ptr<std::vector<uint8_t>>,
@@ -92,7 +91,7 @@
   std::map<disk_cache::ResourceType,
            std::map<uint32_t, std::vector<base::WaitableEvent*>>>
       pending_;
-  bool enabled_;
+  bool enabled_ = true;
 
   persistent_storage::PersistentSettings* persistent_settings_ = nullptr;
 
diff --git a/cobalt/cache/memory_capped_directory.cc b/cobalt/cache/memory_capped_directory.cc
index de70439..d6c40fd 100644
--- a/cobalt/cache/memory_capped_directory.cc
+++ b/cobalt/cache/memory_capped_directory.cc
@@ -97,7 +97,7 @@
     base::DeleteFile(metadata_path, false);
   }
   file_sizes_.erase(file_path);
-  file_keys_with_metadata_.erase(file_path);
+  file_keys_with_metadata_.erase(metadata_path);
   auto* heap = &file_info_heap_;
   for (auto it = heap->begin(); it != heap->end(); ++it) {
     if (it->file_path_ == file_path) {
@@ -126,7 +126,7 @@
 }
 
 std::vector<uint32_t> MemoryCappedDirectory::KeysWithMetadata() {
-  std::vector<uint32_t> keys(file_keys_with_metadata_.size());
+  std::vector<uint32_t> keys;
   for (auto it = file_keys_with_metadata_.begin();
        it != file_keys_with_metadata_.end(); ++it) {
     keys.push_back(it->second);
@@ -134,14 +134,15 @@
   return keys;
 }
 
-std::unique_ptr<base::Value> MemoryCappedDirectory::Metadata(uint32_t key) {
+base::Optional<base::Value> MemoryCappedDirectory::Metadata(uint32_t key) {
   auto metadata_path = GetFilePath(key).AddExtension(kMetadataExtension);
   if (!base::PathExists(metadata_path)) {
-    return nullptr;
+    return base::nullopt;
   }
   std::string serialized_metadata;
   base::ReadFileToString(metadata_path, &serialized_metadata);
-  return base::JSONReader::Read(serialized_metadata);
+  return base::Value::FromUniquePtrValue(
+      base::JSONReader::Read(serialized_metadata));
 }
 
 std::unique_ptr<std::vector<uint8_t>> MemoryCappedDirectory::Retrieve(
diff --git a/cobalt/cache/memory_capped_directory.h b/cobalt/cache/memory_capped_directory.h
index 39d227d..6bbe028 100644
--- a/cobalt/cache/memory_capped_directory.h
+++ b/cobalt/cache/memory_capped_directory.h
@@ -54,7 +54,7 @@
   bool Delete(uint32_t key);
   void DeleteAll();
   std::vector<uint32_t> KeysWithMetadata();
-  std::unique_ptr<base::Value> Metadata(uint32_t key);
+  base::Optional<base::Value> Metadata(uint32_t key);
   std::unique_ptr<std::vector<uint8_t>> Retrieve(uint32_t key);
   void Store(uint32_t key, const std::vector<uint8_t>& data,
              const base::Optional<base::Value>& metadata);
diff --git a/cobalt/configuration/configuration.h b/cobalt/configuration/configuration.h
index 630c814..6471941 100644
--- a/cobalt/configuration/configuration.h
+++ b/cobalt/configuration/configuration.h
@@ -16,7 +16,7 @@
 #define COBALT_CONFIGURATION_CONFIGURATION_H_
 
 #include "base/macros.h"
-#include "cobalt/extension/configuration.h"
+#include "starboard/extension/configuration.h"
 
 namespace base {
 template <typename T>
diff --git a/cobalt/content/licenses/platform/android/licenses_cobalt.txt b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
index 6754136..a286d10 100644
--- a/cobalt/content/licenses/platform/android/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
@@ -735,42 +735,472 @@
 
   Netscape Portable Runtime (NSPR)
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is the Netscape Portable Runtime (NSPR).
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 1998-2000
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is the Netscape Portable Runtime (NSPR).
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 1998-2000
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   symbolize
@@ -1468,42 +1898,472 @@
 
   mozilla_security_manager
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is mozilla.org code.
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 2001
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is mozilla.org code.
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 2001
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   mozilla(url/third_party/mozilla)
@@ -1543,6 +2403,443 @@
   The file url_parse.cc is based on nsURLParsers.cc from Mozilla. This file is
   licensed separately as follows:
 
+  --------------------------------------------------------------------------------
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
   The contents of this file are subject to the Mozilla Public License Version
   1.1 (the "License"); you may not use this file except in compliance with
   the License. You may obtain a copy of the License at
diff --git a/cobalt/content/licenses/platform/default/licenses_cobalt.txt b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
index fd93a89..be071f5 100644
--- a/cobalt/content/licenses/platform/default/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
@@ -176,6 +176,68 @@
    limitations under the License.
 
 
+  Fraunhofer FDK AAC Codec Library
+
+  Software License for The Fraunhofer FDK AAC Codec Library for Android
+  © Copyright  1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+    All rights reserved.
+  1.    INTRODUCTION
+  The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+  the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+  This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+  AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+  audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+  independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+  of the MPEG specifications.
+  Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+  may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+  individually for the purpose of encoding or decoding bit streams in products that are compliant with
+  the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+  these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+  software may already be covered under those patent licenses when it is used for those licensed purposes only.
+  Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+  are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+  applications information and documentation.
+  2.    COPYRIGHT LICENSE
+  Redistribution and use in source and binary forms, with or without modification, are permitted without
+  payment of copyright license fees provided that you satisfy the following conditions:
+  You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+  your modifications thereto in source code form.
+  You must retain the complete text of this software license in the documentation and/or other materials
+  provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+  You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+  modifications thereto to recipients of copies in binary form.
+  The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+  prior written permission.
+  You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+  software or your modifications thereto.
+  Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+  and the date of any change. For modified versions of the FDK AAC Codec, the term
+  "Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+  "Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+  3.    NO PATENT LICENSE
+  NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+  ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+  respect to this software.
+  You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+  by appropriate patent licenses.
+  4.    DISCLAIMER
+  This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+  "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+  of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+  CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+  including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+  or business interruption, however caused and on any theory of liability, whether in contract, strict
+  liability, or tort (including negligence), arising in any way out of the use of this software, even if
+  advised of the possibility of such damage.
+  5.    CONTACT INFORMATION
+  Fraunhofer Institute for Integrated Circuits IIS
+  Attention: Audio and Multimedia Departments - FDK AAC LL
+  Am Wolfsmantel 33
+  91058 Erlangen, Germany
+  www.iis.fraunhofer.de/amm
+  amm-info@iis.fraunhofer.de
+
 
   Chromium
 
@@ -735,42 +797,472 @@
 
   Netscape Portable Runtime (NSPR)
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is the Netscape Portable Runtime (NSPR).
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 1998-2000
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is the Netscape Portable Runtime (NSPR).
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 1998-2000
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   symbolize
@@ -1648,42 +2140,472 @@
 
   mozilla_security_manager
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is mozilla.org code.
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 2001
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is mozilla.org code.
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 2001
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   mozilla(url/third_party/mozilla)
@@ -1723,6 +2645,443 @@
   The file url_parse.cc is based on nsURLParsers.cc from Mozilla. This file is
   licensed separately as follows:
 
+  --------------------------------------------------------------------------------
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
   The contents of this file are subject to the Mozilla Public License Version
   1.1 (the "License"); you may not use this file except in compliance with
   the License. You may obtain a copy of the License at
@@ -5549,3 +6908,31 @@
   liability, whether in an action of contract, tort or otherwise, arising from,
   out of or in connection with the Software or the use or other dealings in the
   Software.
+
+
+  openh264
+
+
+  Copyright (c) 2013, Cisco Systems
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without modification,
+  are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice, this
+    list of conditions and the following disclaimer in the documentation and/or
+    other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
index 8063535..9a104c6 100644
--- a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
@@ -176,6 +176,68 @@
    limitations under the License.
 
 
+  Fraunhofer FDK AAC Codec Library
+
+  Software License for The Fraunhofer FDK AAC Codec Library for Android
+  © Copyright  1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+    All rights reserved.
+  1.    INTRODUCTION
+  The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+  the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+  This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+  AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+  audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+  independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+  of the MPEG specifications.
+  Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+  may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+  individually for the purpose of encoding or decoding bit streams in products that are compliant with
+  the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+  these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+  software may already be covered under those patent licenses when it is used for those licensed purposes only.
+  Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+  are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+  applications information and documentation.
+  2.    COPYRIGHT LICENSE
+  Redistribution and use in source and binary forms, with or without modification, are permitted without
+  payment of copyright license fees provided that you satisfy the following conditions:
+  You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+  your modifications thereto in source code form.
+  You must retain the complete text of this software license in the documentation and/or other materials
+  provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+  You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+  modifications thereto to recipients of copies in binary form.
+  The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+  prior written permission.
+  You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+  software or your modifications thereto.
+  Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+  and the date of any change. For modified versions of the FDK AAC Codec, the term
+  "Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+  "Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+  3.    NO PATENT LICENSE
+  NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+  ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+  respect to this software.
+  You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+  by appropriate patent licenses.
+  4.    DISCLAIMER
+  This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+  "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+  of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+  CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+  including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+  or business interruption, however caused and on any theory of liability, whether in contract, strict
+  liability, or tort (including negligence), arising in any way out of the use of this software, even if
+  advised of the possibility of such damage.
+  5.    CONTACT INFORMATION
+  Fraunhofer Institute for Integrated Circuits IIS
+  Attention: Audio and Multimedia Departments - FDK AAC LL
+  Am Wolfsmantel 33
+  91058 Erlangen, Germany
+  www.iis.fraunhofer.de/amm
+  amm-info@iis.fraunhofer.de
+
 
   Chromium
 
@@ -735,42 +797,472 @@
 
   Netscape Portable Runtime (NSPR)
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is the Netscape Portable Runtime (NSPR).
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 1998-2000
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is the Netscape Portable Runtime (NSPR).
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 1998-2000
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   symbolize
@@ -1648,42 +2140,472 @@
 
   mozilla_security_manager
 
+                             MOZILLA PUBLIC LICENSE
+                                Version 1.1
 
-  /* ***** BEGIN LICENSE BLOCK *****
-   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
-   *
-   * The contents of this file are subject to the Mozilla Public License Version
-   * 1.1 (the "License"); you may not use this file except in compliance with
-   * the License. You may obtain a copy of the License at
-   * http://www.mozilla.org/MPL/
-   *
-   * Software distributed under the License is distributed on an "AS IS" basis,
-   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-   * for the specific language governing rights and limitations under the
-   * License.
-   *
-   * The Original Code is mozilla.org code.
-   *
-   * The Initial Developer of the Original Code is
-   * Netscape Communications Corporation.
-   * Portions created by the Initial Developer are Copyright (C) 2001
-   * the Initial Developer. All Rights Reserved.
-   *
-   * Contributor(s):
-   *
-   * Alternatively, the contents of this file may be used under the terms of
-   * either the GNU General Public License Version 2 or later (the "GPL"), or
-   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-   * in which case the provisions of the GPL or the LGPL are applicable instead
-   * of those above. If you wish to allow use of your version of this file only
-   * under the terms of either the GPL or the LGPL, and not to allow others to
-   * use your version of this file under the terms of the MPL, indicate your
-   * decision by deleting the provisions above and replace them with the notice
-   * and other provisions required by the GPL or the LGPL. If you do not delete
-   * the provisions above, a recipient may use your version of this file under
-   * the terms of any one of the MPL, the GPL or the LGPL.
-   *
-   * ***** END LICENSE BLOCK ***** */
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
+   The contents of this file are subject to the Mozilla Public License Version
+   1.1 (the "License"); you may not use this file except in compliance with
+   the License. You may obtain a copy of the License at
+   http://www.mozilla.org/MPL/
+
+   Software distributed under the License is distributed on an "AS IS" basis,
+   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   for the specific language governing rights and limitations under the
+   License.
+
+   The Original Code is mozilla.org code.
+
+   The Initial Developer of the Original Code is
+   Netscape Communications Corporation.
+   Portions created by the Initial Developer are Copyright (C) 2001
+   the Initial Developer. All Rights Reserved.
+
+   Contributor(s):
+
+   Alternatively, the contents of this file may be used under the terms of
+   either the GNU General Public License Version 2 or later (the "GPL"), or
+   the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   in which case the provisions of the GPL or the LGPL are applicable instead
+   of those above. If you wish to allow use of your version of this file only
+   under the terms of either the GPL or the LGPL, and not to allow others to
+   use your version of this file under the terms of the MPL, indicate your
+   decision by deleting the provisions above and replace them with the notice
+   and other provisions required by the GPL or the LGPL. If you do not delete
+   the provisions above, a recipient may use your version of this file under
+   the terms of any one of the MPL, the GPL or the LGPL.
 
 
   mozilla(url/third_party/mozilla)
@@ -1723,6 +2645,443 @@
   The file url_parse.cc is based on nsURLParsers.cc from Mozilla. This file is
   licensed separately as follows:
 
+  --------------------------------------------------------------------------------
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+  1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+  2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+  3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+  4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+  5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+  6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+  7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+  8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+  9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+  10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+  11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+  12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+  13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+  EXHIBIT A -Mozilla Public License.
+
   The contents of this file are subject to the Mozilla Public License Version
   1.1 (the "License"); you may not use this file except in compliance with
   the License. You may obtain a copy of the License at
@@ -5370,3 +6729,32 @@
   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+  openh264
+
+
+  Copyright (c) 2013, Cisco Systems
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without modification,
+  are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice, this
+    list of conditions and the following disclaimer in the documentation and/or
+    other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cobalt/content/ssl/certs/03179a64.0 b/cobalt/content/ssl/certs/03179a64.0
deleted file mode 100644
index 315f665..0000000
--- a/cobalt/content/ssl/certs/03179a64.0
+++ /dev/null
@@ -1,32 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO
-TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh
-dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y
-MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg
-TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS
-b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS
-M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC
-UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d
-Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p
-rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l
-pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb
-j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC
-KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS
-/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X
-cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH
-1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP
-px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB
-/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7
-MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI
-eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u
-2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS
-v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC
-wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy
-CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e
-vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6
-Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa
-Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL
-eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8
-FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc
-7uzXLg==
------END CERTIFICATE-----
diff --git a/cobalt/csp/content_security_policy.cc b/cobalt/csp/content_security_policy.cc
index e5d502c..ae9e12e 100644
--- a/cobalt/csp/content_security_policy.cc
+++ b/cobalt/csp/content_security_policy.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/csp/content_security_policy.h"
 
+#include <memory>
+
 #include "base/strings/string_util.h"
 #include "base/values.h"
 #include "cobalt/csp/directive_list.h"
@@ -85,7 +85,9 @@
   }
 
   HashAlgorithm valid_hash_algorithms[] = {
-      kHashAlgorithmSha256, kHashAlgorithmSha384, kHashAlgorithmSha512,
+      kHashAlgorithmSha256,
+      kHashAlgorithmSha384,
+      kHashAlgorithmSha512,
   };
 
   for (size_t i = 0; i < arraysize(valid_hash_algorithms); ++i) {
@@ -108,6 +110,9 @@
 
 ResponseHeaders::ResponseHeaders(
     const scoped_refptr<net::HttpResponseHeaders>& response) {
+  if (response == nullptr) {
+    return;
+  }
   response->GetNormalizedHeader("Content-Security-Policy",
                                 &content_security_policy_);
   response->GetNormalizedHeader("Content-Security-Policy-Report-Only",
@@ -136,6 +141,9 @@
 const char ContentSecurityPolicy::kReflectedXSS[] = "reflected-xss";
 const char ContentSecurityPolicy::kReferrer[] = "referrer";
 
+// CSP Level 3 Directives
+const char ContentSecurityPolicy::kWorkerSrc[] = "worker-src";
+
 // Custom Cobalt directive to enforce navigation restrictions.
 const char ContentSecurityPolicy::kLocationSrc[] = "h5vcc-location-src";
 
@@ -159,19 +167,20 @@
 // clang-format off
 bool ContentSecurityPolicy::IsDirectiveName(const std::string& name) {
   std::string lower_name = base::ToLowerASCII(name);
-  return (lower_name == kConnectSrc ||
+  return (
+          // CSP Level 1 Directives
+          lower_name == kConnectSrc ||
           lower_name == kDefaultSrc ||
           lower_name == kFontSrc ||
           lower_name == kFrameSrc ||
           lower_name == kImgSrc ||
-          lower_name == kLocationSrc ||
           lower_name == kMediaSrc ||
           lower_name == kObjectSrc ||
           lower_name == kReportURI ||
           lower_name == kSandbox ||
-          lower_name == kSuborigin ||
           lower_name == kScriptSrc ||
           lower_name == kStyleSrc ||
+          // CSP Level 2 Directives
           lower_name == kBaseURI ||
           lower_name == kChildSrc ||
           lower_name == kFormAction ||
@@ -179,9 +188,15 @@
           lower_name == kPluginTypes ||
           lower_name == kReflectedXSS ||
           lower_name == kReferrer ||
+          // CSP Level 3 Directives
           lower_name == kManifestSrc ||
+          lower_name == kWorkerSrc ||
+          // Directives Defined in Other Documents.
           lower_name == kBlockAllMixedContent ||
-          lower_name == kUpgradeInsecureRequests);
+          lower_name == kUpgradeInsecureRequests ||
+          lower_name == kSuborigin ||
+          // Custom CSP directive for Cobalt
+          lower_name == kLocationSrc);
 }
 // clang-format on
 
@@ -434,18 +449,13 @@
                 << " directive is not supported inside a <meta> element.";
 }
 
-bool ContentSecurityPolicy::AllowJavaScriptURLs(const std::string& context_url,
-                                                int context_line,
-                                                ReportingStatus status) const {
-  FOR_ALL_POLICIES_3(AllowJavaScriptURLs, context_url, context_line, status);
-}
-
 bool ContentSecurityPolicy::AllowInlineEventHandlers(
     const std::string& context_url, int context_line,
     ReportingStatus status) const {
   FOR_ALL_POLICIES_3(AllowInlineEventHandlers, context_url, context_line,
                      status);
 }
+
 bool ContentSecurityPolicy::AllowInlineScript(const std::string& context_url,
                                               int context_line,
                                               const std::string& script_content,
@@ -453,6 +463,15 @@
   FOR_ALL_POLICIES_4(AllowInlineScript, context_url, context_line, status,
                      script_content);
 }
+
+bool ContentSecurityPolicy::AllowInlineWorker(const std::string& context_url,
+                                              int context_line,
+                                              const std::string& script_content,
+                                              ReportingStatus status) const {
+  FOR_ALL_POLICIES_4(AllowInlineWorker, context_url, context_line, status,
+                     script_content);
+}
+
 bool ContentSecurityPolicy::AllowInlineStyle(const std::string& context_url,
                                              int context_line,
                                              const std::string& style_content,
@@ -466,29 +485,36 @@
 }
 
 bool ContentSecurityPolicy::AllowScriptFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowScriptFromSource, url, redirect_status,
                      reporting_status);
 }
 
+bool ContentSecurityPolicy::AllowWorkerFromSource(
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  FOR_ALL_POLICIES_3(AllowWorkerFromSource, url, redirect_status,
+                     reporting_status);
+}
+
 bool ContentSecurityPolicy::AllowObjectFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowObjectFromSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowImageFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowImageFromSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowNavigateToSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   // Note that this is a Cobalt-specific policy to prevent navigation
   // to any unexpected URLs.
   FOR_ALL_POLICIES_3(AllowNavigateToSource, url, redirect_status,
@@ -496,48 +522,48 @@
 }
 
 bool ContentSecurityPolicy::AllowStyleFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowStyleFromSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowFontFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowFontFromSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowMediaFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowMediaFromSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowConnectToSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowConnectToSource, url, redirect_status,
                      reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowFormAction(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowFormAction, url, redirect_status, reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowBaseURI(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowBaseURI, url, redirect_status, reporting_status);
 }
 
 bool ContentSecurityPolicy::AllowManifestFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   FOR_ALL_POLICIES_3(AllowManifestFromSource, url, redirect_status,
                      reporting_status);
 }
@@ -546,6 +572,11 @@
   FOR_ALL_POLICIES_1(AllowScriptNonce, nonce);
 }
 
+bool ContentSecurityPolicy::AllowWorkerWithNonce(
+    const std::string& nonce) const {
+  FOR_ALL_POLICIES_1(AllowWorkerNonce, nonce);
+}
+
 bool ContentSecurityPolicy::AllowStyleWithNonce(
     const std::string& nonce) const {
   FOR_ALL_POLICIES_1(AllowStyleNonce, nonce);
@@ -557,6 +588,12 @@
       source, script_hash_algorithms_used_, policies_);
 }
 
+bool ContentSecurityPolicy::AllowWorkerWithHash(
+    const std::string& source) const {
+  return CheckDigest<&DirectiveList::AllowWorkerHash>(
+      source, script_hash_algorithms_used_, policies_);
+}
+
 bool ContentSecurityPolicy::AllowStyleWithHash(
     const std::string& source) const {
   return CheckDigest<&DirectiveList::AllowStyleHash>(
diff --git a/cobalt/csp/content_security_policy.h b/cobalt/csp/content_security_policy.h
index d8622b7..a5cf380 100644
--- a/cobalt/csp/content_security_policy.h
+++ b/cobalt/csp/content_security_policy.h
@@ -21,6 +21,7 @@
 
 #include "base/callback.h"
 #include "base/containers/hash_tables.h"
+#include "cobalt/csp/directive_list.h"
 #include "cobalt/csp/parsers.h"
 #include "net/http/http_response_headers.h"
 #include "url/gurl.h"
@@ -28,7 +29,6 @@
 namespace cobalt {
 namespace csp {
 
-class DirectiveList;
 class Source;
 
 // Wrap up information about a CSP violation, for passing to the Delegate.
@@ -79,6 +79,7 @@
   typedef std::vector<std::unique_ptr<DirectiveList>> PolicyList;
 
   // CSP Level 1 Directives
+  //   https://www.w3.org/TR/2012/CR-CSP-20121115/
   static const char kConnectSrc[];
   static const char kDefaultSrc[];
   static const char kFontSrc[];
@@ -92,6 +93,7 @@
   static const char kStyleSrc[];
 
   // CSP Level 2 Directives
+  //   https://www.w3.org/TR/2016/REC-CSP2-20161215/
   static const char kBaseURI[];
   static const char kChildSrc[];
   static const char kFormAction[];
@@ -100,35 +102,30 @@
   static const char kReflectedXSS[];
   static const char kReferrer[];
 
-  // Custom CSP directive for Cobalt
-  static const char kLocationSrc[];
+  // CSP Level 3 Directives
+  //   https://www.w3.org/TR/2022/WD-CSP3-20221014/#directive-manifest-src
 
-  // Manifest Directives (to be merged into CSP Level 2)
-  // https://w3c.github.io/manifest/#content-security-policy
   static const char kManifestSrc[];
+  // https://www.w3.org/TR/2022/WD-CSP3-20221014/#directive-worker-src
+  static const char kWorkerSrc[];
 
-  // Mixed Content Directive
-  // https://w3c.github.io/webappsec/specs/mixedcontent/#strict-mode
+  // Directives Defined in Other Documents.
+  //   https://www.w3.org/TR/2022/WD-CSP3-20221014/#directives-elsewhere
+
+  // Mixed Content Directive. Note: Deprecated in the current spec.
+  //   https://www.w3.org/TR/2021/CRD-mixed-content-20211004/#strict-checking
   static const char kBlockAllMixedContent[];
 
-  // https://w3c.github.io/webappsec/specs/upgrade/
+  // The upgrade-insecure-requests Directive.
+  //   https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
   static const char kUpgradeInsecureRequests[];
 
   // Suborigin Directive
-  // https://metromoxie.github.io/webappsec/specs/suborigins/index.html
+  //   https://metromoxie.github.io/webappsec/specs/suborigins/index.html#the-suborigin-directive
   static const char kSuborigin[];
 
-  enum ReportingStatus {
-    kSendReport,
-    kSuppressReport,
-  };
-
-  // When a resource is loaded after a redirect, source paths are
-  // ignored in the matching algorithm.
-  enum RedirectStatus {
-    kDidRedirect,
-    kDidNotRedirect,
-  };
+  // Custom CSP directive h5vcc-location-src for Cobalt
+  static const char kLocationSrc[];
 
   static bool IsDirectiveName(const std::string& name);
 
@@ -174,14 +171,15 @@
   void ReportDirectiveNotSupportedInsideMeta(const std::string& name);
 
   // https://www.w3.org/TR/2015/CR-CSP2-20150721/#directives
-  bool AllowJavaScriptURLs(const std::string& context_url, int context_line,
-                           ReportingStatus status = kSendReport) const;
   bool AllowInlineEventHandlers(const std::string& context_url,
                                 int context_line,
                                 ReportingStatus status = kSendReport) const;
   bool AllowInlineScript(const std::string& context_url, int context_line,
                          const std::string& script_content,
                          ReportingStatus status = kSendReport) const;
+  bool AllowInlineWorker(const std::string& context_url, int context_line,
+                         const std::string& script_content,
+                         ReportingStatus status = kSendReport) const;
   bool AllowInlineStyle(const std::string& context_url, int context_line,
                         const std::string& style_content,
                         ReportingStatus status = kSendReport) const;
@@ -189,6 +187,9 @@
   bool AllowScriptFromSource(const GURL& url,
                              RedirectStatus redirect = kDidNotRedirect,
                              ReportingStatus report = kSendReport) const;
+  bool AllowWorkerFromSource(const GURL& url,
+                             RedirectStatus redirect = kDidNotRedirect,
+                             ReportingStatus report = kSendReport) const;
   bool AllowObjectFromSource(const GURL& url,
                              RedirectStatus redirect = kDidNotRedirect,
                              ReportingStatus report = kSendReport) const;
@@ -227,8 +228,10 @@
   // If these return true, callers can then process the content or
   // issue a load and be safe disabling any further CSP checks.
   bool AllowScriptWithNonce(const std::string& nonce) const;
+  bool AllowWorkerWithNonce(const std::string& nonce) const;
   bool AllowStyleWithNonce(const std::string& nonce) const;
   bool AllowScriptWithHash(const std::string& source) const;
+  bool AllowWorkerWithHash(const std::string& source) const;
   bool AllowStyleWithHash(const std::string& source) const;
 
   void set_uses_script_hash_algorithms(uint8 algorithm) {
@@ -243,6 +246,16 @@
   void NotifyUrlChanged(const GURL& url);
   bool DidSetReferrerPolicy() const;
 
+  const PolicyList& policies() const { return policies_; }
+  void append_policy(const DirectiveList& directive_list) {
+    policies_.emplace_back(new DirectiveList(this, directive_list));
+  }
+
+  const ReferrerPolicy& referrer_policy() const { return referrer_policy_; }
+  void set_referrer_policy(const ReferrerPolicy& referrer_policy) {
+    referrer_policy_ = referrer_policy;
+  }
+
   const GURL& url() const { return url_; }
   void set_enforce_strict_mixed_content_checking() {
     enforce_strict_mixed_content_checking_ = true;
@@ -258,6 +271,8 @@
   void AddPolicyFromHeaderValue(const std::string& header, HeaderType type,
                                 HeaderSource source);
 
+  // List of CSP Policies.
+  //   https://www.w3.org/TR/2022/WD-CSP3-20221014/#csp-list
   PolicyList policies_;
   std::unique_ptr<Source> self_source_;
   std::string self_scheme_;
diff --git a/cobalt/csp/content_security_policy_test.cc b/cobalt/csp/content_security_policy_test.cc
index b70f14c..8ef00fd 100644
--- a/cobalt/csp/content_security_policy_test.cc
+++ b/cobalt/csp/content_security_policy_test.cc
@@ -29,18 +29,54 @@
   std::unique_ptr<ContentSecurityPolicy> csp_;
 };
 
+TEST_F(CspTest, UrlMatchesSelf) {
+  // Test whether the URLs match the source expression.
+  //   https://www.w3.org/TR/2015/CR-CSP2-20150721/#match-source-expression
+  // Tested in more detail in SourceTest.
+  csp_.reset(new ContentSecurityPolicy(GURL("https://www.example.com/foo/bar"),
+                                       callback_));
+  EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com")));
+  EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("HTTPS://www.example.com")));
+  EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com/foo/bar")));
+  EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com/bar/foo")));
+  EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("http://www.example.com/foo/bar")));
+  EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("https://www.example.com:8000")));
+  EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("https://example.com")));
+}
+
 TEST_F(CspTest, SecureSchemeMatchesSelf) {
   csp_.reset(
       new ContentSecurityPolicy(GURL("https://www.example.com"), callback_));
   EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
   EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+
+  csp_.reset(
+      new ContentSecurityPolicy(GURL("HTTPS://www.example.com"), callback_));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+  EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+}
+
+TEST_F(CspTest, FileSchemeDoesNotMatchSelf) {
+  csp_.reset(
+      new ContentSecurityPolicy(GURL("https://www.example.com"), callback_));
+  EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("file://example.com")));
 }
 
 TEST_F(CspTest, InsecureSchemeMatchesSelf) {
   csp_.reset(
       new ContentSecurityPolicy(GURL("http://www.example.com"), callback_));
   EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
   EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTP://example.com")));
+
+  csp_.reset(
+      new ContentSecurityPolicy(GURL("HTTP://www.example.com"), callback_));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+  EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTP://example.com")));
 }
 
 }  // namespace
diff --git a/cobalt/csp/directive.cc b/cobalt/csp/directive.cc
index f386b5d..dd14639 100644
--- a/cobalt/csp/directive.cc
+++ b/cobalt/csp/directive.cc
@@ -23,5 +23,8 @@
                      ContentSecurityPolicy* policy)
     : text_(name + " " + value), policy_(policy) {}
 
+Directive::Directive(ContentSecurityPolicy* policy, const Directive& other)
+    : text_(other.text_), policy_(policy) {}
+
 }  // namespace csp
 }  // namespace cobalt
diff --git a/cobalt/csp/directive.h b/cobalt/csp/directive.h
index 36fa60e..164948b 100644
--- a/cobalt/csp/directive.h
+++ b/cobalt/csp/directive.h
@@ -24,10 +24,23 @@
 
 class ContentSecurityPolicy;
 
+enum ReportingStatus {
+  kSendReport,
+  kSuppressReport,
+};
+
+// When a resource is loaded after a redirect, source paths are
+// ignored in the matching algorithm.
+enum RedirectStatus {
+  kDidRedirect,
+  kDidNotRedirect,
+};
+
 class Directive {
  public:
   Directive(const std::string& name, const std::string& value,
             ContentSecurityPolicy* policy);
+  Directive(ContentSecurityPolicy* policy, const Directive& other);
 
   const std::string& text() const { return text_; }
 
diff --git a/cobalt/csp/directive_list.cc b/cobalt/csp/directive_list.cc
index d62749a..78587cd 100644
--- a/cobalt/csp/directive_list.cc
+++ b/cobalt/csp/directive_list.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/csp/directive_list.h"
 
+#include <memory>
+
 #include "base/base64.h"
 #include "base/strings/stringprintf.h"
 #include "cobalt/csp/crypto.h"
@@ -88,6 +88,42 @@
   }
 }
 
+DirectiveList::DirectiveList(ContentSecurityPolicy* policy,
+                             const DirectiveList& other)
+    : policy_(policy),
+      header_(other.header_),
+      header_type_(other.header_type_),
+      header_source_(other.header_source_),
+      report_only_(other.report_only_),
+      has_sandbox_policy_(other.has_sandbox_policy_),
+      has_suborigin_policy_(other.has_suborigin_policy_),
+      reflected_xss_disposition_(other.reflected_xss_disposition_),
+      did_set_referrer_policy_(other.did_set_referrer_policy_),
+      referrer_policy_(other.referrer_policy_),
+      strict_mixed_content_checking_enforced_(
+          other.strict_mixed_content_checking_enforced_),
+      upgrade_insecure_requests_(other.upgrade_insecure_requests_),
+      plugin_types_(new MediaListDirective(policy, *(other.plugin_types_))),
+      base_uri_(new SourceListDirective(policy, *(other.base_uri_))),
+      child_src_(new SourceListDirective(policy, *(other.child_src_))),
+      connect_src_(new SourceListDirective(policy, *(other.connect_src_))),
+      default_src_(new SourceListDirective(policy, *(other.default_src_))),
+      font_src_(new SourceListDirective(policy, *(other.font_src_))),
+      form_action_(new SourceListDirective(policy, *(other.form_action_))),
+      frame_ancestors_(
+          new SourceListDirective(policy, *(other.frame_ancestors_))),
+      frame_src_(new SourceListDirective(policy, *(other.frame_src_))),
+      img_src_(new SourceListDirective(policy, *(other.img_src_))),
+      location_src_(new SourceListDirective(policy, *(other.location_src_))),
+      media_src_(new SourceListDirective(policy, *(other.media_src_))),
+      manifest_src_(new SourceListDirective(policy, *(other.manifest_src_))),
+      object_src_(new SourceListDirective(policy, *(other.object_src_))),
+      script_src_(new SourceListDirective(policy, *(other.script_src_))),
+      style_src_(new SourceListDirective(policy, *(other.style_src_))),
+      worker_src_(new SourceListDirective(policy, *(other.worker_src_))),
+      report_endpoints_(other.report_endpoints_),
+      eval_disabled_error_message_(other.eval_disabled_error_message_) {}
+
 DirectiveList::~DirectiveList() {}
 
 void DirectiveList::ReportViolation(const std::string& directive_text,
@@ -130,9 +166,8 @@
   return !directive || directive->AllowHash(hashValue);
 }
 
-bool DirectiveList::CheckSource(
-    SourceListDirective* directive, const GURL& url,
-    ContentSecurityPolicy::RedirectStatus redirect_status) const {
+bool DirectiveList::CheckSource(SourceListDirective* directive, const GURL& url,
+                                RedirectStatus redirect_status) const {
   return !directive || directive->Allows(url, redirect_status);
 }
 
@@ -210,8 +245,8 @@
 
 bool DirectiveList::CheckInlineAndReportViolation(
     SourceListDirective* directive, const std::string& console_message,
-    const std::string& context_url, int context_line, bool is_script,
-    const std::string& hash_value) const {
+    const std::string& context_url, int context_line,
+    const char* directive_name, const std::string& hash_value) const {
   if (CheckInline(directive)) {
     return true;
   }
@@ -240,20 +275,18 @@
         " Either the 'unsafe-inline' keyword, a hash (" + hash_value +
         "), or a nonce ('nonce-...') is required to enable inline execution.";
     if (directive == default_src_.get())
-      suffix = suffix + " Note also that '" +
-               std::string(is_script ? "script" : "style") +
-               "-src' was not explicitly set, so 'default-src' is used as a "
+      suffix = suffix + " Note also that '" + std::string(directive_name) +
+               "' was not explicitly set, so 'default-src' is used as a "
                "fallback.";
   }
 
   ReportViolationWithLocation(
-      directive->text(), is_script ? ContentSecurityPolicy::kScriptSrc
-                                   : ContentSecurityPolicy::kStyleSrc,
+      directive->text(), directive_name,
       console_message + "\"" + directive->text() + "\"." + suffix + "\n",
       GURL(), context_url, context_line);
 
   if (!report_only_) {
-    if (is_script) {
+    if (directive_name == ContentSecurityPolicy::kScriptSrc) {
       // policy_->ReportBlockedScriptExecutionToInspector(directive->text());
     }
     return false;
@@ -264,7 +297,7 @@
 bool DirectiveList::CheckSourceAndReportViolation(
     SourceListDirective* directive, const GURL& url,
     const std::string& effective_directive,
-    ContentSecurityPolicy::RedirectStatus redirect_status) const {
+    RedirectStatus redirect_status) const {
   if (CheckSource(directive, url, redirect_status)) {
     return true;
   }
@@ -296,6 +329,8 @@
     prefix = "Refused to load the script '";
   } else if (ContentSecurityPolicy::kStyleSrc == effective_directive) {
     prefix = "Refused to load the stylesheet '";
+  } else if (ContentSecurityPolicy::kWorkerSrc == effective_directive) {
+    prefix = "Refused to load the worker '";
   }
 
   std::string suffix = std::string();
@@ -313,63 +348,69 @@
   return deny_if_enforcing_policy();
 }
 
-bool DirectiveList::AllowJavaScriptURLs(
-    const std::string& context_url, int context_line,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  if (reporting_status == ContentSecurityPolicy::kSendReport) {
-    return CheckInlineAndReportViolation(
-        OperativeDirective(script_src_.get()),
-        "Refused to execute JavaScript URL because it violates the following "
-        "Content Security Policy directive: ",
-        context_url, context_line, true, "sha256-...");
-  }
-  return CheckInline(OperativeDirective(script_src_.get()));
-}
-
 bool DirectiveList::AllowInlineEventHandlers(
     const std::string& context_url, int context_line,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  if (reporting_status == ContentSecurityPolicy::kSendReport) {
+    ReportingStatus reporting_status) const {
+  if (reporting_status == kSendReport) {
     return CheckInlineAndReportViolation(
         OperativeDirective(script_src_.get()),
         "Refused to execute inline event handler because it violates the "
         "following Content Security Policy directive: ",
-        context_url, context_line, true, "sha256-...");
+        context_url, context_line, ContentSecurityPolicy::kScriptSrc,
+        "sha256-...");
   }
   return CheckInline(OperativeDirective(script_src_.get()));
 }
 
-bool DirectiveList::AllowInlineScript(
-    const std::string& context_url, int context_line,
-    ContentSecurityPolicy::ReportingStatus reporting_status,
-    const std::string& content) const {
-  if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowInlineScript(const std::string& context_url,
+                                      int context_line,
+                                      ReportingStatus reporting_status,
+                                      const std::string& content) const {
+  if (reporting_status == kSendReport) {
     return CheckInlineAndReportViolation(
         OperativeDirective(script_src_.get()),
         "Refused to execute inline script because it violates the following "
         "Content Security Policy directive: ",
-        context_url, context_line, true, GetSha256String(content));
+        context_url, context_line, ContentSecurityPolicy::kScriptSrc,
+        GetSha256String(content));
   }
   return CheckInline(OperativeDirective(script_src_.get()));
 }
 
-bool DirectiveList::AllowInlineStyle(
-    const std::string& context_url, int context_line,
-    ContentSecurityPolicy::ReportingStatus reporting_status,
-    const std::string& content) const {
-  if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowInlineWorker(const std::string& context_url,
+                                      int context_line,
+                                      ReportingStatus reporting_status,
+                                      const std::string& content) const {
+  if (reporting_status == kSendReport) {
+    return CheckInlineAndReportViolation(
+        OperativeDirective(worker_src_.get(),
+                           OperativeDirective(script_src_.get())),
+        "Refused to execute inline script because it violates the following "
+        "Content Security Policy directive: ",
+        context_url, context_line, ContentSecurityPolicy::kWorkerSrc,
+        GetSha256String(content));
+  }
+  return CheckInline(OperativeDirective(worker_src_.get(),
+                                        OperativeDirective(script_src_.get())));
+}
+
+bool DirectiveList::AllowInlineStyle(const std::string& context_url,
+                                     int context_line,
+                                     ReportingStatus reporting_status,
+                                     const std::string& content) const {
+  if (reporting_status == kSendReport) {
     return CheckInlineAndReportViolation(
         OperativeDirective(style_src_.get()),
         "Refused to apply inline style because it violates the following "
         "Content Security Policy directive: ",
-        context_url, context_line, false, GetSha256String(content));
+        context_url, context_line, ContentSecurityPolicy::kStyleSrc,
+        GetSha256String(content));
   }
   return CheckInline(OperativeDirective(style_src_.get()));
 }
 
-bool DirectiveList::AllowEval(
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowEval(ReportingStatus reporting_status) const {
+  if (reporting_status == kSendReport) {
     return CheckEvalAndReportViolation(
         OperativeDirective(script_src_.get()),
         "Refused to evaluate a string as JavaScript because 'unsafe-eval' is "
@@ -380,9 +421,9 @@
 }
 
 bool DirectiveList::AllowScriptFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(script_src_.get()), url,
                    ContentSecurityPolicy::kScriptSrc, redirect_status)
@@ -390,10 +431,24 @@
                            redirect_status);
 }
 
+bool DirectiveList::AllowWorkerFromSource(
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
+             ? CheckSourceAndReportViolation(
+                   OperativeDirective(worker_src_.get(),
+                                      OperativeDirective(script_src_.get())),
+                   url, ContentSecurityPolicy::kWorkerSrc, redirect_status)
+             : CheckSource(
+                   OperativeDirective(worker_src_.get(),
+                                      OperativeDirective(script_src_.get())),
+                   url, redirect_status);
+}
+
 bool DirectiveList::AllowObjectFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(object_src_.get()), url,
                    ContentSecurityPolicy::kObjectSrc, redirect_status)
@@ -402,9 +457,9 @@
 }
 
 bool DirectiveList::AllowImageFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(img_src_.get()), url,
                    ContentSecurityPolicy::kImgSrc, redirect_status)
@@ -413,9 +468,9 @@
 }
 
 bool DirectiveList::AllowStyleFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(style_src_.get()), url,
                    ContentSecurityPolicy::kStyleSrc, redirect_status)
@@ -424,9 +479,9 @@
 }
 
 bool DirectiveList::AllowFontFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(font_src_.get()), url,
                    ContentSecurityPolicy::kFontSrc, redirect_status)
@@ -435,9 +490,9 @@
 }
 
 bool DirectiveList::AllowMediaFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(media_src_.get()), url,
                    ContentSecurityPolicy::kMediaSrc, redirect_status)
@@ -446,9 +501,9 @@
 }
 
 bool DirectiveList::AllowManifestFromSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(manifest_src_.get()), url,
                    ContentSecurityPolicy::kManifestSrc, redirect_status)
@@ -457,9 +512,9 @@
 }
 
 bool DirectiveList::AllowConnectToSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    OperativeDirective(connect_src_.get()), url,
                    ContentSecurityPolicy::kConnectSrc, redirect_status)
@@ -468,31 +523,31 @@
 }
 
 bool DirectiveList::AllowNavigateToSource(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
+    const GURL& url, RedirectStatus redirect_status,
+    ReportingStatus reporting_status) const {
   // No fallback to default for h5vcc-location-src policy, so we don't use
   // OperativeDirective() in this case.
-  return reporting_status == ContentSecurityPolicy::kSendReport
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(
                    location_src_.get(), url,
                    ContentSecurityPolicy::kLocationSrc, redirect_status)
              : CheckSource(location_src_.get(), url, redirect_status);
 }
 
-bool DirectiveList::AllowFormAction(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+bool DirectiveList::AllowFormAction(const GURL& url,
+                                    RedirectStatus redirect_status,
+                                    ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(form_action_.get(), url,
                                              ContentSecurityPolicy::kFormAction,
                                              redirect_status)
              : CheckSource(form_action_.get(), url, redirect_status);
 }
 
-bool DirectiveList::AllowBaseURI(
-    const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-    ContentSecurityPolicy::ReportingStatus reporting_status) const {
-  return reporting_status == ContentSecurityPolicy::kSendReport
+bool DirectiveList::AllowBaseURI(const GURL& url,
+                                 RedirectStatus redirect_status,
+                                 ReportingStatus reporting_status) const {
+  return reporting_status == kSendReport
              ? CheckSourceAndReportViolation(base_uri_.get(), url,
                                              ContentSecurityPolicy::kBaseURI,
                                              redirect_status)
@@ -503,6 +558,12 @@
   return CheckNonce(OperativeDirective(script_src_.get()), nonce);
 }
 
+bool DirectiveList::AllowWorkerNonce(const std::string& nonce) const {
+  return CheckNonce(OperativeDirective(worker_src_.get(),
+                                       OperativeDirective(script_src_.get())),
+                    nonce);
+}
+
 bool DirectiveList::AllowStyleNonce(const std::string& nonce) const {
   return CheckNonce(OperativeDirective(style_src_.get()), nonce);
 }
@@ -511,6 +572,12 @@
   return CheckHash(OperativeDirective(script_src_.get()), hash_value);
 }
 
+bool DirectiveList::AllowWorkerHash(const HashValue& hash_value) const {
+  return CheckHash(OperativeDirective(worker_src_.get(),
+                                      OperativeDirective(script_src_.get())),
+                   hash_value);
+}
+
 bool DirectiveList::AllowStyleHash(const HashValue& hash_value) const {
   return CheckHash(OperativeDirective(style_src_.get()), hash_value);
 }
@@ -612,7 +679,7 @@
   if (header_source_ == kHeaderSourceMeta) {
     // The report-uri, frame-ancestors, and sandbox directives are not supported
     // inside a meta element.
-    // https://w3c.github.io/webappsec-csp/#meta-element
+    // https://www.w3.org/TR/2022/WD-CSP3-20221014/#meta-element
     policy_->ReportDirectiveNotSupportedInsideMeta(name);
     return;
   }
@@ -844,6 +911,10 @@
     SetCSPDirective(name, value, &script_src_);
     policy_->set_uses_script_hash_algorithms(
         script_src_->hash_algorithms_used());
+  } else if (lower_name == ContentSecurityPolicy::kWorkerSrc) {
+    SetCSPDirective(name, value, &worker_src_);
+    policy_->set_uses_script_hash_algorithms(
+        worker_src_->hash_algorithms_used());
   } else if (lower_name == ContentSecurityPolicy::kObjectSrc) {
     SetCSPDirective(name, value, &object_src_);
   } else if (lower_name == ContentSecurityPolicy::kFrameAncestors) {
diff --git a/cobalt/csp/directive_list.h b/cobalt/csp/directive_list.h
index 46597b3..38765c2 100644
--- a/cobalt/csp/directive_list.h
+++ b/cobalt/csp/directive_list.h
@@ -20,12 +20,14 @@
 #include <vector>
 
 #include "base/basictypes.h"
-#include "cobalt/csp/content_security_policy.h"
+#include "cobalt/csp/directive.h"
+#include "cobalt/csp/parsers.h"
 #include "url/gurl.h"
 
 namespace cobalt {
 namespace csp {
 
+class ContentSecurityPolicy;
 class MediaListDirective;
 class SourceListDirective;
 
@@ -33,6 +35,7 @@
  public:
   DirectiveList(ContentSecurityPolicy* policy, const base::StringPiece& text,
                 HeaderType header_type, HeaderSource header_source);
+  DirectiveList(ContentSecurityPolicy* policy, const DirectiveList& other);
   ~DirectiveList();
   void Parse(const base::StringPiece& text);
 
@@ -40,62 +43,53 @@
   HeaderType header_type() const { return header_type_; }
   HeaderSource header_source() const { return header_source_; }
 
-  bool AllowJavaScriptURLs(
-      const std::string& context_url, int context_line,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowInlineEventHandlers(
-      const std::string& context_url, int context_line,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowInlineScript(
-      const std::string& context_url, int context_line,
-      ContentSecurityPolicy::ReportingStatus reporting_status,
-      const std::string& script_content) const;
+  bool AllowInlineEventHandlers(const std::string& context_url,
+                                int context_line,
+                                ReportingStatus reporting_status) const;
+  bool AllowInlineScript(const std::string& context_url, int context_line,
+                         ReportingStatus reporting_status,
+                         const std::string& script_content) const;
+  bool AllowInlineWorker(const std::string& context_url, int context_line,
+                         ReportingStatus reporting_status,
+                         const std::string& worker_content) const;
   bool AllowInlineStyle(const std::string& context_url, int context_line,
-                        ContentSecurityPolicy::ReportingStatus reporting_status,
+                        ReportingStatus reporting_status,
                         const std::string& style_content) const;
-  bool AllowEval(ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowPluginType(
-      const std::string& type, const std::string& type_attribute,
-      const GURL& url,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
+  bool AllowEval(ReportingStatus reporting_status) const;
+  bool AllowPluginType(const std::string& type,
+                       const std::string& type_attribute, const GURL& url,
+                       ReportingStatus reporting_status) const;
 
-  bool AllowScriptFromSource(
-      const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowObjectFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowImageFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowStyleFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowFontFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowMediaFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowManifestFromSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowConnectToSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowNavigateToSource(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowFormAction(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
-  bool AllowBaseURI(
-      const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
-      ContentSecurityPolicy::ReportingStatus reporting_status) const;
+  bool AllowScriptFromSource(const GURL& url, RedirectStatus redirect_status,
+                             ReportingStatus reporting_status) const;
+  bool AllowWorkerFromSource(const GURL& url, RedirectStatus redirect_status,
+                             ReportingStatus reporting_status) const;
+  bool AllowObjectFromSource(const GURL&, RedirectStatus redirect_status,
+                             ReportingStatus reporting_status) const;
+  bool AllowImageFromSource(const GURL&, RedirectStatus redirect_status,
+                            ReportingStatus reporting_status) const;
+  bool AllowStyleFromSource(const GURL&, RedirectStatus redirect_status,
+                            ReportingStatus reporting_status) const;
+  bool AllowFontFromSource(const GURL&, RedirectStatus redirect_status,
+                           ReportingStatus reporting_status) const;
+  bool AllowMediaFromSource(const GURL&, RedirectStatus redirect_status,
+                            ReportingStatus reporting_status) const;
+  bool AllowManifestFromSource(const GURL&, RedirectStatus redirect_status,
+                               ReportingStatus reporting_status) const;
+  bool AllowConnectToSource(const GURL&, RedirectStatus redirect_status,
+                            ReportingStatus reporting_status) const;
+  bool AllowNavigateToSource(const GURL&, RedirectStatus redirect_status,
+                             ReportingStatus reporting_status) const;
+  bool AllowFormAction(const GURL&, RedirectStatus redirect_status,
+                       ReportingStatus reporting_status) const;
+  bool AllowBaseURI(const GURL&, RedirectStatus redirect_status,
+                    ReportingStatus reporting_status) const;
   bool AllowScriptNonce(const std::string& script) const;
+  bool AllowWorkerNonce(const std::string& worker) const;
   bool AllowStyleNonce(const std::string& style) const;
   bool AllowScriptHash(const HashValue& script) const;
-  bool AllowStyleHash(const HashValue& script) const;
+  bool AllowWorkerHash(const HashValue& worker) const;
+  bool AllowStyleHash(const HashValue& style) const;
 
   const std::string& eval_disabled_error_message() const {
     return eval_disabled_error_message_;
@@ -160,7 +154,7 @@
   bool CheckHash(SourceListDirective* directive,
                  const HashValue& hash_value) const;
   bool CheckSource(SourceListDirective* directive, const GURL& url,
-                   ContentSecurityPolicy::RedirectStatus redirect_status) const;
+                   RedirectStatus redirect_status) const;
   bool CheckMediaType(MediaListDirective* directive, const std::string& type,
                       const std::string& type_attribute) const;
 
@@ -173,13 +167,14 @@
   bool CheckInlineAndReportViolation(SourceListDirective* directive,
                                      const std::string& console_message,
                                      const std::string& context_url,
-                                     int context_line, bool is_script,
+                                     int context_line,
+                                     const char* directive_name,
                                      const std::string& hash_value) const;
 
-  bool CheckSourceAndReportViolation(
-      SourceListDirective* directive, const GURL& url,
-      const std::string& effective_directive,
-      ContentSecurityPolicy::RedirectStatus redirect_status) const;
+  bool CheckSourceAndReportViolation(SourceListDirective* directive,
+                                     const GURL& url,
+                                     const std::string& effective_directive,
+                                     RedirectStatus redirect_status) const;
   bool CheckMediaTypeAndReportViolation(
       MediaListDirective* directive, const std::string& type,
       const std::string& type_attribute,
@@ -221,6 +216,7 @@
   std::unique_ptr<SourceListDirective> object_src_;
   std::unique_ptr<SourceListDirective> script_src_;
   std::unique_ptr<SourceListDirective> style_src_;
+  std::unique_ptr<SourceListDirective> worker_src_;
 
   std::vector<std::string> report_endpoints_;
 
diff --git a/cobalt/csp/media_list_directive.cc b/cobalt/csp/media_list_directive.cc
index cf7a5b0..c7cee75 100644
--- a/cobalt/csp/media_list_directive.cc
+++ b/cobalt/csp/media_list_directive.cc
@@ -23,6 +23,10 @@
   Parse(base::StringPiece(value));
 }
 
+MediaListDirective::MediaListDirective(ContentSecurityPolicy* policy,
+                                       const MediaListDirective& other)
+    : Directive(policy, other), plugin_types_(other.plugin_types_) {}
+
 bool MediaListDirective::Allows(const std::string& type) const {
   return plugin_types_.find(type) != plugin_types_.end();
 }
diff --git a/cobalt/csp/media_list_directive.h b/cobalt/csp/media_list_directive.h
index 1934cc9..5aa0e60 100644
--- a/cobalt/csp/media_list_directive.h
+++ b/cobalt/csp/media_list_directive.h
@@ -28,6 +28,8 @@
  public:
   MediaListDirective(const std::string& name, const std::string& value,
                      ContentSecurityPolicy* policy);
+  MediaListDirective(ContentSecurityPolicy* policy,
+                     const MediaListDirective& other);
 
   bool Allows(const std::string& type) const;
 
diff --git a/cobalt/csp/source.cc b/cobalt/csp/source.cc
index 3e5bb5b..932c824 100644
--- a/cobalt/csp/source.cc
+++ b/cobalt/csp/source.cc
@@ -33,10 +33,11 @@
   config_.port_wildcard = config.port_wildcard;
 }
 
+Source::Source(ContentSecurityPolicy* policy, const Source& other)
+    : policy_(policy), config_(other.config_) {}
+
 // https://www.w3.org/TR/2015/CR-CSP2-20150721/#match-source-expression
-bool Source::Matches(
-    const GURL& url,
-    ContentSecurityPolicy::RedirectStatus redirect_status) const {
+bool Source::Matches(const GURL& url, RedirectStatus redirect_status) const {
   if (!SchemeMatches(url)) {
     return false;
   }
@@ -50,8 +51,7 @@
     return false;
   }
 
-  bool paths_match = (redirect_status == ContentSecurityPolicy::kDidRedirect) ||
-                     PathMatches(url);
+  bool paths_match = (redirect_status == kDidRedirect) || PathMatches(url);
   return paths_match;
 }
 
diff --git a/cobalt/csp/source.h b/cobalt/csp/source.h
index 1420953..40d0ae6 100644
--- a/cobalt/csp/source.h
+++ b/cobalt/csp/source.h
@@ -46,9 +46,9 @@
 class Source {
  public:
   Source(ContentSecurityPolicy* policy, const SourceConfig& config);
+  Source(ContentSecurityPolicy* policy, const Source& other);
   bool Matches(const GURL& url,
-               ContentSecurityPolicy::RedirectStatus redirect_status =
-                   ContentSecurityPolicy::kDidNotRedirect) const;
+               RedirectStatus redirect_status = kDidNotRedirect) const;
 
  private:
   bool SchemeMatches(const GURL& url) const;
diff --git a/cobalt/csp/source_list.cc b/cobalt/csp/source_list.cc
index 17345f4..902ba39 100644
--- a/cobalt/csp/source_list.cc
+++ b/cobalt/csp/source_list.cc
@@ -84,9 +84,29 @@
       hash_algorithms_used_(0),
       local_network_checker_(checker) {}
 
-bool SourceList::Matches(
-    const GURL& url,
-    ContentSecurityPolicy::RedirectStatus redirect_status) const {
+SourceList::SourceList(const LocalNetworkCheckerInterface* checker,
+                       ContentSecurityPolicy* policy, const SourceList& other)
+    : policy_(policy),
+      directive_name_(other.directive_name_),
+      allow_self_(other.allow_self_),
+      allow_star_(other.allow_star_),
+      allow_inline_(other.allow_inline_),
+      allow_eval_(other.allow_eval_),
+      allow_insecure_connections_to_local_network_(
+          other.allow_insecure_connections_to_local_network_),
+      allow_insecure_connections_to_localhost_(
+          other.allow_insecure_connections_to_localhost_),
+      allow_insecure_connections_to_private_range_(
+          other.allow_insecure_connections_to_private_range_),
+      hash_algorithms_used_(0),
+      local_network_checker_(checker) {
+  for (Source source : other.list_) {
+    list_.push_back(Source(policy, source));
+  }
+}
+
+bool SourceList::Matches(const GURL& url,
+                         RedirectStatus redirect_status) const {
   if (allow_star_ && SchemeCanMatchStar(url)) {
     return true;
   }
diff --git a/cobalt/csp/source_list.h b/cobalt/csp/source_list.h
index 7e67bfa..b9fffa7 100644
--- a/cobalt/csp/source_list.h
+++ b/cobalt/csp/source_list.h
@@ -43,11 +43,11 @@
 
   SourceList(const LocalNetworkCheckerInterface* checker,
              ContentSecurityPolicy* policy, const std::string& directive_name);
+  SourceList(const LocalNetworkCheckerInterface* checker,
+             ContentSecurityPolicy* policy, const SourceList& other);
   void Parse(const base::StringPiece& begin);
 
-  bool Matches(const GURL& url,
-               ContentSecurityPolicy::RedirectStatus =
-                   ContentSecurityPolicy::kDidNotRedirect) const;
+  bool Matches(const GURL& url, RedirectStatus = kDidNotRedirect) const;
   bool AllowInline() const;
   bool AllowEval() const;
   bool AllowNonce(const std::string& nonce) const;
diff --git a/cobalt/csp/source_list_directive.cc b/cobalt/csp/source_list_directive.cc
index fe4ed2c..4981852 100644
--- a/cobalt/csp/source_list_directive.cc
+++ b/cobalt/csp/source_list_directive.cc
@@ -35,9 +35,13 @@
   source_list_.Parse(base::StringPiece(value));
 }
 
-bool SourceListDirective::Allows(
-    const GURL& url,
-    ContentSecurityPolicy::RedirectStatus redirectStatus) const {
+SourceListDirective::SourceListDirective(ContentSecurityPolicy* policy,
+                                         const SourceListDirective& other)
+    : Directive(policy, other),
+      source_list_(&local_network_checker_, policy, other.source_list_) {}
+
+bool SourceListDirective::Allows(const GURL& url,
+                                 RedirectStatus redirectStatus) const {
   return source_list_.Matches(url.is_empty() ? policy()->url() : url,
                               redirectStatus);
 }
diff --git a/cobalt/csp/source_list_directive.h b/cobalt/csp/source_list_directive.h
index 7a68f4c..0d17ff2 100644
--- a/cobalt/csp/source_list_directive.h
+++ b/cobalt/csp/source_list_directive.h
@@ -34,9 +34,10 @@
 
   SourceListDirective(const std::string& name, const std::string& value,
                       ContentSecurityPolicy* policy);
+  SourceListDirective(ContentSecurityPolicy* policy,
+                      const SourceListDirective& other);
 
-  bool Allows(const GURL& url,
-              ContentSecurityPolicy::RedirectStatus redirect_status) const;
+  bool Allows(const GURL& url, RedirectStatus redirect_status) const;
   bool AllowInline() const;
   bool AllowEval() const;
   bool AllowNonce(const std::string& nonce) const;
diff --git a/cobalt/csp/source_list_test.cc b/cobalt/csp/source_list_test.cc
index 6798ac3..d533b99 100644
--- a/cobalt/csp/source_list_test.cc
+++ b/cobalt/csp/source_list_test.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/csp/source_list.h"
 
+#include <memory>
+
 #include "cobalt/csp/content_security_policy.h"
 #include "cobalt/csp/source.h"
 #include "cobalt/network/local_network.h"
@@ -161,21 +161,21 @@
   SourceList source_list(&checker_, csp_.get(), "script-src");
   ParseSourceList(&source_list, sources);
 
-  EXPECT_TRUE(source_list.Matches(GURL("http://example1.com/foo/"),
-                                  ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source_list.Matches(GURL("http://example1.com/bar/"),
-                                  ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source_list.Matches(GURL("http://example2.com/bar/"),
-                                  ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source_list.Matches(GURL("http://example2.com/foo/"),
-                                  ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source_list.Matches(GURL("https://example1.com/foo/"),
-                                  ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source_list.Matches(GURL("https://example1.com/bar/"),
-                                  ContentSecurityPolicy::kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("http://example1.com/foo/"), kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("http://example1.com/bar/"), kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("http://example2.com/bar/"), kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("http://example2.com/foo/"), kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("https://example1.com/foo/"), kDidRedirect));
+  EXPECT_TRUE(
+      source_list.Matches(GURL("https://example1.com/bar/"), kDidRedirect));
 
-  EXPECT_FALSE(source_list.Matches(GURL("http://example3.com/foo/"),
-                                   ContentSecurityPolicy::kDidRedirect));
+  EXPECT_FALSE(
+      source_list.Matches(GURL("http://example3.com/foo/"), kDidRedirect));
 }
 
 TEST_F(SourceListTest, TestInsecureLocalhostDefaultInsecureV4) {
diff --git a/cobalt/csp/source_test.cc b/cobalt/csp/source_test.cc
index ba7338f..6f22f29 100644
--- a/cobalt/csp/source_test.cc
+++ b/cobalt/csp/source_test.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/csp/source.h"
 
+#include <memory>
+
 #include "cobalt/csp/content_security_policy.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
@@ -104,15 +104,14 @@
 
   Source source(csp_.get(), config);
 
-  EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/"),
-                             ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/foo"),
-                             ContentSecurityPolicy::kDidRedirect));
-  EXPECT_TRUE(source.Matches(GURL("https://example.com:8000/foo"),
-                             ContentSecurityPolicy::kDidRedirect));
+  EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/"), kDidRedirect));
+  EXPECT_TRUE(
+      source.Matches(GURL("http://example.com:8000/foo"), kDidRedirect));
+  EXPECT_TRUE(
+      source.Matches(GURL("https://example.com:8000/foo"), kDidRedirect));
 
-  EXPECT_FALSE(source.Matches(GURL("http://not-example.com:8000/foo"),
-                              ContentSecurityPolicy::kDidRedirect));
+  EXPECT_FALSE(
+      source.Matches(GURL("http://not-example.com:8000/foo"), kDidRedirect));
   EXPECT_FALSE(source.Matches(GURL("http://example.com:9000/foo/")));
 }
 
diff --git a/cobalt/demos/content/background-mode-demo/background-mode-demo.js b/cobalt/demos/content/background-mode-demo/background-mode-demo.js
index 2245eb4..d589a2b 100644
--- a/cobalt/demos/content/background-mode-demo/background-mode-demo.js
+++ b/cobalt/demos/content/background-mode-demo/background-mode-demo.js
@@ -13,9 +13,9 @@
 // limitations under the License.
 
 // The page simply plays an audio or a video stream in a loop, it can be used
-// in the following forms:
-//   background-mode-demo.html&type=video
-//   background-mode-demo.html&type=audio
+// by appending following queryable object to url path, e.g.
+//   http://0.0.0.0:8000/cobalt/demos/content/background-mode-demo?type=video
+//   http://0.0.0.0:8000/cobalt/demos/content/background-mode-demo?type=audio
 // If the stream is adaptive, it has to be fit in memory as this demo will
 // download the whole stream at once.
 
@@ -66,7 +66,7 @@
   mediasource.addEventListener('sourceopen', function () {
     if (type == 'audio') {
       var audio_source_buffer = mediasource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
-	downloadAndAppend('../media-element-demo/dash-audio.mp4', 0, kAdaptiveAudioChunkSize * 10, audio_source_buffer, function () {
+      downloadAndAppend('../media-element-demo/public/assets/dash-audio.mp4', 0, kAdaptiveAudioChunkSize * 10, audio_source_buffer, function () {
         mediasource.endOfStream();
       });
     }
@@ -74,10 +74,10 @@
     if (type == 'video') {
       var video_source_buffer = mediasource.addSourceBuffer('video/mp4; codecs="avc1.640028"');
       var audio_source_buffer = mediasource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
-      downloadAndAppend('../media-element-demo/dash-video-1080p.mp4', 0, 15 * 1024 * 1024, video_source_buffer, function () {
+      downloadAndAppend('../media-element-demo/public/assets/dash-video-1080p.mp4', 0, 15 * 1024 * 1024, video_source_buffer, function () {
         video_source_buffer.abort();
            // Append the first two segments of the 240p video so we can see the transition.
-        downloadAndAppend('../media-element-demo/dash-audio.mp4', 0, kAdaptiveAudioChunkSize * 10, audio_source_buffer, function () {
+        downloadAndAppend('../media-element-demo/public/assets/dash-audio.mp4', 0, kAdaptiveAudioChunkSize * 10, audio_source_buffer, function () {
           mediasource.endOfStream();
         });
       });
@@ -96,9 +96,9 @@
 function checkMediaType() {
   var get_parameters = window.location.search.substr(1).split('&');
   for (var param of get_parameters) {
-    splitted = param.split('=');
-    if (splitted[0] == 'type') {
-      type = splitted[1];
+    split = param.split('=');
+    if (split[0] == 'type') {
+      type = split[1];
     }
   }
 
diff --git a/cobalt/demos/content/eme-demo/eme-demo.js b/cobalt/demos/content/eme-demo/eme-demo.js
index 3a0f299..df0b5dc 100644
--- a/cobalt/demos/content/eme-demo/eme-demo.js
+++ b/cobalt/demos/content/eme-demo/eme-demo.js
@@ -49,6 +49,16 @@
 }).then(function(mediaKeys) {
   var videoElement = document.querySelector('video');
 
+  if (mediaKeys.getMetrics) {
+    console.log('Found getMetrics(), calling it ...');
+    try {
+      mediaKeys.getMetrics();
+      console.log('Calling getMetrics() succeeded.');
+    } catch(e) {
+      console.log('Calling getMetrics() failed.');
+    }
+  }
+
   videoElement.setMediaKeys(mediaKeys);
 
   mediaKeySession = mediaKeys.createSession();
diff --git a/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4 b/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4
new file mode 100644
index 0000000..1c15d74
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4
Binary files differ
diff --git a/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html
new file mode 100644
index 0000000..5ab57d7
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Partial Audio</title>
+  <style>
+    body {
+      background-color: white;
+    }
+    video {
+      height: 240px;
+      width: 426px;
+    }
+  </style>
+</head>
+<body>
+  <script type="text/javascript" src="partial-audio-frame.js"></script>
+  <div id="status"></div>
+  <video id="video"></video>
+</body>
+</html>
diff --git a/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js
new file mode 100644
index 0000000..67f659b
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js
@@ -0,0 +1,133 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+"use strict";
+
+function getFmp4AacData(onDataReady) {
+  const FILE_NAME = 'fmp4-aac-44100-tiny.mp4';
+
+  var xhr = new XMLHttpRequest;
+  xhr.responseType = 'arraybuffer';
+  xhr.addEventListener('readystatechange', function onreadystatechange() {
+    if (xhr.readyState == XMLHttpRequest.DONE) {
+      xhr.removeEventListener('readystatechange', onreadystatechange);
+
+      console.log('Media segment downloaded.');
+      onDataReady(xhr.response);
+    }
+  });
+  xhr.open('GET', FILE_NAME, true);
+  console.log('Sending request for media segment ...');
+  xhr.send();
+}
+
+function createAndAttachMediaSource(onSourceOpen) {
+  var video = document.getElementById('video');
+
+  var mediaSource = new MediaSource;
+  mediaSource.addEventListener('sourceopen', onSourceOpen);
+
+  console.log('Attaching MediaSource to video element ...');
+  video.src = window.URL.createObjectURL(mediaSource);
+}
+
+// Audio access units often contain more than one frame (in this case 1024
+// frames).
+// The function creates an audio stream with AUs that not all frames should be
+// played, like:
+//   ... [*AA*] [*BB*] [*CC*] [*DD*] [*EE*] ...
+// 1. Each character represent ~256 frames.
+// 2. The letter represents the AU index, i.e. C is the next AU of B.
+// 3. '*' means the 256 frames are excluded from playback.
+function appendMediaSegment(mediaSource, sourceBuffer, mediaSegment,
+                            currentOffset) {
+  // The input data is 44100hz, and each AU (access unit) contains 1024 frames.
+  var HALF_AU_DURATION_IN_SECONDS = 1024.0 / 44100 / 2;
+  var MAX_DURATION_IN_SECONDS = 5.;
+
+  if (!currentOffset) {
+    currentOffset = 0.0;
+  }
+
+  sourceBuffer.appendWindowEnd = MAX_DURATION_IN_SECONDS;
+
+  sourceBuffer.addEventListener('updateend', function onupdateend() {
+    sourceBuffer.removeEventListener('updateend', onupdateend);
+    sourceBuffer.abort();
+
+    currentOffset += HALF_AU_DURATION_IN_SECONDS;
+
+    if (currentOffset < MAX_DURATION_IN_SECONDS) {
+      appendMediaSegment(mediaSource, sourceBuffer, mediaSegment,
+                         currentOffset);
+    } else {
+      mediaSource.endOfStream();
+      console.log('video.currentTime is ?');
+      var video = document.getElementById('video');
+      console.log('video.currentTime is ' + video.currentTime);
+      video.currentTime = HALF_AU_DURATION_IN_SECONDS / 2;
+      video.play();
+    }
+  });
+
+  console.log('Set timestampOffset to ' + currentOffset + ' before appending.');
+  // Assuming the buffered AUs are
+  // ... [*AAA] [BBBB] [CCCC] [DDDD] [EEEE] ...
+  //
+  // We setup the append by shifting `currentOffset` for 1/2 of an AU (so it
+  // points to the middle of the AU), and `appendWindowStart` for 1/4 of AU
+  // after `currentOffset`:
+  //  currentOffset
+  //        v
+  // ... [*A A A] [BBBB] [CCCC] [DDDD] [EEEE] ...
+  //          ^
+  //   appendWindowStart
+  //
+  // So the new append will start from `currentOffset`, but the first 256 frames
+  // will be masked due to `appendWindowStart`.  The result will be:
+  // ... [*AA*] (all remaining AUs get replaced)
+  // ...   [*XXX] [YYYY] [ZZZZ] ...
+  // i.e.
+  // ... [*AA*] [*XXX] [YYYY] [ZZZZ] ...
+  // This results an AU with first and last 256 frames (out of 1024 frames)
+  // excluded from playback.  A non-conforming implementation will play the
+  // whole AU which takes twice of the time needed.
+  sourceBuffer.timestampOffset = currentOffset;
+  var appendWindowStart = currentOffset + HALF_AU_DURATION_IN_SECONDS / 2;
+  if (currentOffset > 0 && appendWindowStart < sourceBuffer.appendWindowEnd) {
+    sourceBuffer.appendWindowStart = appendWindowStart;
+  }
+  sourceBuffer.appendBuffer(mediaSegment);
+}
+
+function playPartialAudio() {
+  window.setInterval(function() {
+    document.getElementById('status').textContent =
+        'currentTime ' + document.getElementById('video').currentTime;
+  }, 100);
+
+  getFmp4AacData(function(mediaSegment) {
+    createAndAttachMediaSource(function(event) {
+      var mediaSource = event.target;
+
+      console.log('Adding SourceBuffer ...');
+      var sourceBuffer =
+          mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
+
+      appendMediaSegment(mediaSource, sourceBuffer, mediaSegment);
+    });
+  });
+}
+
+addEventListener('load', playPartialAudio);
diff --git a/cobalt/doc/lifecycle.md b/cobalt/doc/lifecycle.md
index 6de3346..2248908 100644
--- a/cobalt/doc/lifecycle.md
+++ b/cobalt/doc/lifecycle.md
@@ -84,7 +84,7 @@
 
 ### Deprecated `SbSystemRequest` functions.
 
-The `SbSytemRequest` functions are declared in `src/starboard/system.h`
+The `SbSystemRequest` functions are declared in `src/starboard/system.h`
 
 * The `SbSystemRequestPause` event is renamed to `SbSystemRequestBlur`
 * The `SbSystemRequestUnpause` event is renamed to `SbSystemRequestFocus`
diff --git a/cobalt/doc/platform_services.md b/cobalt/doc/platform_services.md
index dadf6ae..8860387 100644
--- a/cobalt/doc/platform_services.md
+++ b/cobalt/doc/platform_services.md
@@ -65,7 +65,7 @@
 Implementing the Starboard layer of Platform Service extension support uses the
 following interface in parallel with the IDL interface:
 
-*   [src/cobalt/extension/platform\_service.h](../extension/platform_service.h)
+*   [starboard/extension/platform\_service.h](../extension/platform_service.h)
 
 `CobaltExtensionPlatformServiceApi` is the main interface for the Starboard
 layer.
diff --git a/cobalt/dom/BUILD.gn b/cobalt/dom/BUILD.gn
index ff47acc..e25b7f2 100644
--- a/cobalt/dom/BUILD.gn
+++ b/cobalt/dom/BUILD.gn
@@ -14,6 +14,12 @@
 
 import("//cobalt/build/contents_dir.gni")
 
+source_set("media_settings") {
+  has_pedantic_warnings = true
+  sources = [ "media_settings.h" ]
+  public_deps = [ "//cobalt/base" ]
+}
+
 static_library("dom") {
   has_pedantic_warnings = true
 
@@ -185,10 +191,9 @@
     "lottie_player.h",
     "media_query_list.cc",
     "media_query_list.h",
+    "media_settings.cc",
     "media_source.cc",
     "media_source.h",
-    "media_source_settings.cc",
-    "media_source_settings.h",
     "memory_info.cc",
     "memory_info.h",
     "mime_type_array.cc",
@@ -310,6 +315,7 @@
   }
 
   public_deps = [
+    ":media_settings",
     "//cobalt/browser:generated_types",
     "//cobalt/web",
     "//cobalt/web:window_timers",
@@ -387,7 +393,7 @@
     "local_storage_database_test.cc",
     "location_test.cc",
     "media_query_list_test.cc",
-    "media_source_settings_test.cc",
+    "media_settings_test.cc",
     "mutation_observer_test.cc",
     "named_node_map_test.cc",
     "navigator_licenses_test.cc",
diff --git a/cobalt/dom/dom_settings.cc b/cobalt/dom/dom_settings.cc
index 8390913..7ddf8cb 100644
--- a/cobalt/dom/dom_settings.cc
+++ b/cobalt/dom/dom_settings.cc
@@ -28,7 +28,6 @@
 DOMSettings::DOMSettings(
     const base::DebuggerHooks& debugger_hooks, const int max_dom_element_depth,
     MediaSourceRegistry* media_source_registry,
-    const MediaSourceSettings* media_source_settings,
     media::CanPlayTypeHandler* can_play_type_handler,
     const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info,
     MutationObserverTaskManager* mutation_observer_task_manager,
@@ -37,7 +36,6 @@
       max_dom_element_depth_(max_dom_element_depth),
       microphone_options_(options.microphone_options),
       media_source_registry_(media_source_registry),
-      media_source_settings_(media_source_settings),
       can_play_type_handler_(can_play_type_handler),
       decoder_buffer_memory_info_(decoder_buffer_memory_info),
       mutation_observer_task_manager_(mutation_observer_task_manager) {}
diff --git a/cobalt/dom/dom_settings.h b/cobalt/dom/dom_settings.h
index 58e221f..d2be588 100644
--- a/cobalt/dom/dom_settings.h
+++ b/cobalt/dom/dom_settings.h
@@ -18,7 +18,6 @@
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
 #include "cobalt/base/debugger_hooks.h"
-#include "cobalt/dom/media_source_settings.h"
 #include "cobalt/dom/mutation_observer_task_manager.h"
 #include "cobalt/media/can_play_type_handler.h"
 #include "cobalt/media/decoder_buffer_memory_info.h"
@@ -57,7 +56,6 @@
   DOMSettings(const base::DebuggerHooks& debugger_hooks,
               const int max_dom_element_depth,
               MediaSourceRegistry* media_source_registry,
-              const MediaSourceSettings* media_source_settings,
               media::CanPlayTypeHandler* can_play_type_handler,
               const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info,
               MutationObserverTaskManager* mutation_observer_task_manager,
@@ -74,9 +72,6 @@
   MediaSourceRegistry* media_source_registry() const {
     return media_source_registry_;
   }
-  const MediaSourceSettings* media_source_settings() const {
-    return media_source_settings_;
-  }
   void set_decoder_buffer_memory_info(
       const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info) {
     decoder_buffer_memory_info_ = decoder_buffer_memory_info;
@@ -103,7 +98,6 @@
   const int max_dom_element_depth_;
   const speech::Microphone::Options microphone_options_;
   MediaSourceRegistry* media_source_registry_;
-  const MediaSourceSettings* media_source_settings_;
   media::CanPlayTypeHandler* can_play_type_handler_;
   const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info_;
   MutationObserverTaskManager* mutation_observer_task_manager_;
diff --git a/cobalt/dom/global_event_handlers.idl b/cobalt/dom/global_event_handlers.idl
index c877347..c98824e 100644
--- a/cobalt/dom/global_event_handlers.idl
+++ b/cobalt/dom/global_event_handlers.idl
@@ -51,6 +51,7 @@
   //  https://www.w3.org/TR/2015/REC-pointerevents-20150224/#extensions-to-the-globaleventhandlers-interface
   attribute EventHandler ongotpointercapture;
   attribute EventHandler onlostpointercapture;
+  attribute EventHandler onpointercancel;
   attribute EventHandler onpointerdown;
   attribute EventHandler onpointerenter;
   attribute EventHandler onpointerleave;
diff --git a/cobalt/dom/html_element.cc b/cobalt/dom/html_element.cc
index 7b73605..a30f22b 100644
--- a/cobalt/dom/html_element.cc
+++ b/cobalt/dom/html_element.cc
@@ -1121,7 +1121,7 @@
 void HTMLElement::PurgeCachedBackgroundImages() {
   ClearActiveBackgroundImages();
   if (!cached_background_images_.empty()) {
-    cached_background_images_.clear();
+    ClearCachedBackgroundImages();
     computed_style_valid_ = false;
     descendant_computed_styles_valid_ = false;
   }
@@ -1221,7 +1221,7 @@
 void HTMLElement::OnUiNavScroll(SbTimeMonotonic /* time */) {
   Document* document = node_document();
   scoped_refptr<Window> window(document ? document->window() : nullptr);
-  DispatchEvent(new UIEvent(base::Tokens::scroll(), web::Event::kBubbles,
+  DispatchEvent(new UIEvent(base::Tokens::scroll(), web::Event::kNotBubbles,
                             web::Event::kNotCancelable, window));
 }
 
@@ -2277,6 +2277,29 @@
   active_background_images_.clear();
 }
 
+void HTMLElement::ClearCachedBackgroundImages() {
+  // |cached_background_images_.clear()| cannot be used as it may lead to crash
+  // due to GetLoadTimingInfoAndCreateResourceTiming() being called indirectly
+  // from CachedImage dtor, and GetLoadTimingInfoAndCreateResourceTiming() loops
+  // on the |cached_background_images_| being cleared.
+  //
+  // To move and clear() like below is more straight-forward but leads to more
+  // in flight image loading performance timings get lost.
+  //   auto images = std::move(cached_background_images_);
+  //   DCHECK(cached_background_images_.empty());
+  //   images.clear();
+  //
+  // So images are moved out and cleared one by one.
+  while (!cached_background_images_.empty()) {
+    auto image = std::move(cached_background_images_.back());
+    DCHECK(!cached_background_images_.back());
+    cached_background_images_.pop_back();
+    // TODO(b/265089478): This implementation will lose the performance timing
+    // of |image|, consider refining to record all performance timings.
+    image = nullptr;
+  }
+}
+
 void HTMLElement::UpdateCachedBackgroundImagesFromComputedStyle() {
   ClearActiveBackgroundImages();
 
@@ -2320,10 +2343,11 @@
               cached_image, loaded_callback, base::Closure()));
     }
 
+    ClearCachedBackgroundImages();
     cached_background_images_ = std::move(cached_images);
   } else {
     // Clear the previous cached background image if the display is "none".
-    cached_background_images_.clear();
+    ClearCachedBackgroundImages();
   }
 }
 
diff --git a/cobalt/dom/html_element.h b/cobalt/dom/html_element.h
index 18f4258..e2a6949 100644
--- a/cobalt/dom/html_element.h
+++ b/cobalt/dom/html_element.h
@@ -440,6 +440,9 @@
   // Clear the list of active background images, and notify the animated image
   // tracker to stop the animations.
   void ClearActiveBackgroundImages();
+  // Carefully clear the list of cached background images to avoid crashing in
+  // the nested `HTMLElement::GetLoadTimingInfoAndCreateResourceTiming()` call.
+  void ClearCachedBackgroundImages();
 
   void UpdateCachedBackgroundImagesFromComputedStyle();
 
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index e4fb384b..5812afb 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -31,30 +31,29 @@
 #include "cobalt/base/tokens.h"
 #include "cobalt/cssom/map_to_mesh_function.h"
 #include "cobalt/dom/document.h"
+#include "cobalt/dom/eme/media_encrypted_event.h"
+#include "cobalt/dom/eme/media_encrypted_event_init.h"
 #include "cobalt/dom/html_element_context.h"
 #include "cobalt/dom/html_video_element.h"
+#include "cobalt/dom/media_settings.h"
 #include "cobalt/dom/media_source.h"
 #include "cobalt/dom/media_source_ready_state.h"
 #include "cobalt/loader/fetcher_factory.h"
-#include "cobalt/media/fetcher_buffered_data_source.h"
+#include "cobalt/media/url_fetcher_data_source.h"
 #include "cobalt/media/web_media_player_factory.h"
 #include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/context.h"
 #include "cobalt/web/csp_delegate.h"
 #include "cobalt/web/dom_exception.h"
 #include "cobalt/web/event.h"
-
-#include "cobalt/dom/eme/media_encrypted_event.h"
-#include "cobalt/dom/eme/media_encrypted_event_init.h"
+#include "cobalt/web/web_settings.h"
 
 namespace cobalt {
 namespace dom {
 
-using media::BufferedDataSource;
+using media::DataSource;
 using media::WebMediaPlayer;
 
-const char HTMLMediaElement::kMediaSourceUrlProtocol[] = "blob";
-const double HTMLMediaElement::kMaxTimeupdateEventFrequency = 0.25;
-
 namespace {
 
 #define LOG_MEDIA_ELEMENT_ACTIVITIES 0
@@ -69,6 +68,9 @@
 
 #endif  // LOG_MEDIA_ELEMENT_ACTIVITIES
 
+constexpr char kMediaSourceUrlProtocol[] = "blob";
+constexpr int kTimeupdateEventIntervalInMilliseconds = 200;
+
 DECLARE_INSTANCE_COUNTER(HTMLMediaElement);
 
 loader::RequestMode GetRequestMode(
@@ -818,8 +820,8 @@
                                     const std::string& key_system) {
   DLOG(INFO) << "HTMLMediaElement::LoadResource(" << initial_url << ", "
              << content_type << ", " << key_system;
-  DCHECK(player_);
   if (!player_) {
+    LOG(WARNING) << "LoadResource() without player.";
     return;
   }
 
@@ -887,11 +889,10 @@
                    web::CspDelegate::kMedia);
     request_mode_ = GetRequestMode(GetAttribute("crossOrigin"));
     DCHECK(node_document()->location());
-    std::unique_ptr<BufferedDataSource> data_source(
-        new media::FetcherBufferedDataSource(
-            base::MessageLoop::current()->task_runner(), url, csp_callback,
-            html_element_context()->fetcher_factory()->network_module(),
-            request_mode_, node_document()->location()->GetOriginAsObject()));
+    std::unique_ptr<DataSource> data_source(new media::URLFetcherDataSource(
+        base::MessageLoop::current()->task_runner(), url, csp_callback,
+        html_element_context()->fetcher_factory()->network_module(),
+        request_mode_, node_document()->location()->GetOriginAsObject()));
     player_->LoadProgressive(url, std::move(data_source));
   }
 }
@@ -1025,10 +1026,19 @@
   }
 
   previous_progress_time_ = base::Time::Now().ToDoubleT();
+
+  DCHECK(environment_settings());
+  DCHECK(environment_settings()->context());
+  DCHECK(environment_settings()->context()->web_settings());
+
+  const auto& media_settings =
+      environment_settings()->context()->web_settings()->media_settings();
+  const int interval_in_milliseconds =
+      media_settings.GetMediaElementTimeupdateEventIntervalInMilliseconds()
+          .value_or(kTimeupdateEventIntervalInMilliseconds);
+
   playback_progress_timer_.Start(
-      FROM_HERE,
-      base::TimeDelta::FromMilliseconds(
-          static_cast<int64>(kMaxTimeupdateEventFrequency * 1000)),
+      FROM_HERE, base::TimeDelta::FromMilliseconds(interval_in_milliseconds),
       this, &HTMLMediaElement::OnPlaybackProgressTimer);
 }
 
diff --git a/cobalt/dom/html_media_element.h b/cobalt/dom/html_media_element.h
index 05e7a0e..4276fd8 100644
--- a/cobalt/dom/html_media_element.h
+++ b/cobalt/dom/html_media_element.h
@@ -161,9 +161,6 @@
   const WebMediaPlayer* player() const { return player_.get(); }
 
  private:
-  static const char kMediaSourceUrlProtocol[];
-  static const double kMaxTimeupdateEventFrequency;
-
   // Loading
   void CreateMediaPlayer();
   void ScheduleLoad();
diff --git a/cobalt/dom/html_video_element.idl b/cobalt/dom/html_video_element.idl
index d6decd5..c54a9f4 100644
--- a/cobalt/dom/html_video_element.idl
+++ b/cobalt/dom/html_video_element.idl
@@ -32,5 +32,5 @@
   // INVALID_STATE_ERR exception is raised. The format of the string passed in
   // is the same as the format for the string passed in to
   // HTMLMediaElement.canPlayType().
-  [Conditional=COBALT_ENABLE_SET_MAX_VIDEO_CAPABILITIES, RaisesException] void setMaxVideoCapabilities(DOMString max_video_capabilities);
+  [RaisesException] void setMaxVideoCapabilities(DOMString max_video_capabilities);
 };
diff --git a/cobalt/dom/media_settings.cc b/cobalt/dom/media_settings.cc
new file mode 100644
index 0000000..411889d
--- /dev/null
+++ b/cobalt/dom/media_settings.cc
@@ -0,0 +1,83 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/dom/media_settings.h"
+
+#include <cstring>
+
+#include "base/logging.h"
+
+namespace cobalt {
+namespace dom {
+
+bool MediaSettingsImpl::Set(const std::string& name, int value) {
+  const char* kPrefixes[] = {"MediaElement.", "MediaSource."};
+  if (name.compare(0, strlen(kPrefixes[0]), kPrefixes[0]) != 0 &&
+      name.compare(0, strlen(kPrefixes[1]), kPrefixes[1]) != 0) {
+    return false;
+  }
+
+  base::AutoLock auto_lock(lock_);
+  if (name == "MediaSource.SourceBufferEvictExtraInBytes") {
+    if (value >= 0) {
+      source_buffer_evict_extra_in_bytes_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.MinimumProcessorCountToOffloadAlgorithm") {
+    if (value >= 0) {
+      minimum_processor_count_to_offload_algorithm_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.EnableAsynchronousReduction") {
+    if (value == 0 || value == 1) {
+      is_asynchronous_reduction_enabled_ = value != 0;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.EnableAvoidCopyingArrayBuffer") {
+    if (value == 0 || value == 1) {
+      is_avoid_copying_array_buffer_enabled_ = value != 0;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.MaxSizeForImmediateJob") {
+    if (value >= 0) {
+      max_size_for_immediate_job_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.MaxSourceBufferAppendSizeInBytes") {
+    if (value > 0) {
+      max_source_buffer_append_size_in_bytes_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaElement.TimeupdateEventIntervalInMilliseconds") {
+    if (value > 0) {
+      media_element_timeupdate_event_interval_in_milliseconds_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else {
+    LOG(WARNING) << "Ignore unknown setting with name \"" << name << "\"";
+    return false;
+  }
+  LOG(WARNING) << name << ": ignore invalid value " << value;
+  return false;
+}
+
+}  // namespace dom
+}  // namespace cobalt
diff --git a/cobalt/dom/media_settings.h b/cobalt/dom/media_settings.h
new file mode 100644
index 0000000..a6acb1c
--- /dev/null
+++ b/cobalt/dom/media_settings.h
@@ -0,0 +1,108 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_DOM_MEDIA_SETTINGS_H_
+#define COBALT_DOM_MEDIA_SETTINGS_H_
+
+#include <string>
+
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+
+namespace cobalt {
+namespace dom {
+
+// Holds browser wide settings for media related objects.  Their default values
+// are set in related implementations, and the default values will be overridden
+// if the return values of the member functions are non-empty.
+// Please refer to where these functions are called for the particular
+// behaviors being controlled by them.
+class MediaSettings {
+ public:
+  virtual base::Optional<int> GetSourceBufferEvictExtraInBytes() const = 0;
+  virtual base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
+      const = 0;
+  virtual base::Optional<bool> IsAsynchronousReductionEnabled() const = 0;
+  virtual base::Optional<bool> IsAvoidCopyingArrayBufferEnabled() const = 0;
+  virtual base::Optional<int> GetMaxSizeForImmediateJob() const = 0;
+  virtual base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const = 0;
+
+  virtual base::Optional<int>
+  GetMediaElementTimeupdateEventIntervalInMilliseconds() const = 0;
+
+ protected:
+  MediaSettings() = default;
+  ~MediaSettings() = default;
+
+  MediaSettings(const MediaSettings&) = delete;
+  MediaSettings& operator=(const MediaSettings&) = delete;
+};
+
+// Allows setting the values of MediaSource settings via a name and an int
+// value.
+// This class is thread safe.
+class MediaSettingsImpl : public MediaSettings {
+ public:
+  base::Optional<int> GetSourceBufferEvictExtraInBytes() const override {
+    base::AutoLock auto_lock(lock_);
+    return source_buffer_evict_extra_in_bytes_;
+  }
+  base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
+      const override {
+    base::AutoLock auto_lock(lock_);
+    return minimum_processor_count_to_offload_algorithm_;
+  }
+  base::Optional<bool> IsAsynchronousReductionEnabled() const override {
+    base::AutoLock auto_lock(lock_);
+    return is_asynchronous_reduction_enabled_;
+  }
+  base::Optional<bool> IsAvoidCopyingArrayBufferEnabled() const override {
+    base::AutoLock auto_lock(lock_);
+    return is_avoid_copying_array_buffer_enabled_;
+  }
+  base::Optional<int> GetMaxSizeForImmediateJob() const override {
+    base::AutoLock auto_lock(lock_);
+    return max_size_for_immediate_job_;
+  }
+  base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const override {
+    base::AutoLock auto_lock(lock_);
+    return max_source_buffer_append_size_in_bytes_;
+  }
+
+  base::Optional<int> GetMediaElementTimeupdateEventIntervalInMilliseconds()
+      const override {
+    return media_element_timeupdate_event_interval_in_milliseconds_;
+  }
+
+  // Returns true when the setting associated with `name` is set to `value`.
+  // Returns false when `name` is not associated with any settings, or if
+  // `value` contains an invalid value.
+  bool Set(const std::string& name, int value);
+
+ private:
+  mutable base::Lock lock_;
+  base::Optional<int> source_buffer_evict_extra_in_bytes_;
+  base::Optional<int> minimum_processor_count_to_offload_algorithm_;
+  base::Optional<bool> is_asynchronous_reduction_enabled_;
+  base::Optional<bool> is_avoid_copying_array_buffer_enabled_;
+  base::Optional<int> max_size_for_immediate_job_;
+  base::Optional<int> max_source_buffer_append_size_in_bytes_;
+
+  base::Optional<int> media_element_timeupdate_event_interval_in_milliseconds_;
+};
+
+}  // namespace dom
+}  // namespace cobalt
+
+#endif  // COBALT_DOM_MEDIA_SETTINGS_H_
diff --git a/cobalt/dom/media_settings_test.cc b/cobalt/dom/media_settings_test.cc
new file mode 100644
index 0000000..9b10847
--- /dev/null
+++ b/cobalt/dom/media_settings_test.cc
@@ -0,0 +1,144 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/dom/media_settings.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace dom {
+namespace {
+
+TEST(MediaSettingsImplTest, Empty) {
+  MediaSettingsImpl impl;
+
+  EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
+  EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
+  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
+  EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled());
+  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
+  EXPECT_FALSE(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds());
+}
+
+TEST(MediaSettingsImplTest, SunnyDay) {
+  MediaSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 100));
+  ASSERT_TRUE(
+      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 101));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 103));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 100000));
+  ASSERT_TRUE(
+      impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 100001));
+
+  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 100);
+  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 101);
+  EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
+  EXPECT_TRUE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 103);
+  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 100000);
+  EXPECT_EQ(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds().value(),
+            100001);
+}
+
+TEST(MediaSettingsImplTest, RainyDay) {
+  MediaSettingsImpl impl;
+
+  ASSERT_FALSE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", -100));
+  ASSERT_FALSE(
+      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", -101));
+  ASSERT_FALSE(impl.Set("MediaSource.EnableAsynchronousReduction", 2));
+  ASSERT_FALSE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 2));
+  ASSERT_FALSE(impl.Set("MediaSource.MaxSizeForImmediateJob", -103));
+  ASSERT_FALSE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 0));
+  ASSERT_FALSE(
+      impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 0));
+
+  EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
+  EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
+  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
+  EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled());
+  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
+  EXPECT_FALSE(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds());
+}
+
+TEST(MediaSettingsImplTest, ZeroValuesWork) {
+  MediaSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
+  ASSERT_TRUE(
+      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+  // O is an invalid value for "MediaSource.MaxSourceBufferAppendSizeInBytes".
+  // O is an invalid value for
+  // "MediaElement.TimeupdateEventIntervalInMilliseconds".
+
+  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 0);
+  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 0);
+  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled().value());
+  EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 0);
+}
+
+TEST(MediaSettingsImplTest, Updatable) {
+  MediaSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
+  ASSERT_TRUE(
+      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 1));
+  ASSERT_TRUE(
+      impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 1));
+
+  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 1));
+  ASSERT_TRUE(
+      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 2));
+  ASSERT_TRUE(
+      impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 2));
+
+  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 1);
+  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 1);
+  EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
+  EXPECT_TRUE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 1);
+  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 2);
+  EXPECT_EQ(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds().value(),
+            2);
+}
+
+TEST(MediaSettingsImplTest, InvalidSettingNames) {
+  MediaSettingsImpl impl;
+
+  ASSERT_FALSE(impl.Set("MediaSource.Invalid", 0));
+  ASSERT_FALSE(impl.Set("MediaElement.Invalid", 1));
+  ASSERT_FALSE(impl.Set("Invalid.SourceBufferEvictExtraInBytes", 0));
+  ASSERT_FALSE(impl.Set("Invalid.TimeupdateEventIntervalInMilliseconds", 1));
+}
+
+}  // namespace
+}  // namespace dom
+}  // namespace cobalt
diff --git a/cobalt/dom/media_source.cc b/cobalt/dom/media_source.cc
index a3571f1..e3e5223 100644
--- a/cobalt/dom/media_source.cc
+++ b/cobalt/dom/media_source.cc
@@ -57,6 +57,8 @@
 #include "cobalt/base/polymorphic_downcast.h"
 #include "cobalt/base/tokens.h"
 #include "cobalt/dom/dom_settings.h"
+#include "cobalt/dom/media_settings.h"
+#include "cobalt/web/context.h"
 #include "cobalt/web/dom_exception.h"
 #include "cobalt/web/event.h"
 #include "starboard/media.h"
@@ -72,12 +74,13 @@
 using ::media::PIPELINE_OK;
 using ::media::PipelineStatus;
 
-auto GetMediaSettings(script::EnvironmentSettings* settings) {
-  DOMSettings* dom_settings =
-      base::polymorphic_downcast<DOMSettings*>(settings);
-  DCHECK(dom_settings);
-  DCHECK(dom_settings->media_source_settings());
-  return dom_settings->media_source_settings();
+const MediaSettings& GetMediaSettings(web::EnvironmentSettings* settings) {
+  DCHECK(settings);
+  DCHECK(settings->context());
+  DCHECK(settings->context()->web_settings());
+
+  const auto& web_settings = settings->context()->web_settings();
+  return web_settings->media_settings();
 }
 
 // If the system has more processors than the specified value, SourceBuffer
@@ -86,10 +89,10 @@
 // The default value is 1024, which effectively disable offloading by default.
 // Setting to a reasonably low value (say 0 or 2) will enable algorithm
 // offloading.
-bool IsAlgorithmOffloadEnabled(script::EnvironmentSettings* settings) {
+bool IsAlgorithmOffloadEnabled(web::EnvironmentSettings* settings) {
   int min_process_count_to_offload =
       GetMediaSettings(settings)
-          ->GetMinimumProcessorCountToOffloadAlgorithm()
+          .GetMinimumProcessorCountToOffloadAlgorithm()
           .value_or(1024);
   DCHECK_GE(min_process_count_to_offload, 0);
   return SbSystemGetNumberOfProcessors() >= min_process_count_to_offload;
@@ -99,21 +102,20 @@
 // behaviors.  For example, queued events will be dispatached immediately when
 // possible.
 // The default value is false.
-bool IsAsynchronousReductionEnabled(script::EnvironmentSettings* settings) {
-  return GetMediaSettings(settings)->IsAsynchronousReductionEnabled().value_or(
+bool IsAsynchronousReductionEnabled(web::EnvironmentSettings* settings) {
+  return GetMediaSettings(settings).IsAsynchronousReductionEnabled().value_or(
       false);
 }
 
 // If the size of a job that is part of an algorithm is less than or equal to
 // the return value of this function, the implementation will run the job
 // immediately instead of scheduling it to run later to reduce latency.
-// NOTE: This only works when IsAsynchronousReductionEnabled() returns true,
-//       and it is currently only enabled for buffer append.
-// The default value is 16 KB.  Set to 0 will disable immediate job completely.
-int GetMaxSizeForImmediateJob(script::EnvironmentSettings* settings) {
-  const int kDefaultMaxSize = 16 * 1024;
+// NOTE: This is currently only enabled for buffer append.
+// The default value is 0 KB, which disables immediate job completely.
+int GetMaxSizeForImmediateJob(web::EnvironmentSettings* settings) {
+  const int kDefaultMaxSize = 0;
   auto max_size =
-      GetMediaSettings(settings)->GetMaxSizeForImmediateJob().value_or(
+      GetMediaSettings(settings).GetMaxSizeForImmediateJob().value_or(
           kDefaultMaxSize);
   DCHECK_GE(max_size, 0);
   return max_size;
@@ -123,9 +125,12 @@
 
 MediaSource::MediaSource(script::EnvironmentSettings* settings)
     : web::EventTarget(settings),
-      algorithm_offload_enabled_(IsAlgorithmOffloadEnabled(settings)),
-      asynchronous_reduction_enabled_(IsAsynchronousReductionEnabled(settings)),
-      max_size_for_immediate_job_(GetMaxSizeForImmediateJob(settings)),
+      algorithm_offload_enabled_(
+          IsAlgorithmOffloadEnabled(environment_settings())),
+      asynchronous_reduction_enabled_(
+          IsAsynchronousReductionEnabled(environment_settings())),
+      max_size_for_immediate_job_(
+          GetMaxSizeForImmediateJob(environment_settings())),
       default_algorithm_runner_(asynchronous_reduction_enabled_),
       chunk_demuxer_(NULL),
       ready_state_(kMediaSourceReadyStateClosed),
@@ -577,9 +582,20 @@
 
 SerializedAlgorithmRunner<SourceBufferAlgorithm>*
 MediaSource::GetAlgorithmRunner(int job_size) {
+  if (!asynchronous_reduction_enabled_ &&
+      job_size <= max_size_for_immediate_job_) {
+    // `default_algorithm_runner_` won't run jobs immediately when
+    // `asynchronous_reduction_enabled_` is false, so we use
+    // `immediate_job_algorithm_runner_` instead, which always has asynchronous
+    // reduction enabled.
+    return &immediate_job_algorithm_runner_;
+  }
   if (!offload_algorithm_runner_) {
     return &default_algorithm_runner_;
   }
+  // The logic below is redundant as the code for immediate job can be
+  // consolidated with value of `asynchronous_reduction_enabled_` ignored.  It's
+  // kept as is to leave existing behavior unchanged.
   if (asynchronous_reduction_enabled_ &&
       job_size <= max_size_for_immediate_job_) {
     // Append without posting new tasks is only supported on the default runner.
@@ -599,8 +615,15 @@
 }
 
 void MediaSource::SetReadyState(MediaSourceReadyState ready_state) {
-  if (ready_state == kMediaSourceReadyStateClosed) {
-    chunk_demuxer_ = NULL;
+  if (!offload_algorithm_runner_) {
+    // Setting `chunk_demuxer_` to NULL when there is an active algorithm
+    // running may cause crash.  So `chunk_demuxer_` is reset later in the
+    // function.
+    // When `offload_algorithm_runner_` is null, the logic is kept as is to
+    // ensure that the behavior stays the same when offload is not enabled.
+    if (ready_state == kMediaSourceReadyStateClosed) {
+      chunk_demuxer_ = NULL;
+    }
   }
 
   if (ready_state_ == ready_state) {
@@ -640,6 +663,7 @@
     algorithm_process_thread_.reset();
   }
   offload_algorithm_runner_.reset();
+  chunk_demuxer_ = NULL;
 }
 
 bool MediaSource::IsUpdating() const {
diff --git a/cobalt/dom/media_source.h b/cobalt/dom/media_source.h
index 1c84e8b..0daa77a 100644
--- a/cobalt/dom/media_source.h
+++ b/cobalt/dom/media_source.h
@@ -151,6 +151,11 @@
 
   // The default algorithm runner runs all steps on the web thread.
   DefaultAlgorithmRunner<SourceBufferAlgorithm> default_algorithm_runner_;
+  // The immediate job algorithm runner runs job immediately on the web thread,
+  // it has asynchronous reduction always enabled to run immediate jobs even
+  // when asynchronous reduction is disabled on the `default_algorithm_runner_`.
+  DefaultAlgorithmRunner<SourceBufferAlgorithm> immediate_job_algorithm_runner_{
+      true};
   // The offload algorithm runner offloads some steps to a non-web thread.
   std::unique_ptr<OffloadAlgorithmRunner<SourceBufferAlgorithm>>
       offload_algorithm_runner_;
diff --git a/cobalt/dom/media_source_settings.cc b/cobalt/dom/media_source_settings.cc
deleted file mode 100644
index d39602a..0000000
--- a/cobalt/dom/media_source_settings.cc
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/dom/media_source_settings.h"
-
-#include <cstring>
-
-#include "base/logging.h"
-
-namespace cobalt {
-namespace dom {
-
-bool MediaSourceSettingsImpl::Set(const std::string& name, int value) {
-  const char kPrefix[] = "MediaSource.";
-  if (name.compare(0, strlen(kPrefix), kPrefix) != 0) {
-    return false;
-  }
-
-  base::AutoLock auto_lock(lock_);
-  if (name == "MediaSource.SourceBufferEvictExtraInBytes") {
-    if (value >= 0) {
-      source_buffer_evict_extra_in_bytes_ = value;
-      LOG(INFO) << name << ": set to " << value;
-      return true;
-    }
-  } else if (name == "MediaSource.MinimumProcessorCountToOffloadAlgorithm") {
-    if (value >= 0) {
-      minimum_processor_count_to_offload_algorithm_ = value;
-      LOG(INFO) << name << ": set to " << value;
-      return true;
-    }
-  } else if (name == "MediaSource.EnableAsynchronousReduction") {
-    if (value == 0 || value == 1) {
-      is_asynchronous_reduction_enabled_ = value != 0;
-      LOG(INFO) << name << ": set to " << value;
-      return true;
-    }
-  } else if (name == "MediaSource.MaxSizeForImmediateJob") {
-    if (value >= 0) {
-      max_size_for_immediate_job_ = value;
-      LOG(INFO) << name << ": set to " << value;
-      return true;
-    }
-  } else if (name == "MediaSource.MaxSourceBufferAppendSizeInBytes") {
-    if (value > 0) {
-      max_source_buffer_append_size_in_bytes_ = value;
-      LOG(INFO) << name << ": set to " << value;
-      return true;
-    }
-  } else {
-    LOG(WARNING) << "Ignore unknown setting with name \"" << name << "\"";
-    return false;
-  }
-  LOG(WARNING) << name << ": ignore invalid value " << value;
-  return false;
-}
-
-}  // namespace dom
-}  // namespace cobalt
diff --git a/cobalt/dom/media_source_settings.h b/cobalt/dom/media_source_settings.h
deleted file mode 100644
index a122834..0000000
--- a/cobalt/dom/media_source_settings.h
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
-#define COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
-
-#include <string>
-
-#include "base/optional.h"
-#include "base/synchronization/lock.h"
-
-namespace cobalt {
-namespace dom {
-
-// Holds WebModule wide settings for MediaSource related objects.  Their default
-// values are set in MediaSource related implementations, and the default values
-// will be overridden if the return values of the member functions are
-// non-empty.
-// Please refer to where these functions are called for the particular
-// MediaSource behaviors being controlled by them.
-class MediaSourceSettings {
- public:
-  virtual base::Optional<int> GetSourceBufferEvictExtraInBytes() const = 0;
-  virtual base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
-      const = 0;
-  virtual base::Optional<bool> IsAsynchronousReductionEnabled() const = 0;
-  virtual base::Optional<int> GetMaxSizeForImmediateJob() const = 0;
-  virtual base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const = 0;
-
- protected:
-  MediaSourceSettings() = default;
-  ~MediaSourceSettings() = default;
-
-  MediaSourceSettings(const MediaSourceSettings&) = delete;
-  MediaSourceSettings& operator=(const MediaSourceSettings&) = delete;
-};
-
-// Allows setting the values of MediaSource settings via a name and an int
-// value.
-// This class is thread safe.
-class MediaSourceSettingsImpl : public MediaSourceSettings {
- public:
-  base::Optional<int> GetSourceBufferEvictExtraInBytes() const override {
-    base::AutoLock auto_lock(lock_);
-    return source_buffer_evict_extra_in_bytes_;
-  }
-  base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
-      const override {
-    base::AutoLock auto_lock(lock_);
-    return minimum_processor_count_to_offload_algorithm_;
-  }
-  base::Optional<bool> IsAsynchronousReductionEnabled() const override {
-    base::AutoLock auto_lock(lock_);
-    return is_asynchronous_reduction_enabled_;
-  }
-  base::Optional<int> GetMaxSizeForImmediateJob() const override {
-    base::AutoLock auto_lock(lock_);
-    return max_size_for_immediate_job_;
-  }
-  base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const override {
-    base::AutoLock auto_lock(lock_);
-    return max_source_buffer_append_size_in_bytes_;
-  }
-
-  // Returns true when the setting associated with `name` is set to `value`.
-  // Returns false when `name` is not associated with any settings, or if
-  // `value` contains an invalid value.
-  bool Set(const std::string& name, int value);
-
- private:
-  mutable base::Lock lock_;
-  base::Optional<int> source_buffer_evict_extra_in_bytes_;
-  base::Optional<int> minimum_processor_count_to_offload_algorithm_;
-  base::Optional<bool> is_asynchronous_reduction_enabled_;
-  base::Optional<int> max_size_for_immediate_job_;
-  base::Optional<int> max_source_buffer_append_size_in_bytes_;
-};
-
-}  // namespace dom
-}  // namespace cobalt
-
-#endif  // COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
diff --git a/cobalt/dom/media_source_settings_test.cc b/cobalt/dom/media_source_settings_test.cc
deleted file mode 100644
index d8a27ed..0000000
--- a/cobalt/dom/media_source_settings_test.cc
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/dom/media_source_settings.h"
-
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace cobalt {
-namespace dom {
-namespace {
-
-TEST(MediaSourceSettingsImplTest, Empty) {
-  MediaSourceSettingsImpl impl;
-
-  EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
-  EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
-  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
-  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
-  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
-}
-
-TEST(MediaSourceSettingsImplTest, SunnyDay) {
-  MediaSourceSettingsImpl impl;
-
-  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 100));
-  ASSERT_TRUE(
-      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 101));
-  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 103));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 100000));
-
-  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 100);
-  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 101);
-  EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 103);
-  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 100000);
-}
-
-TEST(MediaSourceSettingsImplTest, RainyDay) {
-  MediaSourceSettingsImpl impl;
-
-  ASSERT_FALSE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", -100));
-  ASSERT_FALSE(
-      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", -101));
-  ASSERT_FALSE(impl.Set("MediaSource.EnableAsynchronousReduction", 2));
-  ASSERT_FALSE(impl.Set("MediaSource.MaxSizeForImmediateJob", -103));
-  ASSERT_FALSE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 0));
-
-  EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
-  EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
-  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
-  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
-  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
-}
-
-TEST(MediaSourceSettingsImplTest, ZeroValuesWork) {
-  MediaSourceSettingsImpl impl;
-
-  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
-  ASSERT_TRUE(
-      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
-  // O is an invalid value for "MediaSource.MaxSourceBufferAppendSizeInBytes".
-
-  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 0);
-  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 0);
-  EXPECT_FALSE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 0);
-}
-
-TEST(MediaSourceSettingsImplTest, Updatable) {
-  MediaSourceSettingsImpl impl;
-
-  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
-  ASSERT_TRUE(
-      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 1));
-
-  ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 1));
-  ASSERT_TRUE(
-      impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 2));
-
-  EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 1);
-  EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 1);
-  EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 1);
-  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 2);
-}
-
-TEST(MediaSourceSettingsImplTest, InvalidSettingNames) {
-  MediaSourceSettingsImpl impl;
-
-  ASSERT_FALSE(impl.Set("MediaSource.Invalid", 0));
-  ASSERT_FALSE(impl.Set("Invalid.SourceBufferEvictExtraInBytes", 0));
-}
-
-}  // namespace
-}  // namespace dom
-}  // namespace cobalt
diff --git a/cobalt/dom/pointer_state.cc b/cobalt/dom/pointer_state.cc
index 315c890..619fadf 100644
--- a/cobalt/dom/pointer_state.cc
+++ b/cobalt/dom/pointer_state.cc
@@ -297,6 +297,19 @@
   client_time_stamps_.erase(pointer_id);
 }
 
+void PointerState::SetWasCancelled(int32_t pointer_id) {
+  client_cancellations_.insert(pointer_id);
+}
+
+bool PointerState::GetWasCancelled(int32_t pointer_id) {
+  auto client_cancellation = client_cancellations_.find(pointer_id);
+  return client_cancellation != client_cancellations_.end();
+}
+
+void PointerState::ClearWasCancelled(int32_t pointer_id) {
+  client_cancellations_.erase(pointer_id);
+}
+
 // static
 bool PointerState::CanQueueEvent(const scoped_refptr<web::Event>& event) {
   return event->GetWrappableType() == base::GetTypeId<PointerEvent>() ||
diff --git a/cobalt/dom/pointer_state.h b/cobalt/dom/pointer_state.h
index 9448d4f..5552f9d 100644
--- a/cobalt/dom/pointer_state.h
+++ b/cobalt/dom/pointer_state.h
@@ -85,6 +85,13 @@
   base::Optional<uint64> GetClientTimeStamp(int32_t pointer_id);
   void ClearTimeStamp(int32_t pointer_id);
 
+  // Tracks whether a certain pointer was cancelled, i.e. if it panned the
+  // page viewport.
+  // https://www.w3.org/TR/pointerevents1/#the-pointercancel-event
+  void SetWasCancelled(int32_t pointer_id);
+  bool GetWasCancelled(int32_t pointer_id);
+  void ClearWasCancelled(int32_t pointer_id);
+
   static bool CanQueueEvent(const scoped_refptr<web::Event>& event);
 
  private:
@@ -106,6 +113,7 @@
 
   std::map<int32_t, math::Vector2dF> client_coordinates_;
   std::map<int32_t, uint64> client_time_stamps_;
+  std::set<int32_t> client_cancellations_;
 };
 
 }  // namespace dom
diff --git a/cobalt/dom/source_buffer.cc b/cobalt/dom/source_buffer.cc
index 449eaa1..267677b 100644
--- a/cobalt/dom/source_buffer.cc
+++ b/cobalt/dom/source_buffer.cc
@@ -56,8 +56,11 @@
 #include "cobalt/base/polymorphic_downcast.h"
 #include "cobalt/base/tokens.h"
 #include "cobalt/dom/dom_settings.h"
+#include "cobalt/dom/media_settings.h"
 #include "cobalt/dom/media_source.h"
+#include "cobalt/web/context.h"
 #include "cobalt/web/dom_exception.h"
+#include "cobalt/web/web_settings.h"
 #include "third_party/chromium/media/base/ranges.h"
 #include "third_party/chromium/media/base/timestamp_constants.h"
 
@@ -87,35 +90,44 @@
   return base::TimeDelta::FromSecondsD(time);
 }
 
+const MediaSettings& GetMediaSettings(web::EnvironmentSettings* settings) {
+  DCHECK(settings);
+  DCHECK(settings->context());
+  DCHECK(settings->context()->web_settings());
+
+  const auto& web_settings = settings->context()->web_settings();
+  return web_settings->media_settings();
+}
+
 // The return value will be used in `SourceBuffer::EvictCodedFrames()` to allow
 // it to evict extra data from the SourceBuffer, so it can reduce the overall
 // memory used by the underlying Demuxer implementation.
 // The default value is 0, i.e. do not evict extra bytes.
-size_t GetEvictExtraInBytes(script::EnvironmentSettings* settings) {
-  DOMSettings* dom_settings =
-      base::polymorphic_downcast<DOMSettings*>(settings);
-  DCHECK(dom_settings);
-  DCHECK(dom_settings->media_source_settings());
-  int bytes = dom_settings->media_source_settings()
-                  ->GetSourceBufferEvictExtraInBytes()
-                  .value_or(0);
+size_t GetEvictExtraInBytes(web::EnvironmentSettings* settings) {
+  const MediaSettings& media_settings = GetMediaSettings(settings);
+
+  int bytes = media_settings.GetSourceBufferEvictExtraInBytes().value_or(0);
   DCHECK_GE(bytes, 0);
+
   return std::max<int>(bytes, 0);
 }
 
-size_t GetMaxAppendSizeInBytes(script::EnvironmentSettings* settings,
+size_t GetMaxAppendSizeInBytes(web::EnvironmentSettings* settings,
                                size_t default_value) {
-  DOMSettings* dom_settings =
-      base::polymorphic_downcast<DOMSettings*>(settings);
-  DCHECK(dom_settings);
-  DCHECK(dom_settings->media_source_settings());
-  int bytes = dom_settings->media_source_settings()
-                  ->GetMaxSourceBufferAppendSizeInBytes()
-                  .value_or(default_value);
+  const MediaSettings& media_settings = GetMediaSettings(settings);
+
+  int bytes = media_settings.GetMaxSourceBufferAppendSizeInBytes().value_or(
+      default_value);
   DCHECK_GT(bytes, 0);
+
   return bytes;
 }
 
+bool IsAvoidCopyingArrayBufferEnabled(web::EnvironmentSettings* settings) {
+  const MediaSettings& media_settings = GetMediaSettings(settings);
+  return media_settings.IsAvoidCopyingArrayBufferEnabled().value_or(false);
+}
+
 }  // namespace
 
 SourceBuffer::OnInitSegmentReceivedHelper::OnInitSegmentReceivedHelper(
@@ -150,9 +162,9 @@
     : web::EventTarget(settings),
       on_init_segment_received_helper_(new OnInitSegmentReceivedHelper(this)),
       id_(id),
-      evict_extra_in_bytes_(GetEvictExtraInBytes(settings)),
-      max_append_buffer_size_(
-          GetMaxAppendSizeInBytes(settings, kDefaultMaxAppendBufferSize)),
+      evict_extra_in_bytes_(GetEvictExtraInBytes(environment_settings())),
+      max_append_buffer_size_(GetMaxAppendSizeInBytes(
+          environment_settings(), kDefaultMaxAppendBufferSize)),
       media_source_(media_source),
       chunk_demuxer_(chunk_demuxer),
       event_queue_(event_queue),
@@ -307,19 +319,38 @@
   append_window_end_ = end;
 }
 
-void SourceBuffer::AppendBuffer(const script::Handle<script::ArrayBuffer>& data,
+void SourceBuffer::AppendBuffer(const script::Handle<ArrayBuffer>& data,
                                 script::ExceptionState* exception_state) {
   TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
                data->ByteLength());
+
+  is_avoid_copying_array_buffer_enabled_ =
+      IsAvoidCopyingArrayBufferEnabled(environment_settings());
+
+  DCHECK(array_buffer_in_use_.IsEmpty());
+  DCHECK(array_buffer_view_in_use_.IsEmpty());
+  if (is_avoid_copying_array_buffer_enabled_) {
+    array_buffer_in_use_ = data;
+  }
+
   AppendBufferInternal(static_cast<const unsigned char*>(data->Data()),
                        data->ByteLength(), exception_state);
 }
 
-void SourceBuffer::AppendBuffer(
-    const script::Handle<script::ArrayBufferView>& data,
-    script::ExceptionState* exception_state) {
+void SourceBuffer::AppendBuffer(const script::Handle<ArrayBufferView>& data,
+                                script::ExceptionState* exception_state) {
   TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
                data->ByteLength());
+
+  is_avoid_copying_array_buffer_enabled_ =
+      IsAvoidCopyingArrayBufferEnabled(environment_settings());
+
+  DCHECK(array_buffer_in_use_.IsEmpty());
+  DCHECK(array_buffer_view_in_use_.IsEmpty());
+  if (is_avoid_copying_array_buffer_enabled_) {
+    array_buffer_view_in_use_ = data;
+  }
+
   AppendBufferInternal(static_cast<const unsigned char*>(data->RawData()),
                        data->ByteLength(), exception_state);
 }
@@ -459,6 +490,8 @@
 
   pending_append_data_.reset();
   pending_append_data_capacity_ = 0;
+  array_buffer_in_use_ = script::Handle<ArrayBuffer>();
+  array_buffer_view_in_use_ = script::Handle<ArrayBufferView>();
 }
 
 double SourceBuffer::GetHighestPresentationTimestamp() const {
@@ -543,19 +576,34 @@
   DCHECK(data || size == 0);
 
   if (data) {
-    if (pending_append_data_capacity_ < size) {
-      pending_append_data_.reset();
-      pending_append_data_.reset(new uint8_t[size]);
-      pending_append_data_capacity_ = size;
+    if (is_avoid_copying_array_buffer_enabled_) {
+      // When |is_avoid_copying_array_buffer_enabled_| is true, we are holding
+      // reference to the underlying JS buffer object, and don't have to make a
+      // copy of the data.
+      if (array_buffer_view_in_use_.IsEmpty()) {
+        DCHECK(!array_buffer_in_use_.IsEmpty());
+        DCHECK_EQ(array_buffer_in_use_->Data(), data);
+        DCHECK_EQ(array_buffer_in_use_->ByteLength(), size);
+      } else {
+        DCHECK_EQ(array_buffer_view_in_use_->RawData(), data);
+        DCHECK_EQ(array_buffer_view_in_use_->ByteLength(), size);
+      }
+    } else {
+      if (pending_append_data_capacity_ < size) {
+        pending_append_data_.reset();
+        pending_append_data_.reset(new uint8_t[size]);
+        pending_append_data_capacity_ = size;
+      }
+      memcpy(pending_append_data_.get(), data, size);
+      data = pending_append_data_.get();
     }
-    memcpy(pending_append_data_.get(), data, size);
   }
 
   ScheduleEvent(base::Tokens::updatestart());
 
   std::unique_ptr<SourceBufferAlgorithm> algorithm(
       new SourceBufferAppendAlgorithm(
-          media_source_, chunk_demuxer_, id_, pending_append_data_.get(), size,
+          media_source_, chunk_demuxer_, id_, data, size,
           max_append_buffer_size_, DoubleToTimeDelta(append_window_start_),
           DoubleToTimeDelta(append_window_end_),
           DoubleToTimeDelta(timestamp_offset_),
@@ -574,6 +622,16 @@
 void SourceBuffer::OnAlgorithmFinalized() {
   DCHECK(active_algorithm_handle_);
   active_algorithm_handle_ = nullptr;
+
+  if (is_avoid_copying_array_buffer_enabled_) {
+    // Allow them to be GCed.
+    array_buffer_in_use_ = script::Handle<ArrayBuffer>();
+    array_buffer_view_in_use_ = script::Handle<ArrayBufferView>();
+    is_avoid_copying_array_buffer_enabled_ = false;
+  } else {
+    DCHECK(array_buffer_in_use_.IsEmpty());
+    DCHECK(array_buffer_view_in_use_.IsEmpty());
+  }
 }
 
 void SourceBuffer::UpdateTimestampOffset(base::TimeDelta timestamp_offset) {
diff --git a/cobalt/dom/source_buffer.h b/cobalt/dom/source_buffer.h
index 25b15ac..c611177 100644
--- a/cobalt/dom/source_buffer.h
+++ b/cobalt/dom/source_buffer.h
@@ -142,6 +142,8 @@
 
  private:
   typedef ::media::MediaTracks MediaTracks;
+  typedef script::ArrayBuffer ArrayBuffer;
+  typedef script::ArrayBufferView ArrayBufferView;
 
   // SourceBuffer is inherited from base::RefCounted<> and its ref count cannot
   // be used on non-web threads.  On the other hand the call to
@@ -210,6 +212,9 @@
   double append_window_start_ = 0;
   double append_window_end_ = std::numeric_limits<double>::infinity();
 
+  bool is_avoid_copying_array_buffer_enabled_ = false;
+  script::Handle<ArrayBuffer> array_buffer_in_use_;
+  script::Handle<ArrayBufferView> array_buffer_view_in_use_;
   std::unique_ptr<uint8_t[]> pending_append_data_;
   size_t pending_append_data_capacity_ = 0;
 
diff --git a/cobalt/dom/testing/stub_environment_settings.h b/cobalt/dom/testing/stub_environment_settings.h
index a70eef3..01d01f3 100644
--- a/cobalt/dom/testing/stub_environment_settings.h
+++ b/cobalt/dom/testing/stub_environment_settings.h
@@ -26,7 +26,7 @@
  public:
   explicit StubEnvironmentSettings(const Options& options = Options())
       : DOMSettings(null_debugger_hooks_, 0, nullptr, nullptr, nullptr, nullptr,
-                    nullptr, options) {}
+                    options) {}
   ~StubEnvironmentSettings() override {}
 
  private:
diff --git a/cobalt/dom_parser/libxml_html_parser_wrapper.cc b/cobalt/dom_parser/libxml_html_parser_wrapper.cc
index e512c71..0866ec5 100644
--- a/cobalt/dom_parser/libxml_html_parser_wrapper.cc
+++ b/cobalt/dom_parser/libxml_html_parser_wrapper.cc
@@ -164,6 +164,10 @@
     if (IsFullDocument()) {
       document()->DecreaseLoadingCounterAndMaybeDispatchLoadEvent();
     }
+
+    // Release parser memory once done
+    htmlFreeParserCtxt(html_parser_context_);
+    html_parser_context_ = NULL;
   }
 }
 
diff --git a/cobalt/dom_parser/xml_decoder_test.cc b/cobalt/dom_parser/xml_decoder_test.cc
index bb021ea..7f3cb42 100644
--- a/cobalt/dom_parser/xml_decoder_test.cc
+++ b/cobalt/dom_parser/xml_decoder_test.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/dom_parser/xml_decoder.h"
 
+#include <memory>
+
 #include "base/callback.h"
 #include "base/optional.h"
 #include "cobalt/dom/attr.h"
@@ -244,7 +244,8 @@
   EXPECT_EQ("element", element->local_name());
 }
 
-TEST_F(XMLDecoderTest, CanDealWithLaughsAttack) {
+// This test is disabled due to memory leak in libxml b/265300062
+TEST_F(XMLDecoderTest, DISABLED_CanDealWithLaughsAttack) {
   const std::string input =
       "<!DOCTYPE doc ["
       "<!ENTITY ha \"Ha !\">"
diff --git a/cobalt/evergreen_tests/evergreen_tests.py b/cobalt/evergreen_tests/evergreen_tests.py
index 00d23ae..75f9f77 100644
--- a/cobalt/evergreen_tests/evergreen_tests.py
+++ b/cobalt/evergreen_tests/evergreen_tests.py
@@ -27,8 +27,6 @@
 from starboard.tools import log_level
 from starboard.tools.paths import REPOSITORY_ROOT
 
-_DEFAULT_PLATFORM_UNDER_TEST = 'linux'
-
 
 def _Exec(cmd, env=None):
   """Executes a command in a subprocess and returns the result."""
@@ -100,7 +98,7 @@
   if use_compressed_system_image:
     command.append('-c')
 
-  command.append(args.platform_under_test)
+  command.append(args.loader_platform)
 
   return _Exec(command, env)
 
@@ -112,10 +110,6 @@
       dest='can_mount_tmpfs',
       action='store_false',
       help='A temporary filesystem cannot be mounted on the target device.')
-  arg_parser.add_argument(
-      '--platform_under_test',
-      default=_DEFAULT_PLATFORM_UNDER_TEST,
-      help='The platform to run the tests on (e.g., linux or raspi).')
   authentication_method = arg_parser.add_mutually_exclusive_group()
   authentication_method.add_argument(
       '--public-key-auth',
diff --git a/cobalt/extension/configuration.h b/cobalt/extension/configuration.h
index 6d715a0..36f5ae4 100644
--- a/cobalt/extension/configuration.h
+++ b/cobalt/extension/configuration.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,213 +15,10 @@
 #ifndef COBALT_EXTENSION_CONFIGURATION_H_
 #define COBALT_EXTENSION_CONFIGURATION_H_
 
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionConfigurationName "dev.cobalt.extension.Configuration"
-
-typedef struct CobaltExtensionConfigurationApi {
-  // Name should be the string |kCobaltExtensionConfigurationName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // The functions below configure Cobalt. All correspond to some GYP variable,
-  // but the implementation of this functions will take precedence over the GYP
-  // variable.
-
-  // This variable defines what Cobalt's preferred strategy should be for
-  // handling internally triggered application exit requests (e.g. the user
-  // chooses to back out of the application).
-  //   'stop'    -- The application should call SbSystemRequestStop() on exit,
-  //                resulting in a complete shutdown of the application.
-  //   'suspend' -- The application should call SbSystemRequestSuspend() on
-  //                exit, resulting in the application being "minimized".
-  //   'noexit'  -- The application should never allow the user to trigger an
-  //                exit, this will be managed by the system.
-  const char* (*CobaltUserOnExitStrategy)();
-
-  // If set to |true|, will enable support for rendering only the regions of
-  // the display that are modified due to animations, instead of re-rendering
-  // the entire scene each frame.  This feature can reduce startup time where
-  // usually there is a small loading spinner animating on the screen.  On GLES
-  // renderers, Cobalt will attempt to implement this support by using
-  // eglSurfaceAttrib(..., EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED), otherwise
-  // the dirty region will be silently disabled. Note that some GLES driver
-  // implementations may internally allocate an extra full screen surface to
-  // support this feature, and many have been noticed to not properly support
-  // this functionality (but they report that they do), and for these reasons
-  // this value is defaulted to |false|.
-  bool (*CobaltRenderDirtyRegionOnly)();
-
-  // Cobalt will call eglSwapInterval() and specify this value before calling
-  // eglSwapBuffers() each frame.
-  int (*CobaltEglSwapInterval)();
-
-  // The URL of default build time splash screen - see
-  // cobalt/doc/splash_screen.md for information about this.
-  const char* (*CobaltFallbackSplashScreenUrl)();
-
-  // If set to |true|, enables Quic.
-  bool (*CobaltEnableQuic)();
-
-  // Cache parameters
-
-  // The following set of parameters define how much memory is reserved for
-  // different Cobalt caches.  These caches affect CPU *and* GPU memory usage.
-  //
-  // The sum of the following caches effectively describes the maximum GPU
-  // texture memory usage (though it doesn't consider video textures and
-  // display color buffers):
-  //   - CobaltSkiaCacheSizeInBytes (GLES2 rasterizer only)
-  //   - CobaltImageCacheSizeInBytes
-  //   - CobaltSkiaGlyphAtlasWidth * CobaltSkiaGlyphAtlasHeight
-  //
-  // The other caches affect CPU memory usage.
-
-  // Determines the capacity of the skia cache.  The Skia cache is maintained
-  // within Skia and is used to cache the results of complicated effects such
-  // as shadows, so that Skia draw calls that are used repeatedly across
-  // frames can be cached into surfaces.  This setting is only relevant when
-  // using the hardware-accelerated Skia rasterizer.
-  int (*CobaltSkiaCacheSizeInBytes)();
-
-  // Determines the amount of GPU memory the offscreen target atlases will
-  // use. This is specific to the direct-GLES rasterizer and caches any render
-  // tree nodes which require skia for rendering. Two atlases will be allocated
-  // from this memory or multiple atlases of the frame size if the limit
-  // allows. It is recommended that enough memory be reserved for two RGBA
-  // atlases about a quarter of the frame size.
-  int (*CobaltOffscreenTargetCacheSizeInBytes)();
-
-  // Determines the capacity of the encoded image cache, which manages encoded
-  // images downloaded from a web page. These images are cached within CPU
-  // memory.  This not only reduces network traffic to download the encoded
-  // images, but also allows the downloaded images to be held during suspend.
-  // Note that there is also a cache for the decoded images whose capacity is
-  // specified in |CobaltImageCacheSizeInBytes|.  The decoded images are often
-  // cached in the GPU memory and will be released during suspend.
-  //
-  // If a system meets the following requirements:
-  // 1. Has a fast image decoder.
-  // 2. Has enough CPU memory, or has a unified memory architecture that allows
-  //    sharing of CPU and GPU memory.
-  // Then it may consider implementing |CobaltEncodedImageCacheSizeInBytes| to
-  // return a much bigger value, and set the return value of
-  // |CobaltImageCacheSizeInBytes| to a much smaller value. This allows the app
-  // to cache significantly more images.
-  //
-  // Setting this to 0 can disable the cache completely.
-  int (*CobaltEncodedImageCacheSizeInBytes)();
-
-  // Determines the capacity of the image cache, which manages image surfaces
-  // downloaded from a web page.  While it depends on the platform, often (and
-  // ideally) these images are cached within GPU memory.
-  // Set to -1 to automatically calculate the value at runtime, based on
-  // features like windows dimensions and the value of
-  // SbSystemGetTotalGPUMemory().
-  int (*CobaltImageCacheSizeInBytes)();
-
-  // Determines the capacity of the local font cache, which manages all fonts
-  // loaded from local files. Newly encountered sections of font files are
-  // lazily loaded into the cache, enabling subsequent requests to the same
-  // file sections to be handled via direct memory access. Once the limit is
-  // reached, further requests are handled via file stream.
-  // Setting the value to 0 disables memory caching and causes all font file
-  // accesses to be done using file streams.
-  int (*CobaltLocalTypefaceCacheSizeInBytes)();
-
-  // Determines the capacity of the remote font cache, which manages all
-  // fonts downloaded from a web page.
-  int (*CobaltRemoteTypefaceCacheSizeInBytes)();
-
-  // Determines the capacity of the mesh cache. Each mesh is held compressed
-  // in main memory, to be inflated into a GPU buffer when needed for
-  // projection.
-  int (*CobaltMeshCacheSizeInBytes)();
-
-  // Deprecated
-  int (*CobaltSoftwareSurfaceCacheSizeInBytes)();
-
-  // Modifying this function's return value to be non-1.0f will result in the
-  // image cache capacity being cleared and then temporarily reduced for the
-  // duration that a video is playing.  This can be useful for some platforms
-  // if they are particularly constrained for (GPU) memory during video
-  // playback.  When playing a video, the image cache is reduced to:
-  // CobaltImageCacheSizeInBytes() *
-  //     CobaltImageCacheCapacityMultiplierWhenPlayingVideo().
-  float (*CobaltImageCacheCapacityMultiplierWhenPlayingVideo)();
-
-  // Determines the size in pixels of the glyph atlas where rendered glyphs are
-  // cached. The resulting memory usage is 2 bytes of GPU memory per pixel.
-  // When a value is used that is too small, thrashing may occur that will
-  // result in visible stutter. Such thrashing is more likely to occur when CJK
-  // language glyphs are rendered and when the size of the glyphs in pixels is
-  // larger, such as for higher resolution displays.
-  // The negative default values indicates to the engine that these settings
-  // should be automatically set.
-  int (*CobaltSkiaGlyphAtlasWidth)();
-  int (*CobaltSkiaGlyphAtlasHeight)();
-
-  // This configuration has been deprecated and is only kept for
-  // backward-compatibility. It has no effect on V8.
-  int (*CobaltJsGarbageCollectionThresholdInBytes)();
-
-  // When specified this value will reduce the cpu memory consumption by
-  // the specified amount. -1 disables the value.
-  int (*CobaltReduceCpuMemoryBy)();
-
-  // When specified this value will reduce the gpu memory consumption by
-  // the specified amount. -1 disables the value.
-  int (*CobaltReduceGpuMemoryBy)();
-
-  // Can be set to enable zealous garbage collection. Zealous garbage
-  // collection will cause garbage collection to occur much more frequently
-  // than normal, for the purpose of finding or reproducing bugs.
-  bool (*CobaltGcZeal)();
-
-  // Defines what kind of rasterizer will be used.  This can be adjusted to
-  // force a stub graphics implementation.
-  // It can be one of the following options:
-  //   'direct-gles' -- Uses a light wrapper over OpenGL ES to handle most
-  //                    draw elements. This will fall back to the skia hardware
-  //                    rasterizer for some render tree node types, but is
-  //                    generally faster on the CPU and GPU. This can handle
-  //                    360 rendering.
-  //   'hardware'    -- As much hardware acceleration of graphics commands as
-  //                    possible. This uses skia to wrap OpenGL ES commands.
-  //                    Required for 360 rendering.
-  //   'stub'        -- Stub graphics rasterization.  A rasterizer object will
-  //                    still be available and valid, but it will do nothing.
-  const char* (*CobaltRasterizerType)();
-
-  // Controls whether or not just in time code should be used.
-  // See "cobalt/doc/performance_tuning.md" for more information on when this
-  // should be used.
-  bool (*CobaltEnableJit)();
-
-  // The fields below this point were added in version 2 or later.
-
-  // A mapping of splash screen topics to fallback URLs.
-  const char* (*CobaltFallbackSplashScreenTopics)();
-
-  // The fields below this point were added in version 3 or later.
-
-  // Determines whether compiled Javascript caching code is enabled.
-  bool (*CobaltCanStoreCompiledJavascript)();
-} CobaltExtensionConfigurationApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/configuration.h"
+#else
+#error "Extensions have moved, please see CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_CONFIGURATION_H_
diff --git a/cobalt/extension/crash_handler.h b/cobalt/extension/crash_handler.h
index 912caba..628c27e 100644
--- a/cobalt/extension/crash_handler.h
+++ b/cobalt/extension/crash_handler.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,40 +15,10 @@
 #ifndef COBALT_EXTENSION_CRASH_HANDLER_H_
 #define COBALT_EXTENSION_CRASH_HANDLER_H_
 
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-#include "third_party/crashpad/wrapper/annotations.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionCrashHandlerName "dev.cobalt.extension.CrashHandler"
-
-typedef struct CobaltExtensionCrashHandlerApi {
-  // Name should be the string |kCobaltExtensionCrashHandlerName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Deprecated in version 2 and later.
-  bool (*OverrideCrashpadAnnotations)(
-      CrashpadAnnotations* crashpad_annotations);
-
-  // The fields below this point were added in version 2 or later.
-
-  // Sets a (key, value) pair for the handler to include when annotating a
-  // crash. Returns true on success and false on failure.
-  bool (*SetString)(const char* key, const char* value);
-} CobaltExtensionCrashHandlerApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/crash_handler.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_CRASH_HANDLER_H_
diff --git a/cobalt/extension/cwrappers.h b/cobalt/extension/cwrappers.h
deleted file mode 100644
index 604110c..0000000
--- a/cobalt/extension/cwrappers.h
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_EXTENSION_CWRAPPERS_H_
-#define COBALT_EXTENSION_CWRAPPERS_H_
-
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionCWrappersName "dev.cobalt.extension.CWrappers"
-
-typedef struct CobaltExtensionCWrappersApi {
-  // Name should be the string kCobaltExtensionCWrapperName.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  double (*PowWrapper)(double base, double exponent);
-} CobaltExtensionCWrappersApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-#endif  // COBALT_EXTENSION_CWRAPPERS_H_
diff --git a/cobalt/extension/demuxer.h b/cobalt/extension/demuxer.h
index 59a3055..96e210b 100644
--- a/cobalt/extension/demuxer.h
+++ b/cobalt/extension/demuxer.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -11,398 +11,14 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-//
-// Contains extension code allowing partners to provide their own demuxer.
-// CobaltExtensionDemuxerApi is the main API.
 
 #ifndef COBALT_EXTENSION_DEMUXER_H_
 #define COBALT_EXTENSION_DEMUXER_H_
 
-#include <stdbool.h>
-#include <stdint.h>
-#include <string.h>
-
-#include "starboard/time.h"
-
-#ifdef __cplusplus
-extern "C" {
+#if SB_API_VERSION <= 14
+#include "starboard/extension/demuxer.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
-#define kCobaltExtensionDemuxerApi "dev.cobalt.extension.Demuxer"
-
-// This must stay in sync with ::media::PipelineStatus. Missing values are
-// either irrelevant to the demuxer or are deprecated values of PipelineStatus.
-typedef enum CobaltExtensionDemuxerStatus {
-  kCobaltExtensionDemuxerOk = 0,
-  kCobaltExtensionDemuxerErrorNetwork = 2,
-  kCobaltExtensionDemuxerErrorAbort = 5,
-  kCobaltExtensionDemuxerErrorInitializationFailed = 6,
-  kCobaltExtensionDemuxerErrorRead = 9,
-  kCobaltExtensionDemuxerErrorInvalidState = 11,
-  kCobaltExtensionDemuxerErrorCouldNotOpen = 12,
-  kCobaltExtensionDemuxerErrorCouldNotParse = 13,
-  kCobaltExtensionDemuxerErrorNoSupportedStreams = 14
-} CobaltExtensionDemuxerStatus;
-
-// Type of side data associated with a buffer.
-typedef enum CobaltExtensionDemuxerSideDataType {
-  kCobaltExtensionDemuxerUnknownSideDataType = 0,
-  kCobaltExtensionDemuxerMatroskaBlockAdditional = 1,
-} CobaltExtensionDemuxerSideDataType;
-
-// This must stay in sync with ::media::AudioCodec.
-typedef enum CobaltExtensionDemuxerAudioCodec {
-  kCobaltExtensionDemuxerCodecUnknownAudio = 0,
-  kCobaltExtensionDemuxerCodecAAC = 1,
-  kCobaltExtensionDemuxerCodecMP3 = 2,
-  kCobaltExtensionDemuxerCodecPCM = 3,
-  kCobaltExtensionDemuxerCodecVorbis = 4,
-  kCobaltExtensionDemuxerCodecFLAC = 5,
-  kCobaltExtensionDemuxerCodecAMR_NB = 6,
-  kCobaltExtensionDemuxerCodecAMR_WB = 7,
-  kCobaltExtensionDemuxerCodecPCM_MULAW = 8,
-  kCobaltExtensionDemuxerCodecGSM_MS = 9,
-  kCobaltExtensionDemuxerCodecPCM_S16BE = 10,
-  kCobaltExtensionDemuxerCodecPCM_S24BE = 11,
-  kCobaltExtensionDemuxerCodecOpus = 12,
-  kCobaltExtensionDemuxerCodecEAC3 = 13,
-  kCobaltExtensionDemuxerCodecPCM_ALAW = 14,
-  kCobaltExtensionDemuxerCodecALAC = 15,
-  kCobaltExtensionDemuxerCodecAC3 = 16
-} CobaltExtensionDemuxerAudioCodec;
-
-// This must stay in sync with ::media::VideoCodec.
-typedef enum CobaltExtensionDemuxerVideoCodec {
-  kCobaltExtensionDemuxerCodecUnknownVideo = 0,
-  kCobaltExtensionDemuxerCodecH264,
-  kCobaltExtensionDemuxerCodecVC1,
-  kCobaltExtensionDemuxerCodecMPEG2,
-  kCobaltExtensionDemuxerCodecMPEG4,
-  kCobaltExtensionDemuxerCodecTheora,
-  kCobaltExtensionDemuxerCodecVP8,
-  kCobaltExtensionDemuxerCodecVP9,
-  kCobaltExtensionDemuxerCodecHEVC,
-  kCobaltExtensionDemuxerCodecDolbyVision,
-  kCobaltExtensionDemuxerCodecAV1,
-} CobaltExtensionDemuxerVideoCodec;
-
-// This must stay in sync with ::media::SampleFormat.
-typedef enum CobaltExtensionDemuxerSampleFormat {
-  kCobaltExtensionDemuxerSampleFormatUnknown = 0,
-  kCobaltExtensionDemuxerSampleFormatU8,   // Unsigned 8-bit w/ bias of 128.
-  kCobaltExtensionDemuxerSampleFormatS16,  // Signed 16-bit.
-  kCobaltExtensionDemuxerSampleFormatS32,  // Signed 32-bit.
-  kCobaltExtensionDemuxerSampleFormatF32,  // Float 32-bit.
-  kCobaltExtensionDemuxerSampleFormatPlanarS16,  // Signed 16-bit planar.
-  kCobaltExtensionDemuxerSampleFormatPlanarF32,  // Float 32-bit planar.
-  kCobaltExtensionDemuxerSampleFormatPlanarS32,  // Signed 32-bit planar.
-  kCobaltExtensionDemuxerSampleFormatS24,        // Signed 24-bit.
-} CobaltExtensionDemuxerSampleFormat;
-
-// This must stay in sync with ::media::ChannelLayout.
-typedef enum CobaltExtensionDemuxerChannelLayout {
-  kCobaltExtensionDemuxerChannelLayoutNone = 0,
-  kCobaltExtensionDemuxerChannelLayoutUnsupported = 1,
-  kCobaltExtensionDemuxerChannelLayoutMono = 2,
-  kCobaltExtensionDemuxerChannelLayoutStereo = 3,
-  kCobaltExtensionDemuxerChannelLayout2_1 = 4,
-  kCobaltExtensionDemuxerChannelLayoutSurround = 5,
-  kCobaltExtensionDemuxerChannelLayout4_0 = 6,
-  kCobaltExtensionDemuxerChannelLayout2_2 = 7,
-  kCobaltExtensionDemuxerChannelLayoutQuad = 8,
-  kCobaltExtensionDemuxerChannelLayout5_0 = 9,
-  kCobaltExtensionDemuxerChannelLayout5_1 = 10,
-  kCobaltExtensionDemuxerChannelLayout5_0Back = 11,
-  kCobaltExtensionDemuxerChannelLayout5_1Back = 12,
-  kCobaltExtensionDemuxerChannelLayout7_0 = 13,
-  kCobaltExtensionDemuxerChannelLayout7_1 = 14,
-  kCobaltExtensionDemuxerChannelLayout7_1Wide = 15,
-  kCobaltExtensionDemuxerChannelLayoutStereoDownmix = 16,
-  kCobaltExtensionDemuxerChannelLayout2point1 = 17,
-  kCobaltExtensionDemuxerChannelLayout3_1 = 18,
-  kCobaltExtensionDemuxerChannelLayout4_1 = 19,
-  kCobaltExtensionDemuxerChannelLayout6_0 = 20,
-  kCobaltExtensionDemuxerChannelLayout6_0Front = 21,
-  kCobaltExtensionDemuxerChannelLayoutHexagonal = 22,
-  kCobaltExtensionDemuxerChannelLayout6_1 = 23,
-  kCobaltExtensionDemuxerChannelLayout6_1Back = 24,
-  kCobaltExtensionDemuxerChannelLayout6_1Front = 25,
-  kCobaltExtensionDemuxerChannelLayout7_0Front = 26,
-  kCobaltExtensionDemuxerChannelLayout7_1WideBack = 27,
-  kCobaltExtensionDemuxerChannelLayoutOctagonal = 28,
-  kCobaltExtensionDemuxerChannelLayoutDiscrete = 29,
-  kCobaltExtensionDemuxerChannelLayoutStereoAndKeyboardMic = 30,
-  kCobaltExtensionDemuxerChannelLayout4_1QuadSide = 31,
-  kCobaltExtensionDemuxerChannelLayoutBitstream = 32
-} CobaltExtensionDemuxerChannelLayout;
-
-// This must stay in sync with ::media::VideoCodecProfile.
-typedef enum CobaltExtensionDemuxerVideoCodecProfile {
-  kCobaltExtensionDemuxerVideoCodecProfileUnknown = -1,
-  kCobaltExtensionDemuxerH264ProfileMin = 0,
-  kCobaltExtensionDemuxerH264ProfileBaseline =
-      kCobaltExtensionDemuxerH264ProfileMin,
-  kCobaltExtensionDemuxerH264ProfileMain = 1,
-  kCobaltExtensionDemuxerH264ProfileExtended = 2,
-  kCobaltExtensionDemuxerH264ProfileHigh = 3,
-  kCobaltExtensionDemuxerH264ProfileHigh10Profile = 4,
-  kCobaltExtensionDemuxerH264ProfileHigh422Profile = 5,
-  kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile = 6,
-  kCobaltExtensionDemuxerH264ProfileScalableBaseline = 7,
-  kCobaltExtensionDemuxerH264ProfileScalableHigh = 8,
-  kCobaltExtensionDemuxerH264ProfileStereoHigh = 9,
-  kCobaltExtensionDemuxerH264ProfileMultiviewHigh = 10,
-  kCobaltExtensionDemuxerH264ProfileMax =
-      kCobaltExtensionDemuxerH264ProfileMultiviewHigh,
-  kCobaltExtensionDemuxerVp8ProfileMin = 11,
-  kCobaltExtensionDemuxerVp8ProfileAny = kCobaltExtensionDemuxerVp8ProfileMin,
-  kCobaltExtensionDemuxerVp8ProfileMax = kCobaltExtensionDemuxerVp8ProfileAny,
-  kCobaltExtensionDemuxerVp9ProfileMin = 12,
-  kCobaltExtensionDemuxerVp9ProfileProfile0 =
-      kCobaltExtensionDemuxerVp9ProfileMin,
-  kCobaltExtensionDemuxerVp9ProfileProfile1 = 13,
-  kCobaltExtensionDemuxerVp9ProfileProfile2 = 14,
-  kCobaltExtensionDemuxerVp9ProfileProfile3 = 15,
-  kCobaltExtensionDemuxerVp9ProfileMax =
-      kCobaltExtensionDemuxerVp9ProfileProfile3,
-  kCobaltExtensionDemuxerHevcProfileMin = 16,
-  kCobaltExtensionDemuxerHevcProfileMain =
-      kCobaltExtensionDemuxerHevcProfileMin,
-  kCobaltExtensionDemuxerHevcProfileMain10 = 17,
-  kCobaltExtensionDemuxerHevcProfileMainStillPicture = 18,
-  kCobaltExtensionDemuxerHevcProfileMax =
-      kCobaltExtensionDemuxerHevcProfileMainStillPicture,
-  kCobaltExtensionDemuxerDolbyVisionProfile0 = 19,
-  kCobaltExtensionDemuxerDolbyVisionProfile4 = 20,
-  kCobaltExtensionDemuxerDolbyVisionProfile5 = 21,
-  kCobaltExtensionDemuxerDolbyVisionProfile7 = 22,
-  kCobaltExtensionDemuxerTheoraProfileMin = 23,
-  kCobaltExtensionDemuxerTheoraProfileAny =
-      kCobaltExtensionDemuxerTheoraProfileMin,
-  kCobaltExtensionDemuxerTheoraProfileMax =
-      kCobaltExtensionDemuxerTheoraProfileAny,
-  kCobaltExtensionDemuxerAv1ProfileMin = 24,
-  kCobaltExtensionDemuxerAv1ProfileProfileMain =
-      kCobaltExtensionDemuxerAv1ProfileMin,
-  kCobaltExtensionDemuxerAv1ProfileProfileHigh = 25,
-  kCobaltExtensionDemuxerAv1ProfileProfilePro = 26,
-  kCobaltExtensionDemuxerAv1ProfileMax =
-      kCobaltExtensionDemuxerAv1ProfileProfilePro,
-  kCobaltExtensionDemuxerDolbyVisionProfile8 = 27,
-  kCobaltExtensionDemuxerDolbyVisionProfile9 = 28,
-} CobaltExtensionDemuxerVideoCodecProfile;
-
-// This must be kept in sync with gfx::ColorSpace::RangeID.
-typedef enum CobaltExtensionDemuxerColorSpaceRangeId {
-  kCobaltExtensionDemuxerColorSpaceRangeIdInvalid = 0,
-  kCobaltExtensionDemuxerColorSpaceRangeIdLimited = 1,
-  kCobaltExtensionDemuxerColorSpaceRangeIdFull = 2,
-  kCobaltExtensionDemuxerColorSpaceRangeIdDerived = 3
-} CobaltExtensionDemuxerColorSpaceRangeId;
-
-// This must be kept in sync with media::VideoDecoderConfig::AlphaMode.
-typedef enum CobaltExtensionDemuxerAlphaMode {
-  kCobaltExtensionDemuxerHasAlpha,
-  kCobaltExtensionDemuxerIsOpaque
-} CobaltExtensionDemuxerAlphaMode;
-
-// This must be kept in sync with ::media::DemuxerStream::Type.
-typedef enum CobaltExtensionDemuxerStreamType {
-  kCobaltExtensionDemuxerStreamTypeUnknown,
-  kCobaltExtensionDemuxerStreamTypeAudio,
-  kCobaltExtensionDemuxerStreamTypeVideo,
-  kCobaltExtensionDemuxerStreamTypeText
-} CobaltExtensionDemuxerStreamType;
-
-// This must be kept in sync with media::EncryptionScheme.
-typedef enum CobaltExtensionDemuxerEncryptionScheme {
-  kCobaltExtensionDemuxerEncryptionSchemeUnencrypted,
-  kCobaltExtensionDemuxerEncryptionSchemeCenc,
-  kCobaltExtensionDemuxerEncryptionSchemeCbcs,
-} CobaltExtensionDemuxerEncryptionScheme;
-
-typedef struct CobaltExtensionDemuxerAudioDecoderConfig {
-  CobaltExtensionDemuxerAudioCodec codec;
-  CobaltExtensionDemuxerSampleFormat sample_format;
-  CobaltExtensionDemuxerChannelLayout channel_layout;
-  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
-  int samples_per_second;
-
-  uint8_t* extra_data;  // Not owned by this struct.
-  int64_t extra_data_size;
-} CobaltExtensionDemuxerAudioDecoderConfig;
-
-typedef struct CobaltExtensionDemuxerVideoDecoderConfig {
-  CobaltExtensionDemuxerVideoCodec codec;
-  CobaltExtensionDemuxerVideoCodecProfile profile;
-
-  // These fields represent the color space.
-  int color_space_primaries;
-  int color_space_transfer;
-  int color_space_matrix;
-  CobaltExtensionDemuxerColorSpaceRangeId color_space_range_id;
-
-  CobaltExtensionDemuxerAlphaMode alpha_mode;
-
-  // These fields represent the coded size.
-  int coded_width;
-  int coded_height;
-
-  // These fields represent the visible rectangle.
-  int visible_rect_x;
-  int visible_rect_y;
-  int visible_rect_width;
-  int visible_rect_height;
-
-  // These fields represent the natural size.
-  int natural_width;
-  int natural_height;
-
-  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
-
-  uint8_t* extra_data;  // Not owned by this struct.
-  int64_t extra_data_size;
-} CobaltExtensionDemuxerVideoDecoderConfig;
-
-typedef struct CobaltExtensionDemuxerSideData {
-  uint8_t* data;  // Not owned by this struct.
-  // Number of bytes in |data|.
-  int64_t data_size;
-  // Specifies the format of |data|.
-  CobaltExtensionDemuxerSideDataType type;
-} CobaltExtensionDemuxerSideData;
-
-typedef struct CobaltExtensionDemuxerBuffer {
-  // The media data for this buffer. Ownership is not transferred via this
-  // struct.
-  uint8_t* data;
-  // Number of bytes in |data|.
-  int64_t data_size;
-  // An array of side data elements containing any side data for this buffer.
-  // Ownership is not transferred via this struct.
-  CobaltExtensionDemuxerSideData* side_data;
-  // Number of elements in |side_data|.
-  int64_t side_data_elements;
-  // Playback time in microseconds.
-  SbTime pts;
-  // Duration of this buffer in microseconds.
-  SbTime duration;
-  // True if this buffer contains a keyframe.
-  bool is_keyframe;
-  // Signifies the end of the stream. If this is true, the other fields will be
-  // ignored.
-  bool end_of_stream;
-} CobaltExtensionDemuxerBuffer;
-
-// Note: |buffer| is the input to this function, not the output. Cobalt
-// implements this function to read media data provided by the implementer of
-// CobaltExtensionDemuxer.
-typedef void (*CobaltExtensionDemuxerReadCB)(
-    CobaltExtensionDemuxerBuffer* buffer, void* user_data);
-
-// A fully synchronous demuxer API. Threading concerns are handled by the code
-// that uses this API.
-// When calling the defined functions, the |user_data| argument must be the
-// void* user_data field stored in this struct.
-typedef struct CobaltExtensionDemuxer {
-  // Initialize must only be called once for a demuxer; subsequent calls can
-  // fail.
-  CobaltExtensionDemuxerStatus (*Initialize)(void* user_data);
-
-  CobaltExtensionDemuxerStatus (*Seek)(SbTime seek_time, void* user_data);
-
-  // Returns the starting time for the media file; it is always positive.
-  SbTime (*GetStartTime)(void* user_data);
-
-  // Returns the time -- in microseconds since Windows epoch -- represented by
-  // presentation timestamp 0. If the timestamps are not associated with a time,
-  // returns 0.
-  SbTime (*GetTimelineOffset)(void* user_data);
-
-  // Calls |read_cb| with a buffer of type |type| and the user data provided by
-  // |read_cb_user_data|. |read_cb| is a synchronous function, so the data
-  // passed to it can safely be freed after |read_cb| returns. |read_cb| must be
-  // called exactly once, and it must be called before Read returns.
-  //
-  // An error can be handled in one of two ways:
-  // 1. Pass a null buffer to read_cb. This will cause the pipeline to handle
-  //    the situation as an error. Alternatively,
-  // 2. Pass an "end of stream" buffer to read_cb. This will cause the relevant
-  //    stream to end normally.
-  void (*Read)(CobaltExtensionDemuxerStreamType type,
-               CobaltExtensionDemuxerReadCB read_cb, void* read_cb_user_data,
-               void* user_data);
-
-  // Returns true and populates |audio_config| if an audio stream is present;
-  // returns false otherwise. |config| must not be null.
-  bool (*GetAudioConfig)(CobaltExtensionDemuxerAudioDecoderConfig* config,
-                         void* user_data);
-
-  // Returns true and populates |video_config| if a video stream is present;
-  // returns false otherwise. |config| must not be null.
-  bool (*GetVideoConfig)(CobaltExtensionDemuxerVideoDecoderConfig* config,
-                         void* user_data);
-
-  // Returns the duration, in microseconds.
-  SbTime (*GetDuration)(void* user_data);
-
-  // Will be passed to all functions.
-  void* user_data;
-} CobaltExtensionDemuxer;
-
-typedef struct CobaltExtensionDemuxerDataSource {
-  // Reads up to |bytes_requested|, writing the data into |data| and returning
-  // the number of bytes read. |data| must be able to store at least
-  // |bytes_requested| bytes. Calling BlockingRead advances the read position.
-  int (*BlockingRead)(uint8_t* data, int bytes_requested, void* user_data);
-
-  // Seeks to |position| (specified in bytes) in the data source.
-  void (*SeekTo)(int position, void* user_data);
-
-  // Returns the offset into the data source, in bytes.
-  int64_t (*GetPosition)(void* user_data);
-
-  // Returns the size of the data source, in bytes.
-  int64_t (*GetSize)(void* user_data);
-
-  // Whether this represents a streaming data source.
-  bool is_streaming;
-
-  // Will be passed to all functions.
-  void* user_data;
-} CobaltExtensionDemuxerDataSource;
-
-typedef struct CobaltExtensionDemuxerApi {
-  // Name should be the string |kCobaltExtensionDemuxerApi|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Creates a demuxer for the content provided by |data_source|. Ownership of
-  // |data_source| is not transferred to this function.
-  //
-  // Ownership of the returned demuxer is transferred to the caller, but it must
-  // be deleted via DestroyDemuxer (below). The caller must not manually delete
-  // the demuxer.
-  CobaltExtensionDemuxer* (*CreateDemuxer)(
-      CobaltExtensionDemuxerDataSource* data_source,
-      CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
-      int64_t supported_audio_codecs_size,
-      CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
-      int64_t supported_video_codecs_size);
-
-  // Destroys |demuxer|. After calling this, |demuxer| must not be dereferenced
-  // or deleted by the caller.
-  void (*DestroyDemuxer)(CobaltExtensionDemuxer* demuxer);
-} CobaltExtensionDemuxerApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-
 #endif  // COBALT_EXTENSION_DEMUXER_H_
diff --git a/cobalt/extension/extension_test.cc b/cobalt/extension/extension_test.cc
deleted file mode 100644
index 58f051e..0000000
--- a/cobalt/extension/extension_test.cc
+++ /dev/null
@@ -1,382 +0,0 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cmath>
-
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/crash_handler.h"
-#include "cobalt/extension/cwrappers.h"
-#include "cobalt/extension/font.h"
-#include "cobalt/extension/free_space.h"
-#include "cobalt/extension/graphics.h"
-#include "cobalt/extension/installation_manager.h"
-#include "cobalt/extension/javascript_cache.h"
-#include "cobalt/extension/media_session.h"
-#include "cobalt/extension/memory_mapped_file.h"
-#include "cobalt/extension/platform_service.h"
-#include "cobalt/extension/updater_notification.h"
-#include "cobalt/extension/url_fetcher_observer.h"
-#include "starboard/system.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace cobalt {
-namespace extension {
-
-TEST(ExtensionTest, PlatformService) {
-  typedef CobaltExtensionPlatformServiceApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionPlatformServiceName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 3u);
-  EXPECT_NE(extension_api->Has, nullptr);
-  EXPECT_NE(extension_api->Open, nullptr);
-  EXPECT_NE(extension_api->Close, nullptr);
-  EXPECT_NE(extension_api->Send, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, Graphics) {
-  typedef CobaltExtensionGraphicsApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionGraphicsName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 5u);
-
-  EXPECT_NE(extension_api->GetMaximumFrameIntervalInMilliseconds, nullptr);
-  float maximum_frame_interval =
-      extension_api->GetMaximumFrameIntervalInMilliseconds();
-  EXPECT_FALSE(std::isnan(maximum_frame_interval));
-
-  if (extension_api->version >= 2) {
-    EXPECT_NE(extension_api->GetMinimumFrameIntervalInMilliseconds, nullptr);
-    float minimum_frame_interval =
-        extension_api->GetMinimumFrameIntervalInMilliseconds();
-    EXPECT_GT(minimum_frame_interval, 0);
-  }
-
-  if (extension_api->version >= 3) {
-    EXPECT_NE(extension_api->IsMapToMeshEnabled, nullptr);
-  }
-
-  if (extension_api->version >= 4) {
-    EXPECT_NE(extension_api->ShouldClearFrameOnShutdown, nullptr);
-    float clear_color_r, clear_color_g, clear_color_b, clear_color_a;
-    if (extension_api->ShouldClearFrameOnShutdown(
-            &clear_color_r, &clear_color_g, &clear_color_b, &clear_color_a)) {
-      EXPECT_GE(clear_color_r, 0.0f);
-      EXPECT_LE(clear_color_r, 1.0f);
-      EXPECT_GE(clear_color_g, 0.0f);
-      EXPECT_LE(clear_color_g, 1.0f);
-      EXPECT_GE(clear_color_b, 0.0f);
-      EXPECT_LE(clear_color_b, 1.0f);
-      EXPECT_GE(clear_color_a, 0.0f);
-      EXPECT_LE(clear_color_a, 1.0f);
-    }
-  }
-
-  if (extension_api->version >= 5) {
-    EXPECT_NE(extension_api->GetMapToMeshColorAdjustments, nullptr);
-    EXPECT_NE(extension_api->GetRenderRootTransform, nullptr);
-  }
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, InstallationManager) {
-  typedef CobaltExtensionInstallationManagerApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionInstallationManagerName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 3u);
-  EXPECT_NE(extension_api->GetCurrentInstallationIndex, nullptr);
-  EXPECT_NE(extension_api->MarkInstallationSuccessful, nullptr);
-  EXPECT_NE(extension_api->RequestRollForwardToInstallation, nullptr);
-  EXPECT_NE(extension_api->GetInstallationPath, nullptr);
-  EXPECT_NE(extension_api->SelectNewInstallationIndex, nullptr);
-  EXPECT_NE(extension_api->GetAppKey, nullptr);
-  EXPECT_NE(extension_api->GetMaxNumberInstallations, nullptr);
-  EXPECT_NE(extension_api->ResetInstallation, nullptr);
-  EXPECT_NE(extension_api->Reset, nullptr);
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, Configuration) {
-  typedef CobaltExtensionConfigurationApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionConfigurationName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 3u);
-  EXPECT_NE(extension_api->CobaltUserOnExitStrategy, nullptr);
-  EXPECT_NE(extension_api->CobaltRenderDirtyRegionOnly, nullptr);
-  EXPECT_NE(extension_api->CobaltEglSwapInterval, nullptr);
-  EXPECT_NE(extension_api->CobaltFallbackSplashScreenUrl, nullptr);
-  EXPECT_NE(extension_api->CobaltEnableQuic, nullptr);
-  EXPECT_NE(extension_api->CobaltSkiaCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltOffscreenTargetCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltEncodedImageCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltImageCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltLocalTypefaceCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltRemoteTypefaceCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltMeshCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltSoftwareSurfaceCacheSizeInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltImageCacheCapacityMultiplierWhenPlayingVideo,
-            nullptr);
-  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasWidth, nullptr);
-  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasHeight, nullptr);
-  EXPECT_NE(extension_api->CobaltJsGarbageCollectionThresholdInBytes, nullptr);
-  EXPECT_NE(extension_api->CobaltReduceCpuMemoryBy, nullptr);
-  EXPECT_NE(extension_api->CobaltReduceGpuMemoryBy, nullptr);
-  EXPECT_NE(extension_api->CobaltGcZeal, nullptr);
-  if (extension_api->version >= 2) {
-    EXPECT_NE(extension_api->CobaltFallbackSplashScreenTopics, nullptr);
-  }
-
-  if (extension_api->version >= 3) {
-    EXPECT_NE(extension_api->CobaltCanStoreCompiledJavascript, nullptr);
-  }
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, MediaSession) {
-  typedef CobaltExtensionMediaSessionApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionMediaSessionName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->OnMediaSessionStateChanged, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, CrashHandler) {
-  typedef CobaltExtensionCrashHandlerApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionCrashHandlerName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 2u);
-  EXPECT_NE(extension_api->OverrideCrashpadAnnotations, nullptr);
-
-  if (extension_api->version >= 2) {
-    EXPECT_NE(extension_api->SetString, nullptr);
-  }
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, CWrappers) {
-  typedef CobaltExtensionCWrappersApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionCWrappersName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->PowWrapper, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, Font) {
-  typedef CobaltExtensionFontApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionFontName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->GetPathFallbackFontDirectory, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, JavaScriptCache) {
-  typedef CobaltExtensionJavaScriptCacheApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionJavaScriptCacheName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->GetCachedScript, nullptr);
-  EXPECT_NE(extension_api->ReleaseCachedScriptData, nullptr);
-  EXPECT_NE(extension_api->StoreCachedScript, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, UrlFetcherObserver) {
-  typedef CobaltExtensionUrlFetcherObserverApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionUrlFetcherObserverName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->FetcherCreated, nullptr);
-  EXPECT_NE(extension_api->FetcherDestroyed, nullptr);
-  EXPECT_NE(extension_api->StartURLRequest, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, UpdaterNotification) {
-  typedef CobaltExtensionUpdaterNotificationApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionUpdaterNotificationName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->UpdaterState, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, MemoryMappedFile) {
-  typedef CobaltExtensionMemoryMappedFileApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionMemoryMappedFileName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->MemoryMapFile, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-
-TEST(ExtensionTest, FreeSpace) {
-  typedef CobaltExtensionFreeSpaceApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionFreeSpaceName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_EQ(extension_api->version, 1u);
-  EXPECT_NE(extension_api->MeasureFreeSpace, nullptr);
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-}  // namespace extension
-}  // namespace cobalt
diff --git a/cobalt/extension/font.h b/cobalt/extension/font.h
index b6c0e5f..6eaac67 100644
--- a/cobalt/extension/font.h
+++ b/cobalt/extension/font.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,31 +15,10 @@
 #ifndef COBALT_EXTENSION_FONT_H_
 #define COBALT_EXTENSION_FONT_H_
 
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionFontName "dev.cobalt.extension.Font"
-
-typedef struct CobaltExtensionFontApi {
-  // Name should be the string |kCobaltExtensionFontName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Provide additional font directory for fonts not available
-  // as system or Cobalt fonts. This is useful for adding local fallback fonts.
-  bool (*GetPathFallbackFontDirectory)(char* path, int path_size);
-} CobaltExtensionFontApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/font.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_FONT_H_
diff --git a/cobalt/extension/free_space.h b/cobalt/extension/free_space.h
index 53d466b..856cf17 100644
--- a/cobalt/extension/free_space.h
+++ b/cobalt/extension/free_space.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,39 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
 #ifndef COBALT_EXTENSION_FREE_SPACE_H_
 #define COBALT_EXTENSION_FREE_SPACE_H_
 
-#include <stdint.h>
-
-#include "starboard/system.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-#define kCobaltExtensionFreeSpaceName "dev.cobalt.extension.FreeSpace"
-
-typedef struct CobaltExtensionFreeSpaceApi {
-  // Name should be the string |kCobaltExtensionFreeSpaceName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Returns the free space in bytes for the provided |system_path_id|.
-  // If there is no implementation for the that |system_path_id| or
-  // if there was an error -1 is returned.
-  int64_t (*MeasureFreeSpace)(SbSystemPathId system_path_id);
-} CobaltExtensionFreeSpaceApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/free_space.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_FREE_SPACE_H_
diff --git a/cobalt/extension/graphics.h b/cobalt/extension/graphics.h
index ed55a2e..e7943c9 100644
--- a/cobalt/extension/graphics.h
+++ b/cobalt/extension/graphics.h
@@ -1,4 +1,4 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,102 +15,10 @@
 #ifndef COBALT_EXTENSION_GRAPHICS_H_
 #define COBALT_EXTENSION_GRAPHICS_H_
 
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionGraphicsName "dev.cobalt.extension.Graphics"
-
-// This structure allows post-processing of output colors for 360 videos.
-// Given "rgba" is the color that the pixel shader calculates, this struct
-// allows additional processing in the form of:
-//   final color = rgba0_scale +
-//                 rgba1_scale * rgba +
-//                 rgba2_scale * rgba * rgba +
-//                 rgba3_scale * rgba * rgba * rgba;
-// The final_color is then clamped to the range of [0,1] for each element.
-typedef struct CobaltExtensionGraphicsMapToMeshColorAdjustment {
-  float rgba0_scale[4];  // multiplier for rgba^0
-  float rgba1_scale[4];  // multiplier for rgba^1
-  float rgba2_scale[4];  // multiplier for rgba^2
-  float rgba3_scale[4];  // multiplier for rgba^3
-} CobaltExtensionGraphicsMapToMeshColorAdjustment;
-
-typedef struct CobaltExtensionGraphicsApi {
-  // Name should be the string kCobaltExtensionGraphicsName.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Get the maximum time between rendered frames. This value can be dynamic
-  // and is queried periodically. This can be used to force the rasterizer to
-  // present a new frame even if nothing has changed visually. Due to the
-  // imprecision of thread scheduling, it may be necessary to specify a lower
-  // interval time to ensure frames aren't skipped when the throttling logic
-  // is executed a little too early. Return a negative number if frames should
-  // only be presented when something changes (i.e. there is no maximum frame
-  // interval).
-  // NOTE: The gyp variable 'cobalt_minimum_frame_time_in_milliseconds' takes
-  // precedence over this. For example, if the minimum frame time is 8ms and
-  // the maximum frame interval is 0ms, then the renderer will target 125 fps.
-  float (*GetMaximumFrameIntervalInMilliseconds)();
-
-  // The fields below this point were added in version 2 or later.
-
-  // Allow throttling of the frame rate. This is expressed in terms of
-  // milliseconds and can be a floating point number. Keep in mind that
-  // swapping frames may take some additional processing time, so it may be
-  // better to specify a lower delay. For example, '33' instead of '33.33'
-  // for 30 Hz refresh.
-  float (*GetMinimumFrameIntervalInMilliseconds)();
-
-  // The fields below this point were added in version 3 or later.
-
-  // Get whether the renderer should support 360 degree video or not.
-  bool (*IsMapToMeshEnabled)();
-
-  // The fields below this point were added in version 4 or later.
-
-  // Specify whether the framebuffer should be cleared when the graphics
-  // system is shutdown and color to use for clearing. The graphics system
-  // is shutdown on suspend or exit. The clear color values should be in the
-  // range of [0,1]; color values are only used if this function returns true.
-  //
-  // The default behavior is to clear to opaque black on shutdown unless this
-  // API specifies otherwise.
-  bool (*ShouldClearFrameOnShutdown)(float* clear_color_red,
-                                     float* clear_color_green,
-                                     float* clear_color_blue,
-                                     float* clear_color_alpha);
-
-  // The fields below this point were added in version 5 or later.
-
-  // Use the provided color adjustments for 360 videos if the function returns
-  // true. See declaration of CobaltExtensionGraphicsMapToMeshColorAdjustment
-  // for details.
-  bool (*GetMapToMeshColorAdjustments)(
-      CobaltExtensionGraphicsMapToMeshColorAdjustment* adjustment);
-
-  // This function can be used to insert a custom transform at the root of
-  // rendering. This allows custom scaling, rotating, etc. of the frame. This
-  // only impacts rendering of the frame -- the web app will not know about this
-  // transform, so it may not layout elements appropriately. This function
-  // should return true if a custom transform should be used.
-  bool (*GetRenderRootTransform)(float* m00, float* m01, float* m02, float* m10,
-                                 float* m11, float* m12, float* m20, float* m21,
-                                 float* m22);
-} CobaltExtensionGraphicsApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/graphics.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_GRAPHICS_H_
diff --git a/cobalt/extension/installation_manager.h b/cobalt/extension/installation_manager.h
index 9c96781..97a0757 100644
--- a/cobalt/extension/installation_manager.h
+++ b/cobalt/extension/installation_manager.h
@@ -1,4 +1,4 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,48 +15,10 @@
 #ifndef COBALT_EXTENSION_INSTALLATION_MANAGER_H_
 #define COBALT_EXTENSION_INSTALLATION_MANAGER_H_
 
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-
-#define IM_EXT_MAX_APP_KEY_LENGTH 1024
-#define IM_EXT_INVALID_INDEX -1
-#define IM_EXT_ERROR -1
-#define IM_EXT_SUCCESS 0
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionInstallationManagerName \
-  "dev.cobalt.extension.InstallationManager"
-
-typedef struct CobaltExtensionInstallationManagerApi {
-  // Name should be the string kCobaltExtensionInstallationManagerName.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // Installation Manager API wrapper.
-  // For more details, check:
-  //  starboard/loader_app/installation_manager.h
-
-  int (*GetCurrentInstallationIndex)();
-  int (*MarkInstallationSuccessful)(int installation_index);
-  int (*RequestRollForwardToInstallation)(int installation_index);
-  int (*GetInstallationPath)(int installation_index, char* path,
-                             int path_length);
-  int (*SelectNewInstallationIndex)();
-  int (*GetAppKey)(char* app_key, int app_key_length);
-  int (*GetMaxNumberInstallations)();
-  int (*ResetInstallation)(int installation_index);
-  int (*Reset)();
-} CobaltExtensionInstallationManagerApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/installation_manager.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_INSTALLATION_MANAGER_H_
diff --git a/cobalt/extension/javascript_cache.h b/cobalt/extension/javascript_cache.h
index 660c578..ef58a74 100644
--- a/cobalt/extension/javascript_cache.h
+++ b/cobalt/extension/javascript_cache.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,48 +15,10 @@
 #ifndef COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
 #define COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
 
-#include <stdbool.h>
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionJavaScriptCacheName \
-  "dev.cobalt.extension.JavaScriptCache"
-
-// The implementation must be thread-safe as the extension would
-// be called from different threads. Also all storage management
-// is delegated to the platform.
-typedef struct CobaltExtensionJavaScriptCacheApi {
-  // Name should be the string |kCobaltExtensionJavaScriptCacheName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Retrieves the cached data for a script using |key|. The |source_length|
-  // provides the actual size of the script source in bytes.  The cached script
-  // bytes will be returned in |cache_data_out|. After the |cache_data| is
-  // processed the memory should be released by calling
-  // |ReleaseScriptCacheData|.
-  bool (*GetCachedScript)(uint32_t key, int source_length,
-                          const uint8_t** cache_data_out,
-                          int* cache_data_length);
-
-  // Releases the memory allocated for the |cache_data| by |GetCachedScript|.
-  void (*ReleaseCachedScriptData)(const uint8_t* cache_data);
-
-  // Stores the cached data for |key|.
-  bool (*StoreCachedScript)(uint32_t key, int source_length,
-                            const uint8_t* cache_data, int cache_data_length);
-} CobaltExtensionJavaScriptCacheApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/javascript_cache.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
diff --git a/cobalt/extension/media_session.h b/cobalt/extension/media_session.h
index 38d3322..1aebd30 100644
--- a/cobalt/extension/media_session.h
+++ b/cobalt/extension/media_session.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,123 +15,10 @@
 #ifndef COBALT_EXTENSION_MEDIA_SESSION_H_
 #define COBALT_EXTENSION_MEDIA_SESSION_H_
 
-#include "starboard/configuration.h"
-#include "starboard/time.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionMediaSessionName "dev.cobalt.extension.MediaSession"
-
-typedef enum CobaltExtensionMediaSessionPlaybackState {
-  kCobaltExtensionMediaSessionNone = 0,
-  kCobaltExtensionMediaSessionPaused = 1,
-  kCobaltExtensionMediaSessionPlaying = 2
-} CobaltExtensionMediaSessionPlaybackState;
-
-typedef enum CobaltExtensionMediaSessionAction {
-  kCobaltExtensionMediaSessionActionPlay,
-  kCobaltExtensionMediaSessionActionPause,
-  kCobaltExtensionMediaSessionActionSeekbackward,
-  kCobaltExtensionMediaSessionActionSeekforward,
-  kCobaltExtensionMediaSessionActionPrevioustrack,
-  kCobaltExtensionMediaSessionActionNexttrack,
-  kCobaltExtensionMediaSessionActionSkipad,
-  kCobaltExtensionMediaSessionActionStop,
-  kCobaltExtensionMediaSessionActionSeekto,
-
-  // Not part of spec, but used in Cobalt implementation.
-  kCobaltExtensionMediaSessionActionNumActions,
-} CobaltExtensionMediaSessionAction;
-
-typedef struct CobaltExtensionMediaImage {
-  // These fields are null-terminated strings copied over from IDL.
-  const char* size;
-  const char* src;
-  const char* type;
-} CobaltExtensionMediaImage;
-
-typedef struct CobaltExtensionMediaMetadata {
-  // These fields are null-terminated strings copied over from IDL.
-  const char* album;
-  const char* artist;
-  const char* title;
-
-  CobaltExtensionMediaImage* artwork;
-  size_t artwork_count;
-} CobaltExtensionMediaMetadata;
-
-typedef struct CobaltExtensionMediaSessionActionDetails {
-  CobaltExtensionMediaSessionAction action;
-
-  // Seek time/offset are non-negative. Negative value signifies "unset".
-  double seek_offset;
-  double seek_time;
-
-  bool fast_seek;
-} CobaltExtensionMediaSessionActionDetails;
-
-typedef void (*CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback)(
-    CobaltExtensionMediaSessionPlaybackState state, void* callback_context);
-typedef void (*CobaltExtensionMediaSessionInvokeActionCallback)(
-    CobaltExtensionMediaSessionActionDetails details, void* callback_context);
-
-// This struct and all its members should only be used for piping data to each
-// platform's implementation of OnMediaSessionStateChanged and they are only
-// valid within the scope of that function. Any data inside must be copied if it
-// will be referenced later.
-typedef struct CobaltExtensionMediaSessionState {
-  SbTimeMonotonic duration;
-  CobaltExtensionMediaSessionPlaybackState actual_playback_state;
-  bool available_actions[kCobaltExtensionMediaSessionActionNumActions];
-  CobaltExtensionMediaMetadata* metadata;
-  double actual_playback_rate;
-  SbTimeMonotonic current_playback_position;
-  bool has_position_state;
-} CobaltExtensionMediaSessionState;
-
-typedef struct CobaltExtensionMediaSessionApi {
-  // Name should be the string kCobaltExtensionMediaSessionName.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // MediaSession API Wrapper.
-
-  void (*OnMediaSessionStateChanged)(
-      CobaltExtensionMediaSessionState session_state);
-
-  // Register MediaSessionClient callbacks when the platform create a new
-  // MediaSessionClient.
-  void (*RegisterMediaSessionCallbacks)(
-      void* callback_context,
-      CobaltExtensionMediaSessionInvokeActionCallback invoke_action_callback,
-      CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback
-          update_platform_playback_state_callback);
-
-  // Destroy platform's MediaSessionClient after the Cobalt's
-  // MediaSessionClient has been destroyed.
-  void (*DestroyMediaSessionClientCallback)();
-
-  // Starboard method for updating playback state.
-  void (*UpdateActiveSessionPlatformPlaybackState)(
-      CobaltExtensionMediaSessionPlaybackState state);
-} CobaltExtensionMediaSessionApi;
-
-inline void CobaltExtensionMediaSessionActionDetailsInit(
-    CobaltExtensionMediaSessionActionDetails* details,
-    CobaltExtensionMediaSessionAction action) {
-  details->action = action;
-  details->seek_offset = -1.0;
-  details->seek_time = -1.0;
-  details->fast_seek = false;
-}
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/media_session.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_MEDIA_SESSION_H_
diff --git a/cobalt/extension/memory_mapped_file.h b/cobalt/extension/memory_mapped_file.h
index 7c1c4bb..c5ac54c 100644
--- a/cobalt/extension/memory_mapped_file.h
+++ b/cobalt/extension/memory_mapped_file.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,47 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
 #ifndef COBALT_EXTENSION_MEMORY_MAPPED_FILE_H_
 #define COBALT_EXTENSION_MEMORY_MAPPED_FILE_H_
 
-#include <stdint.h>
-
-#include "starboard/memory.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-#define kCobaltExtensionMemoryMappedFileName \
-  "dev.cobalt.extension.MemoryMappedFile"
-
-typedef struct CobaltExtensionMemoryMappedFileApi {
-  // Name should be the string |kCobaltExtensionMemoryMappedFileName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Memory maps a file at the specified |address| starting at |file_offset|
-  // and  mapping |size| bytes. The |address| argument can be NULL in which
-  // case new memory buffer will be allocated. If a non NULL |address| is
-  // passed the memory should be resreved in advance through |SbMemoryMap|.
-  // To release the memory call |SbMemoryUnmap|.
-  // The |file_offset| must be a multiple of |kSbMemoryPageSize|.
-  // Returns NULL or error.
-  void* (*MemoryMapFile)(void* address, const char* path,
-                         SbMemoryMapFlags flags, int64_t file_offset,
-                         int64_t size);
-
-} CobaltExtensionMemoryMappedFileApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/memory_mapped_file.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_MEMORY_MAPPED_FILE_H_
diff --git a/cobalt/extension/on_screen_keyboard.h b/cobalt/extension/on_screen_keyboard.h
deleted file mode 100644
index 315e2b7..0000000
--- a/cobalt/extension/on_screen_keyboard.h
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_EXTENSION_ON_SCREEN_KEYBOARD_H_
-#define COBALT_EXTENSION_ON_SCREEN_KEYBOARD_H_
-
-#include "starboard/system.h"
-#include "starboard/window.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionOnScreenKeyboardName \
-  "dev.cobalt.extension.OnScreenKeyboard"
-
-typedef struct CobaltExtensionOnScreenKeyboardApi {
-  // Name should be the string
-  // |kCobaltExtensionOnScreenKeyboardName|. This helps to validate that
-  // the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // This function overrides the background color of on-screen keyboard in RGB
-  // color space, where r, g, b are between 0 and 255.
-  void (*SetBackgroundColor)(SbWindow window, uint8_t r, uint8_t g, uint8_t b);
-
-  // This function overrides the light theme of on-screen keyboard.
-  void (*SetLightTheme)(SbWindow window, bool light_theme);
-} CobaltExtensionOnScreenKeyboardApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-#endif  // COBALT_EXTENSION_ON_SCREEN_KEYBOARD_H_
diff --git a/cobalt/extension/platform_service.h b/cobalt/extension/platform_service.h
index 4b66e86..fca88ef 100644
--- a/cobalt/extension/platform_service.h
+++ b/cobalt/extension/platform_service.h
@@ -1,4 +1,4 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,74 +15,10 @@
 #ifndef COBALT_EXTENSION_PLATFORM_SERVICE_H_
 #define COBALT_EXTENSION_PLATFORM_SERVICE_H_
 
-#include <stdint.h>
-
-#include "starboard/configuration.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct CobaltExtensionPlatformServicePrivate
-    CobaltExtensionPlatformServicePrivate;
-typedef CobaltExtensionPlatformServicePrivate* CobaltExtensionPlatformService;
-
-// Well-defined value for an invalid |Service|.
-#define kCobaltExtensionPlatformServiceInvalid \
-  ((CobaltExtensionPlatformService)0)
-
-#define kCobaltExtensionPlatformServiceName \
-  "dev.cobalt.extension.PlatformService"
-
-// Checks whether a |CobaltExtensionPlatformService| is valid.
-static SB_C_INLINE bool CobaltExtensionPlatformServiceIsValid(
-    CobaltExtensionPlatformService service) {
-  return service != kCobaltExtensionPlatformServiceInvalid;
-}
-
-// When a client receives a message from a service, the service will be passed
-// in as the |context| here, with |data|, which has length |length|.
-typedef void (*ReceiveMessageCallback)(void* context, const void* data,
-                                       uint64_t length);
-
-typedef struct CobaltExtensionPlatformServiceApi {
-  // Name should be the string kCobaltExtensionPlatformServiceName.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Return whether the platform has service indicated by |name|.
-  bool (*Has)(const char* name);
-
-  // Open and return a service by name.
-  //
-  // |context|: pointer to context object for callback.
-  // |name|: name of the service.
-  // |receive_callback|: callback to run when Cobalt should receive data.
-  CobaltExtensionPlatformService (*Open)(
-      void* context, const char* name, ReceiveMessageCallback receive_callback);
-
-  // Close the service passed in as |service|.
-  void (*Close)(CobaltExtensionPlatformService service);
-
-  // Send |data| of length |length| to |service|. If there is a synchronous
-  // response, it will be returned via void* and |output_length| will be set
-  // to its length. The returned void* will be owned by the caller, and must
-  // be deallocated via SbMemoryDeallocate() by the caller when appropriate.
-  // If there is no synchronous response, NULL will be returned and
-  // |output_length| will be 0. The |invalid_state| will be set to true if the
-  // service is not currently able to accept data, and otherwise will be set to
-  // false.
-  void* (*Send)(CobaltExtensionPlatformService service, void* data,
-                uint64_t length, uint64_t* output_length, bool* invalid_state);
-} CobaltExtensionPlatformServiceApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/platform_service.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_PLATFORM_SERVICE_H_
diff --git a/cobalt/extension/updater_notification.h b/cobalt/extension/updater_notification.h
index 906cafc..e56e77d 100644
--- a/cobalt/extension/updater_notification.h
+++ b/cobalt/extension/updater_notification.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,54 +15,10 @@
 #ifndef COBALT_EXTENSION_UPDATER_NOTIFICATION_H_
 #define COBALT_EXTENSION_UPDATER_NOTIFICATION_H_
 
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionUpdaterNotificationName \
-  "dev.cobalt.extension.UpdaterNotification"
-
-typedef enum CobaltExtensionUpdaterNotificationState {
-  kCobaltExtensionUpdaterNotificationStateNone = 0,
-  kCobaltExtensionUpdaterNotificationStateChecking = 1,
-  kCobaltExtensionUpdaterNotificationStateUpdateAvailable = 2,
-  kCobaltExtensionUpdaterNotificationStateDownloading = 3,
-  kCobaltExtensionUpdaterNotificationStateDownloaded = 4,
-  kCobaltExtensionUpdaterNotificationStateInstalling = 5,
-#if SB_API_VERSION > 13
-  kCobaltExtensionUpdaterNotificationStateUpdated = 6,
-  kCobaltExtensionUpdaterNotificationStateUpToDate = 7,
-  kCobaltExtensionUpdaterNotificationStateUpdateFailed = 8,
+#if SB_API_VERSION <= 14
+#include "starboard/extension/updater_notification.h"
 #else
-  kCobaltExtensionUpdaterNotificationStatekUpdated = 6,
-  kCobaltExtensionUpdaterNotificationStatekUpToDate = 7,
-  kCobaltExtensionUpdaterNotificationStatekUpdateFailed = 8,
-#endif
-} CobaltExtensionUpdaterNotificationState;
-
-typedef struct CobaltExtensionUpdaterNotificationApi {
-  // Name should be the string |kCobaltExtensionUpdaterNotificationName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // Notify the Starboard implementation of the updater state.
-  // The Starboard platform can check if the device is low on storage
-  // and prompt the user to free some storage. The implementation
-  // should keep track of the frequency of showing the prompt to the
-  // user and try to minimize the number of user notifications.
-  void (*UpdaterState)(CobaltExtensionUpdaterNotificationState state,
-                       const char* current_evergreen_version);
-} CobaltExtensionUpdaterNotificationApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_UPDATER_NOTIFICATION_H_
diff --git a/cobalt/extension/url_fetcher_observer.h b/cobalt/extension/url_fetcher_observer.h
index d9a4a10..65619d3 100644
--- a/cobalt/extension/url_fetcher_observer.h
+++ b/cobalt/extension/url_fetcher_observer.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,40 +15,10 @@
 #ifndef COBALT_EXTENSION_URL_FETCHER_OBSERVER_H_
 #define COBALT_EXTENSION_URL_FETCHER_OBSERVER_H_
 
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define URL_FETCHER_OBSERVER_MAX_URL_SIZE 128
-#define URL_FETCHER_COMMAND_LINE_SWITCH "url_fetcher_observer"
-
-#define kCobaltExtensionUrlFetcherObserverName \
-  "dev.cobalt.extension.UrlFetcherObserver"
-
-typedef struct CobaltExtensionUrlFetcherObserverApi {
-  // Name should be the string |kCobaltExtensionUrlFetcherObserverName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-  // The UrlFetcher for the specified |url| was created.
-  void (*FetcherCreated)(const char* url);
-
-  // The UrlFetcher for the specified |url| was destroyed.
-  void (*FetcherDestroyed)(const char* url);
-
-  // The URL request started for the specified |url|.
-  void (*StartURLRequest)(const char* url);
-} CobaltExtensionUrlFetcherObserverApi;
-
-#ifdef __cplusplus
-}  // extern "C"
+#if SB_API_VERSION <= 14
+#include "starboard/extension/url_fetcher_observer.h"
+#else
+#error "Extensions have moved, please see Starboard CHANGELOG for details."
 #endif
 
 #endif  // COBALT_EXTENSION_URL_FETCHER_OBSERVER_H_
diff --git a/cobalt/fetch/embedded_scripts/fetch.js b/cobalt/fetch/embedded_scripts/fetch.js
index ec89191..87435e2 100644
--- a/cobalt/fetch/embedded_scripts/fetch.js
+++ b/cobalt/fetch/embedded_scripts/fetch.js
@@ -1,22 +1,22 @@
-'use strict';(function(h){function J(a){"string"!==typeof a&&(a=String(a));if(/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(a))throw new e("Invalid character in header field name");return a.toLowerCase()}function X(a){"string"!==typeof a&&(a=String(a));var b;var c=0;for(b=a.length;c<b;c++){var d=a.charCodeAt(c);if(9!==d&&10!==d&&13!==d&&32!==d)break}for(b=a.length-1;b>c&&(d=a.charCodeAt(b),9===d||10===d||13===d||32===d);b--);a=a.substring(c,b+1);c=0;for(b=a.length;c<b;c++)if(d=a.charCodeAt(c),256<=d||0===d||
-10===d||13===d)throw new e("Invalid character in header field value");return a}function fa(a,b){throw new e("Immutable header cannot be modified");}function ha(a,b){return!1}function ia(a,b){a=a.toLowerCase();return-1<ja.indexOf(a)||a.startsWith("proxy-")||a.startsWith("sec-")?!0:!1}function ka(a,b){a=a.toLowerCase();return-1<la.indexOf(a)||"content-type"===a&&(b=b.split(";")[0].toLowerCase(),-1<ma.indexOf(b))?!1:!0}function S(a,b){return-1<na.indexOf(a.toLowerCase())?!0:!1}function n(a){this[p]=
-new P;void 0===this[A]&&(this[A]=ha);if(void 0!==a){if(null===a||"object"!==typeof a)throw new e("Constructing Headers with invalid parameters");a instanceof n?a.forEach(function(b,c){this.append(c,b)},this):K.isArray(a)?a.forEach(function(b){if(2!==b.length)throw new e("Constructing Headers with invalid parameters");this.append(b[0],b[1])},this):Object.getOwnPropertyNames(a).forEach(function(b){this.append(b,a[b])},this)}}function L(a,b){var c=oa(n.prototype);c[A]=b;n.call(c,a);return c}function T(a){if(a.bodyUsed)return z.reject(new e("Body was already read"));
-if(null===a.body)return z.resolve(new v(0));if(pa(a.body))return z.reject(new e("ReadableStream was already locked"));var b=a.body.getReader(),c=[],d=0;return b.read().then(function q(f){if(f.done){if(0===c.length)f=new v(0);else if(1===c.length)f=new v(c[0].buffer);else{f=new v(d);for(var g=0,w=c.length,M=0;g<w;g++)f.set(c[g],M),M+=c[g].length}return f}return f.value instanceof v?(d+=f.value.length,c.push(f.value),b.read().then(q)):z.reject(new e("Invalid stream data type"))})}function Y(){this._initBody=
-function(a){this[U]=!1;this[r]=null===a||void 0===a?null:a instanceof V?a:new V({start:function(b){if(a)if("string"===typeof a)b.enqueue(FetchInternal.encodeToUTF8(a));else if(Z.prototype.isPrototypeOf(a))b.enqueue(new v(a.slice(0)));else if(qa(a)){var c=new v(a.buffer);c=v.from(c.slice(a.byteOffset,a.byteLength+1));b.enqueue(c)}else if(a instanceof Blob)b.enqueue(new v(FetchInternal.blobToArrayBuffer(a)));else throw new e("Unsupported BodyInit type");b.close()}});this[x].get("content-type")||("string"===
-typeof a?this[x].set("content-type","text/plain;charset=UTF-8"):a instanceof Blob&&""!==a.type&&this[x].set("content-type",a.type))};W(this,{body:{get:function(){return this[r]}},bodyUsed:{get:function(){return this[U]?!0:this[r]?!!ra(this[r]):!1}}});this.arrayBuffer=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return a.buffer})};this.text=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return FetchInternal.decodeFromUTF8(a)})};
+'use strict';(function(h){function J(a){"string"!==typeof a&&(a=String(a));if(/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(a))throw new e("Invalid character in header field name");return a.toLowerCase()}function Y(a){"string"!==typeof a&&(a=String(a));var b;var c=0;for(b=a.length;c<b;c++){var d=a.charCodeAt(c);if(9!==d&&10!==d&&13!==d&&32!==d)break}for(b=a.length-1;b>c&&(d=a.charCodeAt(b),9===d||10===d||13===d||32===d);b--);a=a.substring(c,b+1);c=0;for(b=a.length;c<b;c++)if(d=a.charCodeAt(c),256<=d||0===d||
+10===d||13===d)throw new e("Invalid character in header field value");return a}function ia(a,b){throw new e("Immutable header cannot be modified");}function ja(a,b){return!1}function ka(a,b){a=a.toLowerCase();return-1<la.indexOf(a)||a.startsWith("proxy-")||a.startsWith("sec-")?!0:!1}function ma(a,b){a=a.toLowerCase();return-1<na.indexOf(a)||"content-type"===a&&(b=b.split(";")[0].toLowerCase(),-1<oa.indexOf(b))?!1:!0}function S(a,b){return-1<pa.indexOf(a.toLowerCase())?!0:!1}function n(a){this[p]=
+new P;void 0===this[A]&&(this[A]=ja);if(void 0!==a){if(null===a||"object"!==typeof a)throw new e("Constructing Headers with invalid parameters");a instanceof n?a.forEach(function(b,c){this.append(c,b)},this):K.isArray(a)?a.forEach(function(b){if(2!==b.length)throw new e("Constructing Headers with invalid parameters");this.append(b[0],b[1])},this):Object.getOwnPropertyNames(a).forEach(function(b){this.append(b,a[b])},this)}}function L(a,b){var c=qa(n.prototype);c[A]=b;n.call(c,a);return c}function T(a){if(a.bodyUsed)return z.reject(new e("Body was already read"));
+if(null===a.body)return z.resolve(new v(0));if(ra(a.body))return z.reject(new e("ReadableStream was already locked"));var b=a.body.getReader(),c=[],d=0;return b.read().then(function q(f){if(f.done){if(0===c.length)f=new v(0);else if(1===c.length)f=new v(c[0].buffer);else{f=new v(d);for(var g=0,w=c.length,M=0;g<w;g++)f.set(c[g],M),M+=c[g].length}return f}return f.value instanceof v?(d+=f.value.length,c.push(f.value),b.read().then(q)):z.reject(new e("Invalid stream data type"))})}function Z(){this._initBody=
+function(a){this[U]=!1;this[r]=null===a||void 0===a?null:a instanceof V?a:new V({start(b){if(a)if("string"===typeof a)b.enqueue(FetchInternal.encodeToUTF8(a));else if(aa.prototype.isPrototypeOf(a))b.enqueue(new v(a.slice(0)));else if(sa(a)){var c=new v(a.buffer);c=v.from(c.slice(a.byteOffset,a.byteLength+1));b.enqueue(c)}else if(a instanceof Blob)b.enqueue(new v(FetchInternal.blobToArrayBuffer(a)));else throw new e("Unsupported BodyInit type");b.close()}});this[x].get("content-type")||("string"===
+typeof a?this[x].set("content-type","text/plain;charset=UTF-8"):a instanceof Blob&&""!==a.type&&this[x].set("content-type",a.type))};W(this,{body:{get:function(){return this[r]}},bodyUsed:{get:function(){return this[U]?!0:this[r]?!!ta(this[r]):!1}}});this.arrayBuffer=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return a.buffer})};this.text=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return FetchInternal.decodeFromUTF8(a)})};
 this.json=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):this.text().then(JSON.parse)};return this}function B(a,b){var c=void 0!==b&&null!==b&&void 0===b.cloneBody;b=b||{};var d=b.body||b.cloneBody;""===b.body&&(d="");var l=b.headers,f=new AbortController;this[F]=f.signal;f=null;if(a instanceof B)this[C]=a.url,this[G]=a.cache,this[H]=a.credentials,void 0===l&&(l=a.headers),this[I]=a.integrity,this[D]=a.method,this[t]=a.mode,c&&"navigate"===this[t]&&(this[t]="same-origin"),
-this[N]=a.redirect,d||null===a.body||(d=a.body,a[U]=!0),f=a[F];else{this[C]=String(a);if(!FetchInternal.isUrlValid(this[C],!1))throw new e("Invalid request URL");this[t]="cors";this[H]="same-origin"}if(void 0!==b.window&&null!==b.window)throw new e("Invalid request window");this[G]=b.cache||this[G]||"default";if(-1===sa.indexOf(this[G]))throw new e("Invalid request cache mode");this[H]=b.credentials||this[H]||"same-origin";if(-1===ta.indexOf(this[H]))throw new e("Invalid request credentials");void 0!==
-b.integrity?this[I]=b.integrity:void 0===this[I]&&(this[I]="");a=(b.method||this[D]||"GET").toUpperCase();if(-1===ua.indexOf(a))throw new e("Invalid request method");this[D]=a;if(b.mode&&-1===va.indexOf(b.mode))throw new e("Invalid request mode");this[t]=b.mode||this[t]||"no-cors";if("no-cors"===this[t]){if(-1===wa.indexOf(this[D]))throw new e("Invalid request method for no-cors");if(""!==this[I])throw new e("Request integrity data is not allowed with no-cors");}if("same-origin"!==this[t]&&"only-if-cached"===
-this[G])throw new e("Request mode must be same-origin for only-if-cached");this[N]=b.redirect||this[N]||"follow";if(-1===xa.indexOf(this[N]))throw new e("Invalid request redirect mode");this[x]="no-cors"===this[t]?L(l,ka):L(l,ia);if(("GET"===this[D]||"HEAD"===this[D])&&d)throw new e("Request body is not allowed for GET or HEAD");"signal"in b&&(f=b.signal);f&&this[F].follow(f);this._initBody(d)}function ya(a,b){var c=L(void 0,b);a.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(d){var l=
-d.split(":");if(d=l.shift().trim())l=l.join(":").trim(),c.append(d,l)});return c}function u(a,b){b||(b={});this[Q]="default";this[y]="status"in b?b.status:200;if(200>this[y]||599<this[y])throw new aa("Invalid response status");this[ba]=200<=this[y]&&300>this[y];if("statusText"in b){var c=b.statusText;for(var d=0,l=c.length,f;d<l;d++)if(f=c.charCodeAt(d),9!==f&&(32>f||255<f||127===f))throw e("Invalid response status text");}else c="OK";this[R]=c;this[x]=L(b.headers,S);this[C]=b.url||"";if(a&&-1<za.indexOf(this[y]))throw new e("Response body is not allowed with a null body status");
-this[E]=b.is_aborted||!1;this._initBody(a)}if(!h.fetch){var K=h.Array,Z=h.ArrayBuffer,oa=h.Object.create,W=h.Object.defineProperties,k=h.Symbol,Aa=k.iterator,P=h.Map,aa=h.RangeError,e=h.TypeError,v=h.Uint8Array,z=h.Promise,V=h.ReadableStream,ca=h.ReadableStreamTee,ra=h.IsReadableStreamDisturbed,pa=h.IsReadableStreamLocked,r=k("body"),U=k("bodyUsed"),G=k("cache"),H=k("credentials"),A=k("guardCallback"),x=k("headers"),I=k("integrity"),p=k("map"),D=k("method"),t=k("mode"),ba=k("ok"),N=k("redirect"),
-y=k("status"),R=k("statusText"),Q=k("type"),C=k("url"),E=k("is_aborted"),F=k("signal"),ja="accept-charset accept-encoding access-control-request-headers access-control-request-method connection content-length cookie cookie2 date dnt expect host keep-alive origin referer te trailer transfer-encoding upgrade via".split(" "),na=["set-cookie","set-cookie2"],la=["accept","accept-language","content-language"],ma=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],sa="default no-store reload no-cache force-cache only-if-cached".split(" "),
-ta=["omit","same-origin","include"],ua="DELETE GET HEAD OPTIONS POST PUT".split(" "),wa=["GET","HEAD","POST"],va=["same-origin","no-cors","cors"],xa=["follow","error","manual"],za=[101,204,205,304],Ba=[301,302,303,307,308],Ca="[object Int8Array];[object Uint8Array];[object Uint8ClampedArray];[object Int16Array];[object Uint16Array];[object Int32Array];[object Uint32Array];[object Float32Array];[object Float64Array]".split(";"),qa=Z.isView||function(a){return a&&-1<Ca.indexOf(Object.prototype.toString.call(a))};
-n.prototype.append=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to append");a=J(a);b=X(b);this[A](a,b)||(this[p].has(a)?this[p].set(a,this[p].get(a)+", "+b):this[p].set(a,b))};n.prototype["delete"]=function(a){if(1!==arguments.length)throw e("Invalid parameters to delete");this[A](a,"invalid")||this[p].delete(J(a))};n.prototype.get=function(a){if(1!==arguments.length)throw e("Invalid parameters to get");a=J(a);var b=this[p].get(a);return void 0!==b?b:null};n.prototype.has=function(a){if(1!==
-arguments.length)throw e("Invalid parameters to has");return this[p].has(J(a))};n.prototype.set=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to set");a=J(a);b=X(b);this[A](a,b)||this[p].set(a,b)};n.prototype.forEach=function(a,b){var c=this;K.from(this[p].entries()).sort().forEach(function(d){a.call(b,d[1],d[0],c)})};n.prototype.keys=function(){return(new P(K.from(this[p].entries()).sort())).keys()};n.prototype.values=function(){return(new P(K.from(this[p].entries()).sort())).values()};
-n.prototype.entries=function(){return(new P(K.from(this[p].entries()).sort())).entries()};n.prototype[Aa]=n.prototype.entries;B.prototype.clone=function(){var a=null;null!==this[r]&&(a=ca(this[r],!0),this[r]=a[0],a=a[1]);return new B(this,{cloneBody:a,signal:this[F]})};W(B.prototype,{cache:{get:function(){return this[G]}},credentials:{get:function(){return this[H]}},headers:{get:function(){return this[x]}},integrity:{get:function(){return this[I]}},method:{get:function(){return this[D]}},mode:{get:function(){return this[t]}},
-redirect:{get:function(){return this[N]}},url:{get:function(){return this[C]}},signal:{get:function(){return this[F]}}});Y.call(B.prototype);Y.call(u.prototype);u.prototype.clone=function(){var a=null;null!==this[r]&&(a=ca(this[r],!0),this[r]=a[0],a=a[1]);return new u(a,{status:this[y],statusText:this[R],headers:L(this[x],S),url:this[C],is_aborted:this[E]})};W(u.prototype,{headers:{get:function(){return this[x]}},ok:{get:function(){return this[ba]}},status:{get:function(){return this[y]}},statusText:{get:function(){return this[R]}},
-type:{get:function(){return this[Q]}},url:{get:function(){return this[C]}}});u.error=function(){var a=new u(null);a[x][A]=fa;a[Q]="error";a[y]=0;a[R]="";return a};u.redirect=function(a,b){if(!FetchInternal.isUrlValid(a,!0))throw new e("Invalid URL for response redirect");void 0===b&&(b=302);if(-1===Ba.indexOf(b))throw new aa("Invalid status code for response redirect");return new u(null,{status:b,headers:{location:a}})};h.Headers=n;h.Request=B;h.Response=u;h.fetch=function(a,b){return new z(function(c,
-d){var l=!1,f=!1,q=new B(a,b),g=new XMLHttpRequest,w=null;if(q.signal.aborted)return d(new DOMException("Aborted","AbortError"));var M=new V({start:function(m){w=m},cancel:function(m){l=!0;g.abort()}}),Da=function(){if(!l){l=!0;M.cancel();if(w)try{ReadableStreamDefaultControllerError(w,new DOMException("Aborted","AbortError"))}catch(m){}setTimeout(function(){try{g.abort()}catch(m){}},0)}};g.onload=function(){w.close()};g.onreadystatechange=function(){if(g.readyState===g.HEADERS_RECEIVED){var m={status:g.status,
-statusText:g.statusText,headers:ya(g.getAllResponseHeaders()||"",S)};m.url="responseURL"in g?g.responseURL:m.headers.get("X-Request-URL");try{var O=new u(M,m);q[F].addEventListener("abort",function(){O[E]=!0;Da();d(new DOMException("Aborted","AbortError"))});O[Q]=f?"cors":"basic";c(O)}catch(Ea){d(Ea)}}};g.onerror=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};g.ontimeout=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};
-g.open(q.method,q.url,!0);"include"===q.credentials&&(g.withCredentials=!0);q.headers.forEach(function(m,O){g.setRequestHeader(O,m)});var da=function(m){l||w.enqueue(m)},ea=function(m){f=m};null===q.body?g.fetch(da,ea,null):T(q).then(function(m){g.fetch(da,ea,m)})})};h.fetch.polyfill=!0}})(this);
\ No newline at end of file
+this[N]=a.redirect,d||null===a.body||(d=a.body,a[U]=!0),f=a[F];else{this[C]=String(a);if(!FetchInternal.isUrlValid(this[C],!1))throw new e("Invalid request URL");this[t]="cors";this[H]="same-origin"}if(void 0!==b.window&&null!==b.window)throw new e("Invalid request window");this[G]=b.cache||this[G]||"default";if(-1===ua.indexOf(this[G]))throw new e("Invalid request cache mode");this[H]=b.credentials||this[H]||"same-origin";if(-1===va.indexOf(this[H]))throw new e("Invalid request credentials");void 0!==
+b.integrity?this[I]=b.integrity:void 0===this[I]&&(this[I]="");a=(b.method||this[D]||"GET").toUpperCase();if(-1===wa.indexOf(a))throw new e("Invalid request method");this[D]=a;if(b.mode&&-1===xa.indexOf(b.mode))throw new e("Invalid request mode");this[t]=b.mode||this[t]||"no-cors";if("no-cors"===this[t]){if(-1===ya.indexOf(this[D]))throw new e("Invalid request method for no-cors");if(""!==this[I])throw new e("Request integrity data is not allowed with no-cors");}if("same-origin"!==this[t]&&"only-if-cached"===
+this[G])throw new e("Request mode must be same-origin for only-if-cached");this[N]=b.redirect||this[N]||"follow";if(-1===za.indexOf(this[N]))throw new e("Invalid request redirect mode");this[x]="no-cors"===this[t]?L(l,ma):L(l,ka);if(("GET"===this[D]||"HEAD"===this[D])&&d)throw new e("Request body is not allowed for GET or HEAD");"signal"in b&&(f=b.signal);f&&this[F].follow(f);this._initBody(d)}function Aa(a,b){var c=L(void 0,b);a.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(d){var l=
+d.split(":");if(d=l.shift().trim())l=l.join(":").trim(),c.append(d,l)});return c}function u(a,b){b||={};this[Q]="default";this[y]="status"in b?b.status:200;if(200>this[y]||599<this[y])throw new ba("Invalid response status");this[ca]=200<=this[y]&&300>this[y];if("statusText"in b){var c=b.statusText;for(var d=0,l=c.length,f;d<l;d++)if(f=c.charCodeAt(d),9!==f&&(32>f||255<f||127===f))throw e("Invalid response status text");}else c="OK";this[R]=c;this[x]=L(b.headers,S);this[C]=b.url||"";if(a&&-1<da.indexOf(this[y]))throw new e("Response body is not allowed with a null body status");
+this[E]=b.is_aborted||!1;this._initBody(a)}if(!h.fetch){var K=h.Array,aa=h.ArrayBuffer,qa=h.Object.create,W=h.Object.defineProperties,k=h.Symbol,Ba=k.iterator,P=h.Map,ba=h.RangeError,e=h.TypeError,v=h.Uint8Array,z=h.Promise,V=h.ReadableStream,ea=h.ReadableStreamTee,ta=h.IsReadableStreamDisturbed,ra=h.IsReadableStreamLocked,r=k("body"),U=k("bodyUsed"),G=k("cache"),H=k("credentials"),A=k("guardCallback"),x=k("headers"),I=k("integrity"),p=k("map"),D=k("method"),t=k("mode"),ca=k("ok"),N=k("redirect"),
+y=k("status"),R=k("statusText"),Q=k("type"),C=k("url"),E=k("is_aborted"),F=k("signal"),la="accept-charset accept-encoding access-control-request-headers access-control-request-method connection content-length cookie cookie2 date dnt expect host keep-alive origin referer te trailer transfer-encoding upgrade via".split(" "),pa=["set-cookie","set-cookie2"],na=["accept","accept-language","content-language"],oa=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],ua="default no-store reload no-cache force-cache only-if-cached".split(" "),
+va=["omit","same-origin","include"],wa="DELETE GET HEAD OPTIONS POST PUT".split(" "),ya=["GET","HEAD","POST"],xa=["same-origin","no-cors","cors"],za=["follow","error","manual"],da=[101,204,205,304],Ca=[301,302,303,307,308],Da="[object Int8Array];[object Uint8Array];[object Uint8ClampedArray];[object Int16Array];[object Uint16Array];[object Int32Array];[object Uint32Array];[object Float32Array];[object Float64Array]".split(";"),sa=aa.isView||function(a){return a&&-1<Da.indexOf(Object.prototype.toString.call(a))};
+n.prototype.append=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to append");a=J(a);b=Y(b);this[A](a,b)||(this[p].has(a)?this[p].set(a,this[p].get(a)+", "+b):this[p].set(a,b))};n.prototype["delete"]=function(a){if(1!==arguments.length)throw e("Invalid parameters to delete");this[A](a,"invalid")||this[p].delete(J(a))};n.prototype.get=function(a){if(1!==arguments.length)throw e("Invalid parameters to get");a=J(a);var b=this[p].get(a);return void 0!==b?b:null};n.prototype.has=function(a){if(1!==
+arguments.length)throw e("Invalid parameters to has");return this[p].has(J(a))};n.prototype.set=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to set");a=J(a);b=Y(b);this[A](a,b)||this[p].set(a,b)};n.prototype.forEach=function(a,b){var c=this;K.from(this[p].entries()).sort().forEach(function(d){a.call(b,d[1],d[0],c)})};n.prototype.keys=function(){return(new P(K.from(this[p].entries()).sort())).keys()};n.prototype.values=function(){return(new P(K.from(this[p].entries()).sort())).values()};
+n.prototype.entries=function(){return(new P(K.from(this[p].entries()).sort())).entries()};n.prototype[Ba]=n.prototype.entries;B.prototype.clone=function(){var a=null;null!==this[r]&&(a=ea(this[r],!0),this[r]=a[0],a=a[1]);return new B(this,{cloneBody:a,signal:this[F]})};W(B.prototype,{cache:{get:function(){return this[G]}},credentials:{get:function(){return this[H]}},headers:{get:function(){return this[x]}},integrity:{get:function(){return this[I]}},method:{get:function(){return this[D]}},mode:{get:function(){return this[t]}},
+redirect:{get:function(){return this[N]}},url:{get:function(){return this[C]}},signal:{get:function(){return this[F]}}});Z.call(B.prototype);Z.call(u.prototype);u.prototype.clone=function(){var a=null;null!==this[r]&&(a=ea(this[r],!0),this[r]=a[0],a=a[1]);return new u(a,{status:this[y],statusText:this[R],headers:L(this[x],S),url:this[C],is_aborted:this[E]})};W(u.prototype,{headers:{get:function(){return this[x]}},ok:{get:function(){return this[ca]}},status:{get:function(){return this[y]}},statusText:{get:function(){return this[R]}},
+type:{get:function(){return this[Q]}},url:{get:function(){return this[C]}}});u.error=function(){var a=new u(null);a[x][A]=ia;a[Q]="error";a[y]=0;a[R]="";return a};u.redirect=function(a,b){if(!FetchInternal.isUrlValid(a,!0))throw new e("Invalid URL for response redirect");void 0===b&&(b=302);if(-1===Ca.indexOf(b))throw new ba("Invalid status code for response redirect");return new u(null,{status:b,headers:{location:a}})};h.Headers=n;h.Request=B;h.Response=u;h.fetch=function(a,b){return new z(function(c,
+d){var l=!1,f=!1,q=new B(a,b),g=new XMLHttpRequest,w=null;if(q.signal.aborted)return d(new DOMException("Aborted","AbortError"));var M=new V({start(m){w=m},cancel(m){l=!0;g.abort()}}),Ea=function(){if(!l){l=!0;M.cancel();if(w)try{ReadableStreamDefaultControllerError(w,new DOMException("Aborted","AbortError"))}catch(m){}setTimeout(function(){try{g.abort()}catch(m){}},0)}};g.onload=function(){w.close()};g.onreadystatechange=function(){if(g.readyState===g.HEADERS_RECEIVED){var m={status:g.status,statusText:g.statusText,
+headers:Aa(g.getAllResponseHeaders()||"",S)};m.url="responseURL"in g?g.responseURL:m.headers.get("X-Request-URL");try{let X=-1==da.indexOf(g.status);var O=new u(X?M:null,m);q[F].addEventListener("abort",()=>{O[E]=!0;Ea();d(new DOMException("Aborted","AbortError"))});O[Q]=f?"cors":"basic";c(O)}catch(X){d(X)}}};g.onerror=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};g.ontimeout=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};
+g.open(q.method,q.url,!0);"include"===q.credentials&&(g.withCredentials=!0);q.headers.forEach(function(m,O){g.setRequestHeader(O,m)});var fa=function(m){l||w.enqueue(m)},ha=function(m){f=m};null===q.body?g.fetch(fa,ha,null):T(q).then(function(m){g.fetch(fa,ha,m)})})};h.fetch.polyfill=!0}})(this);
diff --git a/cobalt/fetch/fetch.js b/cobalt/fetch/fetch.js
index 8c8162b..cae8152 100644
--- a/cobalt/fetch/fetch.js
+++ b/cobalt/fetch/fetch.js
@@ -854,7 +854,8 @@
                      xhr.responseURL : init.headers.get('X-Request-URL')
           try {
             // 6. Let responseObject be a new Response object
-            var response = new Response(responseStream, init)
+            let body_allowed = NULL_BODY_STATUSES.indexOf(xhr.status) == -1
+            var response = new Response(body_allowed ? responseStream : null, init)
             // 7. Let locallyAborted be false - done in response constructor
 
             request[SIGNAL_SLOT].addEventListener('abort',() => {
diff --git a/cobalt/h5vcc/BUILD.gn b/cobalt/h5vcc/BUILD.gn
index 188c6ed..c31a775 100644
--- a/cobalt/h5vcc/BUILD.gn
+++ b/cobalt/h5vcc/BUILD.gn
@@ -22,9 +22,6 @@
   if (enable_account_manager) {
     defines += [ "COBALT_ENABLE_ACCOUNT_MANAGER" ]
   }
-  if (enable_sso) {
-    defines += [ "COBALT_ENABLE_SSO" ]
-  }
 }
 
 static_library("h5vcc") {
@@ -79,11 +76,11 @@
     "//cobalt/cache",
     "//cobalt/configuration",
     "//cobalt/dom",
+    "//cobalt/media",
     "//cobalt/network",
     "//cobalt/persistent_storage:persistent_settings",
     "//cobalt/script",
     "//cobalt/speech",
-    "//cobalt/sso",
     "//cobalt/storage",
     "//cobalt/trace_event",
     "//cobalt/watchdog",
@@ -101,13 +98,6 @@
     ]
   }
 
-  if (enable_sso) {
-    sources += [
-      "h5vcc_sso.cc",
-      "h5vcc_sso.h",
-    ]
-  }
-
   if (sb_is_evergreen) {
     sources += [
       "h5vcc_updater.cc",
diff --git a/cobalt/h5vcc/h5vcc.cc b/cobalt/h5vcc/h5vcc.cc
index e88b901..6f2b436 100644
--- a/cobalt/h5vcc/h5vcc.cc
+++ b/cobalt/h5vcc/h5vcc.cc
@@ -20,7 +20,6 @@
 #endif
 
 #include "cobalt/persistent_storage/persistent_settings.h"
-#include "cobalt/sso/sso_interface.h"
 
 namespace cobalt {
 namespace h5vcc {
@@ -32,15 +31,13 @@
   c_val_ = new dom::CValView();
   crash_log_ = new H5vccCrashLog();
   runtime_ = new H5vccRuntime(settings.event_dispatcher);
-  settings_ = new H5vccSettings(
-      settings.set_media_source_setting_func, settings.network_module,
+  settings_ =
+      new H5vccSettings(settings.set_web_setting_func, settings.media_module,
+                        settings.network_module,
 #if SB_IS(EVERGREEN)
-      settings.updater_module,
+                        settings.updater_module,
 #endif
-      settings.user_agent_data, settings.global_environment);
-#if defined(COBALT_ENABLE_SSO)
-  sso_ = new H5vccSso();
-#endif
+                        settings.user_agent_data, settings.global_environment);
   storage_ =
       new H5vccStorage(settings.network_module, settings.persistent_settings);
   trace_event_ = new H5vccTraceEvent();
@@ -77,7 +74,6 @@
   tracer->Trace(crash_log_);
   tracer->Trace(runtime_);
   tracer->Trace(settings_);
-  tracer->Trace(sso_);
   tracer->Trace(storage_);
   tracer->Trace(system_);
   tracer->Trace(trace_event_);
diff --git a/cobalt/h5vcc/h5vcc.h b/cobalt/h5vcc/h5vcc.h
index 0dce20d..138a67b 100644
--- a/cobalt/h5vcc/h5vcc.h
+++ b/cobalt/h5vcc/h5vcc.h
@@ -27,7 +27,6 @@
 #include "cobalt/h5vcc/h5vcc_crash_log.h"
 #include "cobalt/h5vcc/h5vcc_runtime.h"
 #include "cobalt/h5vcc/h5vcc_settings.h"
-#include "cobalt/h5vcc/h5vcc_sso.h"
 #include "cobalt/h5vcc/h5vcc_storage.h"
 #include "cobalt/h5vcc/h5vcc_system.h"
 #include "cobalt/h5vcc/h5vcc_trace_event.h"
@@ -46,7 +45,8 @@
  public:
   struct Settings {
     Settings()
-        : network_module(NULL),
+        : media_module(NULL),
+          network_module(NULL),
 #if SB_IS(EVERGREEN)
           updater_module(NULL),
 #endif
@@ -55,7 +55,8 @@
           user_agent_data(NULL),
           global_environment(NULL) {
     }
-    H5vccSettings::SetMediaSourceSettingFunc set_media_source_setting_func;
+    H5vccSettings::SetSettingFunc set_web_setting_func;
+    media::MediaModule* media_module;
     network::NetworkModule* network_module;
 #if SB_IS(EVERGREEN)
     updater::UpdaterModule* updater_module;
@@ -81,9 +82,6 @@
   const scoped_refptr<H5vccCrashLog>& crash_log() const { return crash_log_; }
   const scoped_refptr<H5vccRuntime>& runtime() const { return runtime_; }
   const scoped_refptr<H5vccSettings>& settings() const { return settings_; }
-#if defined(COBALT_ENABLE_SSO)
-  const scoped_refptr<H5vccSso>& sso() const { return sso_; }
-#endif
   const scoped_refptr<H5vccStorage>& storage() const { return storage_; }
   const scoped_refptr<H5vccSystem>& system() const { return system_; }
   const scoped_refptr<H5vccTraceEvent>& trace_event() const {
@@ -104,7 +102,6 @@
   scoped_refptr<H5vccCrashLog> crash_log_;
   scoped_refptr<H5vccRuntime> runtime_;
   scoped_refptr<H5vccSettings> settings_;
-  scoped_refptr<H5vccSso> sso_;
   scoped_refptr<H5vccStorage> storage_;
   scoped_refptr<H5vccSystem> system_;
   scoped_refptr<H5vccTraceEvent> trace_event_;
diff --git a/cobalt/h5vcc/h5vcc.idl b/cobalt/h5vcc/h5vcc.idl
index 4428dda..9201a36 100644
--- a/cobalt/h5vcc/h5vcc.idl
+++ b/cobalt/h5vcc/h5vcc.idl
@@ -38,8 +38,6 @@
   readonly attribute CValView cVal;
   readonly attribute H5vccRuntime runtime;
   readonly attribute H5vccSettings settings;
-  [Conditional=COBALT_ENABLE_SSO]
-      readonly attribute H5vccSso sso;
   readonly attribute H5vccStorage storage;
   readonly attribute H5vccSystem system;
   readonly attribute H5vccTraceEvent traceEvent;
diff --git a/cobalt/h5vcc/h5vcc_crash_log.cc b/cobalt/h5vcc/h5vcc_crash_log.cc
index a240d92..27c6981 100644
--- a/cobalt/h5vcc/h5vcc_crash_log.cc
+++ b/cobalt/h5vcc/h5vcc_crash_log.cc
@@ -20,7 +20,7 @@
 #include "base/atomicops.h"
 #include "base/memory/singleton.h"
 #include "base/synchronization/lock.h"
-#include "cobalt/extension/crash_handler.h"
+#include "starboard/extension/crash_handler.h"
 
 #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
 #include STARBOARD_CORE_DUMP_HANDLER_INCLUDE
diff --git a/cobalt/h5vcc/h5vcc_platform_service.h b/cobalt/h5vcc/h5vcc_platform_service.h
index 6fd41ef..26ec4c4 100644
--- a/cobalt/h5vcc/h5vcc_platform_service.h
+++ b/cobalt/h5vcc/h5vcc_platform_service.h
@@ -22,12 +22,12 @@
 #include "base/message_loop/message_loop.h"
 #include "base/optional.h"
 #include "base/single_thread_task_runner.h"
-#include "cobalt/extension/platform_service.h"
 #include "cobalt/script/array_buffer.h"
 #include "cobalt/script/callback_function.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/wrappable.h"
 #include "cobalt/web/dom_exception.h"
+#include "starboard/extension/platform_service.h"
 
 namespace cobalt {
 namespace h5vcc {
diff --git a/cobalt/h5vcc/h5vcc_runtime.cc b/cobalt/h5vcc/h5vcc_runtime.cc
index ac392dd..faf56ce 100644
--- a/cobalt/h5vcc/h5vcc_runtime.cc
+++ b/cobalt/h5vcc/h5vcc_runtime.cc
@@ -14,14 +14,17 @@
 
 #include "cobalt/h5vcc/h5vcc_runtime.h"
 
-#include "base/synchronization/lock.h"
+#include <memory>
+#include <utility>
+
 #include "cobalt/base/deep_link_event.h"
 #include "cobalt/base/polymorphic_downcast.h"
 
 namespace cobalt {
 namespace h5vcc {
 H5vccRuntime::H5vccRuntime(base::EventDispatcher* event_dispatcher)
-    : event_dispatcher_(event_dispatcher) {
+    : event_dispatcher_(event_dispatcher),
+      message_loop_(base::MessageLoop::current()) {
   on_deep_link_ = new H5vccDeepLinkEventTarget(
       base::Bind(&H5vccRuntime::GetUnconsumedDeepLink, base::Unretained(this)));
   on_pause_ = new H5vccRuntimeEventTarget;
@@ -29,7 +32,7 @@
 
   DCHECK(event_dispatcher_);
   deep_link_event_callback_ =
-      base::Bind(&H5vccRuntime::OnDeepLinkEvent, base::Unretained(this));
+      base::Bind(&H5vccRuntime::OnEventForDeepLink, base::Unretained(this));
   event_dispatcher_->AddEventCallback(base::DeepLinkEvent::TypeId(),
                                       deep_link_event_callback_);
 }
@@ -40,7 +43,7 @@
 }
 
 std::string H5vccRuntime::initial_deep_link() {
-  base::AutoLock auto_lock(lock_);
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (consumed_callback_) {
     std::move(consumed_callback_).Run();
   }
@@ -51,7 +54,7 @@
 }
 
 const std::string& H5vccRuntime::GetUnconsumedDeepLink() {
-  base::AutoLock auto_lock(lock_);
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (!initial_deep_link_.empty() && consumed_callback_) {
     LOG(INFO) << "Returning stashed unconsumed deep link: "
               << initial_deep_link_;
@@ -81,11 +84,22 @@
   tracer->Trace(on_resume_);
 }
 
-void H5vccRuntime::OnDeepLinkEvent(const base::Event* event) {
-  base::AutoLock auto_lock(lock_);
-  const base::DeepLinkEvent* deep_link_event =
-      base::polymorphic_downcast<const base::DeepLinkEvent*>(event);
-  const std::string& link = deep_link_event->link();
+void H5vccRuntime::OnEventForDeepLink(const base::Event* event) {
+  std::unique_ptr<base::DeepLinkEvent> deep_link_event(
+      new base::DeepLinkEvent(event));
+  if (base::MessageLoop::current() != message_loop_) {
+    message_loop_->task_runner()->PostTask(
+        FROM_HERE,
+        base::Bind(&H5vccRuntime::OnDeepLinkEvent, base::Unretained(this),
+                   base::Passed(&deep_link_event)));
+    return;
+  }
+  OnDeepLinkEvent(std::move(deep_link_event));
+}
+
+void H5vccRuntime::OnDeepLinkEvent(std::unique_ptr<base::DeepLinkEvent> event) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  const std::string& link = event->link();
   if (link.empty()) {
     return;
   }
@@ -94,13 +108,13 @@
     // Store the link for later consumption.
     LOG(INFO) << "Stashing deep link: " << link;
     initial_deep_link_ = link;
-    consumed_callback_ = deep_link_event->callback();
+    consumed_callback_ = event->callback();
   } else {
     // Send the link.
     DCHECK(consumed_callback_.is_null());
     LOG(INFO) << "Dispatching deep link event: " << link;
     on_deep_link()->DispatchEvent(link);
-    base::OnceClosure callback(deep_link_event->callback());
+    base::OnceClosure callback(event->callback());
     if (callback) {
       std::move(callback).Run();
     }
diff --git a/cobalt/h5vcc/h5vcc_runtime.h b/cobalt/h5vcc/h5vcc_runtime.h
index 88be673..560543f 100644
--- a/cobalt/h5vcc/h5vcc_runtime.h
+++ b/cobalt/h5vcc/h5vcc_runtime.h
@@ -15,9 +15,12 @@
 #ifndef COBALT_H5VCC_H5VCC_RUNTIME_H_
 #define COBALT_H5VCC_H5VCC_RUNTIME_H_
 
+#include <memory>
 #include <string>
 
 #include "base/callback.h"
+#include "base/threading/thread_checker.h"
+#include "cobalt/base/deep_link_event.h"
 #include "cobalt/base/event_dispatcher.h"
 #include "cobalt/h5vcc/h5vcc_deep_link_event_target.h"
 #include "cobalt/h5vcc/h5vcc_runtime_event_target.h"
@@ -41,7 +44,8 @@
 
  private:
   // Called by the event dispatcher to handle deep link events.
-  void OnDeepLinkEvent(const base::Event* event);
+  void OnEventForDeepLink(const base::Event* event);
+  void OnDeepLinkEvent(std::unique_ptr<base::DeepLinkEvent> event);
 
   // Returns the initial deep link if it's unconsumed.
   const std::string& GetUnconsumedDeepLink();
@@ -59,7 +63,13 @@
   base::EventCallback deep_link_event_callback_;
   base::OnceClosure consumed_callback_;
 
-  base::Lock lock_;
+  // Track the message loop that created this object so deep link events are
+  // handled from the same thread.
+  base::MessageLoop* message_loop_;
+
+  // Thread checker ensures all calls to DOM element are made from the same
+  // thread that it is created in.
+  THREAD_CHECKER(thread_checker_);
 
   DISALLOW_COPY_AND_ASSIGN(H5vccRuntime);
 };
diff --git a/cobalt/h5vcc/h5vcc_settings.cc b/cobalt/h5vcc/h5vcc_settings.cc
index 1735270..1eef9e6 100644
--- a/cobalt/h5vcc/h5vcc_settings.cc
+++ b/cobalt/h5vcc/h5vcc_settings.cc
@@ -19,15 +19,16 @@
 namespace cobalt {
 namespace h5vcc {
 
-H5vccSettings::H5vccSettings(
-    const SetMediaSourceSettingFunc& set_media_source_setting_func,
-    cobalt::network::NetworkModule* network_module,
+H5vccSettings::H5vccSettings(const SetSettingFunc& set_web_setting_func,
+                             cobalt::media::MediaModule* media_module,
+                             cobalt::network::NetworkModule* network_module,
 #if SB_IS(EVERGREEN)
-    cobalt::updater::UpdaterModule* updater_module,
+                             cobalt::updater::UpdaterModule* updater_module,
 #endif
-    web::NavigatorUAData* user_agent_data,
-    script::GlobalEnvironment* global_environment)
-    : set_media_source_setting_func_(set_media_source_setting_func),
+                             web::NavigatorUAData* user_agent_data,
+                             script::GlobalEnvironment* global_environment)
+    : set_web_setting_func_(set_web_setting_func),
+      media_module_(media_module),
       network_module_(network_module),
 #if SB_IS(EVERGREEN)
       updater_module_(updater_module),
@@ -37,6 +38,7 @@
 }
 
 bool H5vccSettings::Set(const std::string& name, int32 value) const {
+  const char kMediaPrefix[] = "Media.";
   const char kNavigatorUAData[] = "NavigatorUAData";
   const char kQUIC[] = "QUIC";
 
@@ -44,11 +46,16 @@
   const char kUpdaterMinFreeSpaceBytes[] = "Updater.MinFreeSpaceBytes";
 #endif
 
-  if (set_media_source_setting_func_ &&
-      set_media_source_setting_func_.Run(name, value)) {
+  if (set_web_setting_func_ && set_web_setting_func_.Run(name, value)) {
     return true;
   }
 
+  if (name.rfind(kMediaPrefix, 0) == 0) {
+    return media_module_ ? media_module_->SetConfiguration(
+                               name.substr(strlen(kMediaPrefix)), value)
+                         : false;
+  }
+
   if (name.compare(kNavigatorUAData) == 0 && value == 1) {
     global_environment_->BindTo("userAgentData", user_agent_data_, "navigator");
     return true;
diff --git a/cobalt/h5vcc/h5vcc_settings.h b/cobalt/h5vcc/h5vcc_settings.h
index a73e59f..83a3adb 100644
--- a/cobalt/h5vcc/h5vcc_settings.h
+++ b/cobalt/h5vcc/h5vcc_settings.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "cobalt/media/media_module.h"
 #include "cobalt/network/network_module.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/wrappable.h"
@@ -35,9 +36,10 @@
 class H5vccSettings : public script::Wrappable {
  public:
   typedef base::Callback<bool(const std::string& name, int value)>
-      SetMediaSourceSettingFunc;
+      SetSettingFunc;
 
-  H5vccSettings(const SetMediaSourceSettingFunc& set_media_source_setting_func,
+  H5vccSettings(const SetSettingFunc& set_web_setting_func,
+                cobalt::media::MediaModule* media_module,
                 cobalt::network::NetworkModule* network_module,
 #if SB_IS(EVERGREEN)
                 cobalt::updater::UpdaterModule* updater_module,
@@ -53,7 +55,8 @@
   DEFINE_WRAPPABLE_TYPE(H5vccSettings);
 
  private:
-  const SetMediaSourceSettingFunc set_media_source_setting_func_;
+  const SetSettingFunc set_web_setting_func_;
+  cobalt::media::MediaModule* media_module_ = nullptr;
   cobalt::network::NetworkModule* network_module_ = nullptr;
 #if SB_IS(EVERGREEN)
   cobalt::updater::UpdaterModule* updater_module_ = nullptr;
diff --git a/cobalt/h5vcc/h5vcc_sso.cc b/cobalt/h5vcc/h5vcc_sso.cc
deleted file mode 100644
index bb94e73..0000000
--- a/cobalt/h5vcc/h5vcc_sso.cc
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/h5vcc/h5vcc_sso.h"
-
-namespace cobalt {
-namespace h5vcc {
-
-H5vccSso::H5vccSso() : sso_(std::move(sso::CreateSSO())) {}
-
-std::string H5vccSso::GetApiKey() {
-  DCHECK(sso_);
-  return sso_->getApiKey();
-}
-
-std::string H5vccSso::GetOauthClientId() {
-  DCHECK(sso_);
-  return sso_->getOauthClientId();
-}
-
-std::string H5vccSso::GetOauthClientSecret() {
-  DCHECK(sso_);
-  return sso_->getOauthClientSecret();
-}
-
-}  // namespace h5vcc
-}  // namespace cobalt
diff --git a/cobalt/h5vcc/h5vcc_sso.h b/cobalt/h5vcc/h5vcc_sso.h
deleted file mode 100644
index 4755f7f..0000000
--- a/cobalt/h5vcc/h5vcc_sso.h
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_H5VCC_H5VCC_SSO_H_
-#define COBALT_H5VCC_H5VCC_SSO_H_
-
-#include <memory>
-#include <string>
-
-#include "cobalt/script/wrappable.h"
-#include "cobalt/sso/sso_interface.h"
-
-namespace cobalt {
-namespace h5vcc {
-
-class H5vccSso : public script::Wrappable {
- public:
-  H5vccSso();
-
-  std::string GetApiKey();
-  std::string GetOauthClientId();
-  std::string GetOauthClientSecret();
-
-  DEFINE_WRAPPABLE_TYPE(H5vccSso);
-
- private:
-  std::unique_ptr<sso::SsoInterface> sso_;
-
-  DISALLOW_COPY_AND_ASSIGN(H5vccSso);
-};
-
-}  // namespace h5vcc
-}  // namespace cobalt
-
-#endif  // COBALT_H5VCC_H5VCC_SSO_H_
diff --git a/cobalt/h5vcc/h5vcc_sso.idl b/cobalt/h5vcc/h5vcc_sso.idl
deleted file mode 100644
index 6ac9757..0000000
--- a/cobalt/h5vcc/h5vcc_sso.idl
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-[
-    Conditional=COBALT_ENABLE_SSO,
-] interface H5vccSso {
-    DOMString getApiKey();
-    DOMString getOauthClientId();
-    DOMString getOauthClientSecret();
-};
diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc
index cecdd0c..301c0f2 100644
--- a/cobalt/h5vcc/h5vcc_storage.cc
+++ b/cobalt/h5vcc/h5vcc_storage.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "cobalt/h5vcc/h5vcc_storage.h"
+
 #include <algorithm>
 #include <memory>
 #include <string>
@@ -21,7 +23,6 @@
 #include "base/files/file_util.h"
 #include "base/values.h"
 #include "cobalt/cache/cache.h"
-#include "cobalt/h5vcc/h5vcc_storage.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "cobalt/storage/storage_manager.h"
 #include "net/base/completion_once_callback.h"
@@ -29,7 +30,6 @@
 #include "net/disk_cache/cobalt/resource_type.h"
 #include "net/http/http_cache.h"
 #include "net/http/http_transaction_factory.h"
-
 #include "starboard/common/file.h"
 #include "starboard/common/string.h"
 
@@ -40,9 +40,7 @@
 
 const char kTestFileName[] = "cache_test_file.json";
 
-const uint32 kWriteBufferSize = 1024 * 1024;
-
-const uint32 kReadBufferSize = 1024 * 1024;
+const uint32 kBufferSize = 16384;  // 16 KB
 
 H5vccStorageWriteTestResponse WriteTestResponse(std::string error = "",
                                                 uint32 bytes_written = 0) {
@@ -157,13 +155,13 @@
   write_buf.append(test_string.substr(0, test_size % test_string.length()));
 
   // Incremental Writes of test_data, copies SbWriteAll, using a maximum
-  // kWriteBufferSize per write.
+  // kBufferSize per write.
   uint32 total_bytes_written = 0;
 
   do {
-    auto bytes_written = test_file.Write(
-        write_buf.data() + total_bytes_written,
-        std::min(kWriteBufferSize, test_size - total_bytes_written));
+    auto bytes_written =
+        test_file.Write(write_buf.data() + total_bytes_written,
+                        std::min(kBufferSize, test_size - total_bytes_written));
     if (bytes_written <= 0) {
       SbFileDelete(test_file_path.c_str());
       return WriteTestResponse("SbWrite -1 return value error");
@@ -196,21 +194,21 @@
   }
 
   // Incremental Reads of test_data, copies SbReadAll, using a maximum
-  // kReadBufferSize per write.
+  // kBufferSize per write.
   uint32 total_bytes_read = 0;
 
   do {
-    char read_buf[kReadBufferSize];
+    auto read_buffer = std::make_unique<char[]>(kBufferSize);
     auto bytes_read = test_file.Read(
-        read_buf, std::min(kReadBufferSize, test_size - total_bytes_read));
+        read_buffer.get(), std::min(kBufferSize, test_size - total_bytes_read));
     if (bytes_read <= 0) {
       SbFileDelete(test_file_path.c_str());
       return VerifyTestResponse("SbRead -1 return value error");
     }
 
-    // Verify read_buf equivalent to a repeated test_string.
+    // Verify read_buffer equivalent to a repeated test_string.
     for (auto i = 0; i < bytes_read; ++i) {
-      if (read_buf[i] !=
+      if (read_buffer.get()[i] !=
           test_string[(total_bytes_read + i) % test_string.size()]) {
         return VerifyTestResponse(
             "File test data does not match with test data string");
@@ -235,7 +233,7 @@
   if (!quota.has_other() || !quota.has_html() || !quota.has_css() ||
       !quota.has_image() || !quota.has_font() || !quota.has_splash() ||
       !quota.has_uncompiled_js() || !quota.has_compiled_js() ||
-      !quota.has_cache_api()) {
+      !quota.has_cache_api() || !quota.has_service_worker_js()) {
     return SetQuotaResponse(
         "H5vccStorageResourceTypeQuotaBytesDictionary input parameter missing "
         "required fields.");
@@ -244,7 +242,7 @@
   if (quota.other() < 0 || quota.html() < 0 || quota.css() < 0 ||
       quota.image() < 0 || quota.font() < 0 || quota.splash() < 0 ||
       quota.uncompiled_js() < 0 || quota.compiled_js() < 0 ||
-      quota.cache_api() < 0) {
+      quota.cache_api() < 0 || quota.service_worker_js() < 0) {
     return SetQuotaResponse(
         "H5vccStorageResourceTypeQuotaBytesDictionary input parameter fields "
         "cannot have a negative value.");
@@ -253,7 +251,7 @@
   auto quota_total = quota.other() + quota.html() + quota.css() +
                      quota.image() + quota.font() + quota.splash() +
                      quota.uncompiled_js() + quota.compiled_js() +
-                     quota.cache_api();
+                     quota.cache_api() + quota.service_worker_js();
 
   uint32_t max_quota_size = 24 * 1024 * 1024;
 #if SB_API_VERSION >= 14
@@ -291,6 +289,8 @@
                             static_cast<uint32_t>(quota.compiled_js()));
   SetAndSaveQuotaForBackend(disk_cache::kCacheApi,
                             static_cast<uint32_t>(quota.cache_api()));
+  SetAndSaveQuotaForBackend(disk_cache::kServiceWorkerScript,
+                            static_cast<uint32_t>(quota.service_worker_js()));
   return SetQuotaResponse("", true);
 }
 
@@ -328,6 +328,8 @@
           ->GetMaxCacheStorageInBytes(disk_cache::kCompiledScript)
           .value());
   quota.set_cache_api(cache_backend_->GetQuota(disk_cache::kCacheApi));
+  quota.set_service_worker_js(
+      cache_backend_->GetQuota(disk_cache::kServiceWorkerScript));
 
   uint32_t max_quota_size = 24 * 1024 * 1024;
 #if SB_API_VERSION >= 14
diff --git a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
index 322f82d..c23a1b8 100644
--- a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
+++ b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
@@ -22,5 +22,6 @@
   unsigned long uncompiled_js;
   unsigned long compiled_js;
   unsigned long cache_api;
+  unsigned long service_worker_js;
   unsigned long total;
 };
diff --git a/cobalt/layout/BUILD.gn b/cobalt/layout/BUILD.gn
index 5070a65..b95057e 100644
--- a/cobalt/layout/BUILD.gn
+++ b/cobalt/layout/BUILD.gn
@@ -117,6 +117,7 @@
     "//cobalt/render_tree",
     "//cobalt/render_tree:animations",
     "//cobalt/ui_navigation",
+    "//cobalt/ui_navigation/scroll_engine",
     "//cobalt/web_animations",
     "//third_party/icu:icuuc",
   ]
diff --git a/cobalt/layout/box.cc b/cobalt/layout/box.cc
index a42730d..2e48d7d 100644
--- a/cobalt/layout/box.cc
+++ b/cobalt/layout/box.cc
@@ -60,8 +60,8 @@
 using cobalt::render_tree::RoundedCorner;
 using cobalt::render_tree::RoundedCorners;
 using cobalt::render_tree::ViewportFilter;
-using cobalt::render_tree::animations::Animation;
 using cobalt::render_tree::animations::AnimateNode;
+using cobalt::render_tree::animations::Animation;
 
 namespace cobalt {
 namespace layout {
@@ -258,11 +258,13 @@
 }
 
 RectLayoutUnit Box::GetTransformedBoxFromRootWithScroll(
-    const RectLayoutUnit& box_from_margin_box) const {
+    const RectLayoutUnit& box_from_margin_box,
+    bool transform_forms_root) const {
   // Get the transformed box from root while factoring in scrollLeft and
   // scrollTop of intermediate containers.
   return GetTransformedBox(
-      GetMarginBoxTransformFromContainingBlockWithScroll(nullptr),
+      GetMarginBoxTransformFromContainingBlockWithScroll(
+          nullptr, transform_forms_root /* transform_forms_root */),
       box_from_margin_box);
 }
 
@@ -383,38 +385,51 @@
 }
 
 math::Matrix3F Box::GetMarginBoxTransformFromContainingBlockInternal(
-    const ContainerBox* containing_block, bool include_scroll) const {
+    const ContainerBox* containing_block, bool transform_forms_root,
+    bool include_scroll) const {
   math::Matrix3F transform = math::Matrix3F::Identity();
   if (this == containing_block) {
     return transform;
   }
 
+  bool only_apply_scroll_transform = false;
+
   // Walk up the containing block tree to build the transform matrix.
   // The logic is similar to using ApplyTransformActionToCoordinate with exit
   // transform but a matrix is calculated instead; logic analogous to
   // GetMarginBoxOffsetFromRoot is also factored in.
   for (const Box* box = this;;) {
     // Factor in the margin box offset.
-    transform =
-        math::TranslateMatrix(
-            box->margin_box_offset_from_containing_block().x().toFloat(),
-            box->margin_box_offset_from_containing_block().y().toFloat()) *
-        transform;
+    if (!only_apply_scroll_transform) {
+      transform =
+          math::TranslateMatrix(
+              box->margin_box_offset_from_containing_block().x().toFloat(),
+              box->margin_box_offset_from_containing_block().y().toFloat()) *
+          transform;
+    }
 
     // Factor in the box's transform.
     if (box->IsTransformed()) {
-      Vector2dLayoutUnit transform_rect_offset =
-          box->margin_box_offset_from_containing_block() +
-          box->GetBorderBoxOffsetFromMarginBox();
-      transform =
-          GetCSSTransform(box->computed_style()->transform().get(),
-                          box->computed_style()->transform_origin().get(),
-                          math::RectF(transform_rect_offset.x().toFloat(),
-                                      transform_rect_offset.y().toFloat(),
-                                      box->GetBorderBoxWidth().toFloat(),
-                                      box->GetBorderBoxHeight().toFloat()),
-                          box->ComputeUiNavFocusForTransform()) *
-          transform;
+      if (transform_forms_root) {
+        if (!include_scroll) {
+          break;
+        }
+        only_apply_scroll_transform = true;
+      }
+      if (!only_apply_scroll_transform) {
+        Vector2dLayoutUnit transform_rect_offset =
+            box->margin_box_offset_from_containing_block() +
+            box->GetBorderBoxOffsetFromMarginBox();
+        transform =
+            GetCSSTransform(box->computed_style()->transform().get(),
+                            box->computed_style()->transform_origin().get(),
+                            math::RectF(transform_rect_offset.x().toFloat(),
+                                        transform_rect_offset.y().toFloat(),
+                                        box->GetBorderBoxWidth().toFloat(),
+                                        box->GetBorderBoxHeight().toFloat()),
+                            box->ComputeUiNavFocusForTransform()) *
+            transform;
+      }
     }
 
     const ContainerBox* container = box->GetContainingBlock();
@@ -422,16 +437,18 @@
       break;
     }
 
-    // Convert the transform into the container's coordinate space.
-    Vector2dLayoutUnit containing_block_offset =
-        box->GetContainingBlockOffsetFromItsContentBox(container) +
-        container->GetContentBoxOffsetFromMarginBox();
-    transform = math::TranslateMatrix(containing_block_offset.x().toFloat(),
-                                      containing_block_offset.y().toFloat()) *
-                transform;
+    if (!only_apply_scroll_transform) {
+      // Convert the transform into the container's coordinate space.
+      Vector2dLayoutUnit containing_block_offset =
+          box->GetContainingBlockOffsetFromItsContentBox(container) +
+          container->GetContentBoxOffsetFromMarginBox();
+      transform = math::TranslateMatrix(containing_block_offset.x().toFloat(),
+                                        containing_block_offset.y().toFloat()) *
+                  transform;
+    }
 
     // Factor in the container's scrollLeft / scrollTop as needed.
-    if (include_scroll && container->ui_nav_item_ &&
+    if (container && include_scroll && container->ui_nav_item_ &&
         container->ui_nav_item_->IsContainer()) {
       float left, top;
       container->ui_nav_item_->GetContentOffset(&left, &top);
@@ -447,13 +464,14 @@
 math::Matrix3F Box::GetMarginBoxTransformFromContainingBlock(
     const ContainerBox* containing_block) const {
   return GetMarginBoxTransformFromContainingBlockInternal(
-      containing_block, false /* include_scroll */);
+      containing_block, false /* transform_forms_root */,
+      false /* include_scroll */);
 }
 
 math::Matrix3F Box::GetMarginBoxTransformFromContainingBlockWithScroll(
-    const ContainerBox* containing_block) const {
+    const ContainerBox* containing_block, bool transform_forms_root) const {
   return GetMarginBoxTransformFromContainingBlockInternal(
-      containing_block, true /* include_scroll */);
+      containing_block, transform_forms_root, true /* include_scroll */);
 }
 
 Vector2dLayoutUnit Box::GetMarginBoxOffsetFromRoot(
@@ -535,6 +553,14 @@
   return padding_top() + height() + padding_bottom();
 }
 
+RectLayoutUnit Box::GetClampedPaddingBox(bool transform_forms_root) const {
+  auto padding_box_offset = GetPaddingBoxOffsetFromRoot(transform_forms_root);
+  auto clamped_padding_box = GetClampedPaddingBoxSize();
+  return RectLayoutUnit(padding_box_offset.x(), padding_box_offset.y(),
+                        clamped_padding_box.width(),
+                        clamped_padding_box.height());
+}
+
 SizeLayoutUnit Box::GetClampedPaddingBoxSize() const {
   // Padding box size depends on the content and padding areas
   // Its dimensions cannot be negative because the content and padding areas
@@ -1211,7 +1237,9 @@
 }
 
 bool Box::IsUnderCoordinate(const Vector2dLayoutUnit& coordinate) const {
-  RectLayoutUnit rect = GetBorderBoxFromRoot(true /*transform_forms_root*/);
+  RectLayoutUnit rect = GetTransformedBoxFromRootWithScroll(
+      GetBorderBoxFromMarginBox(), true /* transform_forms_root */);
+
   bool res =
       coordinate.x() >= rect.x() && coordinate.x() <= rect.x() + rect.width() &&
       coordinate.y() >= rect.y() && coordinate.y() <= rect.y() + rect.height();
diff --git a/cobalt/layout/box.h b/cobalt/layout/box.h
index a36935a..9c17a87 100644
--- a/cobalt/layout/box.h
+++ b/cobalt/layout/box.h
@@ -281,7 +281,8 @@
   RectLayoutUnit GetTransformedBoxFromRoot(
       const RectLayoutUnit& box_from_margin_box) const;
   RectLayoutUnit GetTransformedBoxFromRootWithScroll(
-      const RectLayoutUnit& box_from_margin_box) const;
+      const RectLayoutUnit& box_from_margin_box,
+      bool transform_forms_root = false) const;
   RectLayoutUnit GetTransformedBoxFromContainingBlock(
       const ContainerBox* containing_block,
       const RectLayoutUnit& box_from_margin_box) const;
@@ -372,7 +373,7 @@
   math::Matrix3F GetMarginBoxTransformFromContainingBlock(
       const ContainerBox* containing_block) const;
   math::Matrix3F GetMarginBoxTransformFromContainingBlockWithScroll(
-      const ContainerBox* containing_block) const;
+      const ContainerBox* containing_block, bool transform_forms_root) const;
 
   Vector2dLayoutUnit GetMarginBoxOffsetFromRoot(
       bool transform_forms_root) const;
@@ -412,6 +413,7 @@
   LayoutUnit padding_bottom() const { return padding_insets_.bottom(); }
   LayoutUnit GetPaddingBoxWidth() const;
   LayoutUnit GetPaddingBoxHeight() const;
+  RectLayoutUnit GetClampedPaddingBox(bool transform_forms_root) const;
   SizeLayoutUnit GetClampedPaddingBoxSize() const;
 
   RectLayoutUnit GetPaddingBoxFromMarginBox() const;
@@ -861,7 +863,8 @@
   // Get the transform for this box from the specified containing block (which
   // may be null to indicate root).
   math::Matrix3F GetMarginBoxTransformFromContainingBlockInternal(
-      const ContainerBox* containing_block, bool include_scroll) const;
+      const ContainerBox* containing_block, bool transform_forms_root,
+      bool include_scroll) const;
 
   // Some custom CSS transform functions require a UI navigation focus item as
   // input. This computes the appropriate UI navigation item for this box's
diff --git a/cobalt/layout/layout.cc b/cobalt/layout/layout.cc
index bd9ee75..07bdcc8 100644
--- a/cobalt/layout/layout.cc
+++ b/cobalt/layout/layout.cc
@@ -24,7 +24,6 @@
 #include "cobalt/dom/html_body_element.h"
 #include "cobalt/dom/html_element_context.h"
 #include "cobalt/dom/html_html_element.h"
-#include "cobalt/extension/graphics.h"
 #include "cobalt/layout/benchmark_stat_names.h"
 #include "cobalt/layout/box_generator.h"
 #include "cobalt/layout/initial_containing_block.h"
@@ -32,6 +31,7 @@
 #include "cobalt/layout/used_style.h"
 #include "cobalt/render_tree/animations/animate_node.h"
 #include "cobalt/render_tree/matrix_transform_node.h"
+#include "starboard/extension/graphics.h"
 
 namespace cobalt {
 namespace layout {
diff --git a/cobalt/layout/topmost_event_target.cc b/cobalt/layout/topmost_event_target.cc
index c4a1b72..fec6280 100644
--- a/cobalt/layout/topmost_event_target.cc
+++ b/cobalt/layout/topmost_event_target.cc
@@ -14,10 +14,13 @@
 
 #include "cobalt/layout/topmost_event_target.h"
 
+#include <vector>
+
 #include "base/optional.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/token.h"
 #include "cobalt/base/tokens.h"
+#include "cobalt/cssom/computed_style_utils.h"
 #include "cobalt/cssom/keyword_value.h"
 #include "cobalt/dom/document.h"
 #include "cobalt/dom/html_element.h"
@@ -30,6 +33,8 @@
 #include "cobalt/dom/pointer_state.h"
 #include "cobalt/dom/ui_event.h"
 #include "cobalt/dom/wheel_event.h"
+#include "cobalt/layout/layout_unit.h"
+#include "cobalt/math/rect_f.h"
 #include "cobalt/math/vector2d.h"
 #include "cobalt/math/vector2d_f.h"
 #include "cobalt/web/event.h"
@@ -77,6 +82,82 @@
   return NULL;
 }
 
+scoped_refptr<dom::HTMLElement> FindFirstElementWithScrollType(
+    scoped_refptr<dom::HTMLElement> target_element,
+    ui_navigation::scroll_engine::ScrollType major_scroll_axis,
+    bool scrolling_right, bool scrolling_down) {
+  auto current_element = target_element;
+  bool scrolling_left = !scrolling_right;
+  bool scrolling_up = !scrolling_down;
+  bool horizontal_scroll_axis =
+      major_scroll_axis == ui_navigation::scroll_engine::ScrollType::Horizontal;
+  bool vertical_scroll_axis =
+      major_scroll_axis == ui_navigation::scroll_engine::ScrollType::Vertical;
+
+  while (true) {
+    float scroll_top_lower_bound;
+    float scroll_left_lower_bound;
+    float scroll_top_upper_bound;
+    float scroll_left_upper_bound;
+    float offset_x;
+    float offset_y;
+
+    if (!current_element->parent_element()) {
+      break;
+    }
+    current_element = current_element->parent_element()->AsHTMLElement();
+    auto current_ui_nav_item = current_element->GetUiNavItem();
+    if (!current_ui_nav_item) continue;
+
+    current_ui_nav_item->GetBounds(
+        &scroll_top_lower_bound, &scroll_left_lower_bound,
+        &scroll_top_upper_bound, &scroll_left_upper_bound);
+    current_ui_nav_item->GetContentOffset(&offset_x, &offset_y);
+
+    bool can_scroll_left = scroll_left_lower_bound < offset_x;
+    bool can_scroll_right = scroll_left_upper_bound > offset_x;
+    bool can_scroll_up = scroll_top_lower_bound < offset_y;
+    bool can_scroll_down = scroll_top_upper_bound > offset_y;
+
+    if ((scrolling_left && can_scroll_left && horizontal_scroll_axis) ||
+        (scrolling_right && can_scroll_right && horizontal_scroll_axis) ||
+        (scrolling_up && can_scroll_up && vertical_scroll_axis) ||
+        (scrolling_down && can_scroll_down && vertical_scroll_axis)) {
+      return current_element;
+    }
+  }
+  return nullptr;
+}
+
+bool TransformCanBeAppliedToBox(const Box* box, math::Vector2dF* coordinate) {
+  return !box->IsTransformed() || box->ApplyTransformActionToCoordinate(
+                                      Box::kEnterTransform, coordinate);
+}
+
+bool CoordinateCanTargetBox(const Box* box, math::Vector2dF* coordinate) {
+  if (!cssom::IsOverflowCropped(box->computed_style())) {
+    return true;
+  }
+  LayoutUnit coordinate_x(coordinate->x());
+  LayoutUnit coordinate_y(coordinate->y());
+
+  bool transform_forms_root = false;
+  auto padding_box = box->GetClampedPaddingBox(transform_forms_root);
+  return padding_box.Contains(coordinate_x, coordinate_y);
+}
+
+bool ShouldConsiderElementAndChildren(dom::Element* element,
+                                      math::Vector2dF* coordinate) {
+  LayoutBoxes* layout_boxes = GetLayoutBoxesIfNotEmpty(element);
+  const Box* box = layout_boxes->boxes().front();
+  if (!box->computed_style()) {
+    return true;
+  }
+
+  return TransformCanBeAppliedToBox(box, coordinate) &&
+         CoordinateCanTargetBox(box, coordinate);
+}
+
 }  // namespace
 void TopmostEventTarget::ConsiderElement(dom::Element* element,
                                          const math::Vector2dF& coordinate) {
@@ -84,16 +165,9 @@
   math::Vector2dF element_coordinate(coordinate);
   LayoutBoxes* layout_boxes = GetLayoutBoxesIfNotEmpty(element);
   if (layout_boxes) {
-    const Box* box = layout_boxes->boxes().front();
-    if (box->computed_style() && box->IsTransformed()) {
-      // Early out if the transform cannot be applied. This can occur if the
-      // transform matrix is not invertible.
-      if (!box->ApplyTransformActionToCoordinate(Box::kEnterTransform,
-                                                 &element_coordinate)) {
-        return;
-      }
+    if (!ShouldConsiderElementAndChildren(element, &element_coordinate)) {
+      return;
     }
-
     scoped_refptr<dom::HTMLElement> html_element = element->AsHTMLElement();
     if (html_element && html_element->CanBeDesignatedByPointerIfDisplayed()) {
       ConsiderBoxes(html_element, layout_boxes, element_coordinate);
@@ -129,6 +203,112 @@
   }
 }
 
+void TopmostEventTarget::CancelScrollsInParentNavItems(
+    scoped_refptr<dom::HTMLElement> target_element) {
+  // Cancel any scrolls in the tree.
+  std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel;
+  auto current_element = target_element;
+  while (true) {
+    if (!current_element->parent_element()) {
+      break;
+    }
+    current_element = current_element->parent_element()->AsHTMLElement();
+    auto current_ui_nav_item = current_element->GetUiNavItem();
+    if (current_ui_nav_item) {
+      scrolls_to_cancel.push_back(current_ui_nav_item);
+    }
+  }
+
+  scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::Bind(&ui_navigation::scroll_engine::ScrollEngine::
+                     CancelActiveScrollsForNavItems,
+                 base::Unretained(scroll_engine_), scrolls_to_cancel));
+}
+
+void TopmostEventTarget::HandleScrollState(
+    scoped_refptr<dom::HTMLElement> target_element,
+    const dom::PointerEvent* pointer_event, dom::PointerState* pointer_state,
+    dom::PointerEventInit* event_init) {
+  // On pointer down, cancel any scrolls happening for UI nav items above
+  // that element. Additionally, save the pointer coordinates.
+  //
+  // On pointer move, check if we've reached the threshold to start
+  // scrolling. If we have, find the first scroll container we can scroll.
+  // Then send that scroll container, initial pointer event coords, current
+  // pointer event coords, scroll direction.
+  bool pointer_type_is_accepted = pointer_event->pointer_type() == "mouse" ||
+                                  pointer_event->pointer_type() == "pen" ||
+                                  pointer_event->pointer_type() == "touch";
+  if (!scroll_engine_ || !pointer_event || !pointer_type_is_accepted) {
+    return;
+  }
+
+  bool should_clear_pointer_state =
+      pointer_event->type() == base::Tokens::pointerup();
+
+  auto pointer_id = pointer_event->pointer_id();
+  auto pointer_coordinates =
+      math::Vector2dF(pointer_event->client_x(), pointer_event->client_y());
+
+  if (pointer_event->type() == base::Tokens::pointerdown()) {
+    CancelScrollsInParentNavItems(target_element);
+    pointer_state->SetClientCoordinates(pointer_id, pointer_coordinates);
+    pointer_state->SetClientTimeStamp(pointer_id, pointer_event->time_stamp());
+    return;
+  }
+
+  auto initial_coordinates = pointer_state->GetClientCoordinates(pointer_id);
+  auto initial_time_stamp = pointer_state->GetClientTimeStamp(pointer_id);
+  if (pointer_event->type() == base::Tokens::pointermove() &&
+      initial_coordinates.has_value() && initial_time_stamp.has_value()) {
+    cobalt::math::Vector2dF drag_vector =
+        initial_coordinates.value() - pointer_coordinates;
+    float x = drag_vector.x();
+    float y = drag_vector.y();
+
+    if (drag_vector.Length() >=
+        ui_navigation::scroll_engine::kDragDistanceThreshold) {
+      // Get major scroll direction.
+      ui_navigation::scroll_engine::ScrollType scroll_type =
+          std::abs(x) > std::abs(y)
+              ? ui_navigation::scroll_engine::ScrollType::Horizontal
+              : ui_navigation::scroll_engine::ScrollType::Vertical;
+      auto element_to_scroll = FindFirstElementWithScrollType(
+          target_element, scroll_type, x > 0, y > 0);
+      if (!element_to_scroll) {
+        return;
+      }
+
+      const scoped_refptr<dom::Window>& view = event_init->view();
+      element_to_scroll->DispatchEvent(new dom::PointerEvent(
+          base::Tokens::pointercancel(), web::Event::kBubbles,
+          web::Event::kNotCancelable, view, *event_init));
+      element_to_scroll->DispatchEvent(
+          new dom::PointerEvent(base::Tokens::pointerout(), view, *event_init));
+      element_to_scroll->DispatchEvent(new dom::PointerEvent(
+          base::Tokens::pointerleave(), web::Event::kNotBubbles,
+          web::Event::kNotCancelable, view, *event_init));
+      pointer_state->SetWasCancelled(pointer_id);
+
+      should_clear_pointer_state = true;
+      scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+          FROM_HERE,
+          base::Bind(
+              &ui_navigation::scroll_engine::ScrollEngine::HandleScrollStart,
+              base::Unretained(scroll_engine_),
+              element_to_scroll->GetUiNavItem(), scroll_type, pointer_id,
+              initial_coordinates.value(), initial_time_stamp.value(),
+              pointer_coordinates, pointer_event->time_stamp()));
+    }
+  }
+
+  if (should_clear_pointer_state) {
+    pointer_state->ClearClientCoordinates(pointer_id);
+    pointer_state->ClearTimeStamp(pointer_id);
+  }
+}
+
 namespace {
 // Return the nearest common ancestor of previous_element and target_element
 scoped_refptr<dom::Element> GetNearestCommonAncestor(
@@ -342,6 +522,10 @@
 }
 }  // namespace
 
+TopmostEventTarget::TopmostEventTarget(
+    ui_navigation::scroll_engine::ScrollEngine* scroll_engine)
+    : scroll_engine_(scroll_engine) {}
+
 void TopmostEventTarget::MaybeSendPointerEvents(
     const scoped_refptr<web::Event>& event) {
   TRACE_EVENT0("cobalt::layout",
@@ -405,6 +589,11 @@
     target_element = FindTopmostEventTarget(view->document(), coordinate);
   }
 
+  if (target_element && pointer_event) {
+    HandleScrollState(target_element, pointer_event, pointer_state,
+                      &event_init);
+  }
+
   scoped_refptr<dom::HTMLElement> previous_html_element(
       previous_html_element_weak_);
 
@@ -417,8 +606,16 @@
                              target_element, nearest_common_ancestor,
                              &event_init);
 
+  bool event_was_cancelled = pointer_event && pointer_state->GetWasCancelled(
+                                                  pointer_event->pointer_id());
+  if (pointer_event && pointer_event->type() == base::Tokens::pointerup()) {
+    pointer_state->ClearWasCancelled(pointer_event->pointer_id());
+  }
+
   if (target_element) {
-    target_element->DispatchEvent(event);
+    if (!event_was_cancelled) {
+      target_element->DispatchEvent(event);
+    }
   }
 
   if (pointer_event) {
@@ -432,7 +629,7 @@
       pointer_state->ClearPendingPointerCaptureTargetOverride(
           pointer_event->pointer_id());
     }
-    if (target_element && !is_touchpad_event) {
+    if (target_element && !is_touchpad_event && !event_was_cancelled) {
       SendCompatibilityMappingMouseEvent(target_element, event, pointer_event,
                                          event_init,
                                          &mouse_event_prevent_flags_);
diff --git a/cobalt/layout/topmost_event_target.h b/cobalt/layout/topmost_event_target.h
index 078e179..1d1dd9b 100644
--- a/cobalt/layout/topmost_event_target.h
+++ b/cobalt/layout/topmost_event_target.h
@@ -21,10 +21,12 @@
 #include "base/memory/weak_ptr.h"
 #include "cobalt/dom/document.h"
 #include "cobalt/dom/html_element.h"
+#include "cobalt/dom/pointer_event.h"
 #include "cobalt/layout/box.h"
 #include "cobalt/layout/layout_boxes.h"
 #include "cobalt/math/vector2d.h"
 #include "cobalt/math/vector2d_f.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
 #include "cobalt/web/event.h"
 
 namespace cobalt {
@@ -32,7 +34,8 @@
 
 class TopmostEventTarget {
  public:
-  TopmostEventTarget() {}
+  explicit TopmostEventTarget(
+      ui_navigation::scroll_engine::ScrollEngine* scroll_engine);
 
   void MaybeSendPointerEvents(const scoped_refptr<web::Event>& event);
 
@@ -41,6 +44,13 @@
       const scoped_refptr<dom::Document>& document,
       const math::Vector2dF& coordinate);
 
+  void HandleScrollState(scoped_refptr<dom::HTMLElement> target_element,
+                         const dom::PointerEvent* pointer_event,
+                         dom::PointerState* pointer_state,
+                         dom::PointerEventInit* event_init);
+  void CancelScrollsInParentNavItems(
+      scoped_refptr<dom::HTMLElement> target_element);
+
   void ConsiderElement(dom::Element* element,
                        const math::Vector2dF& coordinate);
   void ConsiderBoxes(const scoped_refptr<dom::HTMLElement>& html_element,
@@ -52,6 +62,8 @@
   scoped_refptr<Box> box_;
   Box::RenderSequence render_sequence_;
 
+  ui_navigation::scroll_engine::ScrollEngine* scroll_engine_;
+
   // This map stores the pointer types for which the 'prevent mouse event' flag
   // has been set as part of the compatibility mapping steps defined at
   // https://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events.
diff --git a/cobalt/layout_tests/layout_snapshot.cc b/cobalt/layout_tests/layout_snapshot.cc
index 88d07f3..e137530 100644
--- a/cobalt/layout_tests/layout_snapshot.cc
+++ b/cobalt/layout_tests/layout_snapshot.cc
@@ -27,6 +27,7 @@
 #include "cobalt/dom/window.h"
 #include "cobalt/network/network_module.h"
 #include "cobalt/render_tree/resource_provider.h"
+#include "cobalt/web/web_settings.h"
 #include "starboard/window.h"
 
 using cobalt::cssom::ViewportSize;
@@ -73,6 +74,7 @@
   // Some layout tests test Content Security Policy; allow HTTP so we
   // don't interfere.
   net_options.https_requirement = network::kHTTPSOptional;
+  web::WebSettingsImpl web_settings;
   network::NetworkModule network_module(
       browser::CreateUserAgentString(
           browser::GetUserAgentPlatformInfoFromSystem()),
@@ -91,6 +93,7 @@
   // we take advantage of the convenience of inline script tags.
   web_module_options.enable_inline_script_warnings = false;
 
+  web_module_options.web_options.web_settings = &web_settings;
   web_module_options.web_options.network_module = &network_module;
 
   // Prepare a slot for our results to be placed when ready.
@@ -99,7 +102,7 @@
   // Create the WebModule and wait for a layout to occur.
   browser::WebModule web_module("SnapshotURL");
   web_module.Run(
-      url, base::kApplicationStateStarted,
+      url, base::kApplicationStateStarted, nullptr /* scroll_engine */,
       base::Bind(&WebModuleOnRenderTreeProducedCallback, &results, &run_loop,
                  base::MessageLoop::current()),
       base::Bind(&WebModuleErrorCallback, &run_loop,
diff --git a/cobalt/layout_tests/testdata/BUILD.gn b/cobalt/layout_tests/testdata/BUILD.gn
index 5741156..494c692 100644
--- a/cobalt/layout_tests/testdata/BUILD.gn
+++ b/cobalt/layout_tests/testdata/BUILD.gn
@@ -1399,8 +1399,10 @@
     "css3-fonts/5-2-use-numerical-font-weights-in-family-face-matching.html",
     "css3-fonts/5-2-use-specified-font-family-if-available-expected.png",
     "css3-fonts/5-2-use-specified-font-family-if-available.html",
-    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png",
-    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html",
+    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png",
+    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html",
+    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png",
+    "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html",
     "css3-fonts/color-emojis-should-render-properly-expected.png",
     "css3-fonts/color-emojis-should-render-properly.html",
     "css3-fonts/layout_tests.txt",
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png
new file mode 100644
index 0000000..3b06f5b
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html
new file mode 100644
index 0000000..a640c4e
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+ | If no font family is specified that supports the characters, determine the
+ | font to use via system font fallback.
+ |   https://www.w3.org/TR/css3-fonts/#system-font-fallback
+ -->
+<html>
+<head>
+  <meta charset="utf-8">
+  <style>
+    body {
+      margin: 0;
+      font-family: Roboto;
+      font-size: 30px;
+      font-weight: normal;
+      color: #fff;
+    }
+    .containing-block {
+      background-color: #03a9f4;
+      width: 500px;
+    }
+  </style>
+</head>
+<body>
+  <div class="containing-block">
+    <span>🍔🌞😜</span>
+  </div>
+</body>
+</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png
deleted file mode 100644
index 8560d78..0000000
--- a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png
+++ /dev/null
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png
new file mode 100644
index 0000000..49fa021
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html
new file mode 100644
index 0000000..68f6fa8
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+ | If no font family is specified that supports the characters, determine the
+ | font to use via system font fallback.
+ |   https://www.w3.org/TR/css3-fonts/#system-font-fallback
+ -->
+<html>
+<head>
+  <meta charset="utf-8">
+  <style>
+    body {
+      margin: 0;
+      font-family: Roboto;
+      font-size: 30px;
+      font-weight: normal;
+      color: #fff;
+    }
+    .containing-block {
+      background-color: #03a9f4;
+      width: 500px;
+    }
+  </style>
+</head>
+<body>
+  <div class="containing-block">
+    <span>בְּרֵאשִׁ֖ית The Hegemony Consul sat on the balcony of his ebony spaceship. δῖος δῖοσ金魚 中國哲學書電子化計劃計劃 وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَ  ☃ ᠦᡈ‍ᠣᡄ➤</span>
+  </div>
+</body>
+</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html
deleted file mode 100644
index 899ee15..0000000
--- a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!--
- | If no font family is specified that supports the characters, determine the
- | font to use via system font fallback.
- |   https://www.w3.org/TR/css3-fonts/#system-font-fallback
- -->
-<html>
-<head>
-  <meta charset="utf-8">
-  <style>
-    body {
-      margin: 0;
-      font-family: Roboto;
-      font-size: 30px;
-      font-weight: normal;
-      color: #fff;
-    }
-    .containing-block {
-      background-color: #03a9f4;
-      width: 500px;
-    }
-  </style>
-</head>
-<body>
-  <div class="containing-block">
-    <span>🍔🌞 בְּרֵאשִׁ֖ית The Hegemony Consul sat on the balcony of his ebony spaceship. δῖος δῖοσ金魚 😜 中國哲學書電子化計劃計劃 وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَ  ☃ ᠦᡈ‍ᠣᡄ➤</span>
-  </div>
-</body>
-</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt b/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
index 8c922d4..554c89d 100644
--- a/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
+++ b/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
@@ -10,7 +10,8 @@
 5-2-use-first-available-listed-font-family
 5-2-use-numerical-font-weights-in-family-face-matching
 5-2-use-specified-font-family-if-available
-5-2-use-system-fallback-if-no-matching-family-is-found
+5-2-use-system-fallback-if-no-matching-family-is-found-emoji
+5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji
 color-emojis-should-render-properly
 synthetic-bolding-should-not-occur-on-bold-font
 synthetic-bolding-should-occur-on-non-bold-font
diff --git a/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
index ade7e63..a26a02e 100644
--- a/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
+++ b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
@@ -1,21 +1,270 @@
 # Service Worker API tests
 
-cache-storage/common.https.html, PASS
-cache-storage/serviceworker/cache-add.https.html, DISABLE
-cache-storage/serviceworker/cache-delete.https.html, DISABLE
-cache-storage/serviceworker/cache-match.https.html, DISABLE
-cache-storage/serviceworker/cache-put.https.html, DISABLE
-cache-storage/serviceworker/cache-storage-match.https.html, DISABLE
-cache-storage/serviceworker/cache-storage.https.html, DISABLE
-cache-storage/worker/cache-add.https.html, PASS
-cache-storage/worker/cache-delete.https.html, PASS
-cache-storage/worker/cache-match.https.html, PASS
-cache-storage/worker/cache-put.https.html, PASS
-cache-storage/worker/cache-storage-match.https.html, PASS
-cache-storage/worker/cache-storage.https.html, PASS
-cache-storage/window/cache-add.https.html, PASS
-cache-storage/window/cache-delete.https.html, PASS
-cache-storage/window/cache-match.https.html, PASS
-cache-storage/window/cache-put.https.html, PASS
-cache-storage/window/cache-storage-match.https.html, PASS
-cache-storage/window/cache-storage.https.html, PASS
+service-worker/register-default-scope.https.html, PASS
+service-worker/rejections.https.html, PASS
+service-worker/serviceworkerobject-scripturl.https.html, PASS
+service-worker/unregister.https.html, PASS
+service-worker/registration-script-url.https.html, DISABLE
+service-worker/registration-security-error.https.html, DISABLE
+service-worker/Service-Worker-Allowed-header.https.html, DISABLE
+service-worker/update-result.https.html, DISABLE
+service-worker/update-missing-import-scripts.https.html, DISABLE
+
+# Tests pass with memory leakage issue.
+service-worker/update-no-cache-request-headers.https.html, DISABLE
+
+# b/266605500
+# Caught signal: SIGSEGV on linux-x64x11-sbversion-12_evergreen-x64-sbversion-12_nightly
+service-worker/clients-matchall-on-evaluation.https.html, DISABLE
+
+# b/266605087 TypeError: Cannot set property 'onerror' of null"
+service-worker/skip-waiting-without-client.https.html, DISABLE
+
+# b/234788479 Implement waiting for update worker state tasks in Install algorithm.
+service-worker/activation-after-registration.https.html, DISABLE
+
+# b/264920834 heap-use-after-free bug
+service-worker/registration-basic.https.html, DISABLE
+service-worker/activate-event-after-install-state-change.https.html, DISABLE
+service-worker/clients-matchall-frozen.https.html, DISABLE
+service-worker/fetch-event-add-async.https.html, DISABLE
+service-worker/multiple-update.https.html, DISABLE
+service-worker/register-wait-forever-in-install-worker.https.html, DISABLE
+service-worker/registration-events.https.html, DISABLE
+service-worker/registration-schedule-job.https.html, DISABLE
+service-worker/service-worker-header.https.html, DISABLE
+service-worker/state.https.html, DISABLE
+service-worker/uncontrolled-page.https.html, DISABLE
+
+# b/267538636 assert_throws_js: eval() should throw EvalError
+service-worker/service-worker-csp-connect.https.html, DISABLE
+service-worker/service-worker-csp-default.https.html, DISABLE
+service-worker/service-worker-csp-script.https.html, DISABLE
+
+# "Module" type of dedicated worker is supported in Cobalt
+service-worker/dedicated-worker-service-worker-interception.https.html, DISABLE
+service-worker/registration-scope-module-static-import.https.html, DISABLE
+service-worker/update-module-request-mode.https.html, DISABLE
+service-worker/update-registration-with-type.https.html, DISABLE
+service-worker/registration-script-module.https.html, DISABLE
+
+# Channel API is not supported in Cobalt
+service-worker/extendable-event-waituntil.https.html, DISABLE
+service-worker/immutable-prototype-serviceworker.https.html, DISABLE
+service-worker/indexeddb.https.html, DISABLE
+service-worker/postmessage.https.html, DISABLE
+service-worker/registration-end-to-end.https.html, DISABLE
+
+# Below are manual tests from web-platform-tests without automation.
+service-worker/fetch-event-is-history-backward-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-is-history-forward-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-is-reload-navigation-manual.https.html, DISABLE
+
+# b/265841607 Unhandled rejection with value: object "TypeError"
+service-worker/import-scripts-cross-origin.https.html, DISABLE
+service-worker/import-scripts-mime-types.https.html, DISABLE
+service-worker/import-scripts-redirect.https.html, DISABLE
+service-worker/import-scripts-resource-map.https.html, DISABLE
+service-worker/import-scripts-updated-flag.https.html, DISABLE
+service-worker/same-site-cookies.https.html, DISABLE
+
+# b/265844662
+service-worker/interface-requirements-sw.https.html, DISABLE
+
+# b/265847846
+service-worker/navigate-window.https.html, DISABLE
+
+# b/267230419 Error type incorrect
+service-worker/registration-script.https.html, DISABLE
+
+# broken on Chrome
+service-worker/credentials.https.html, DISABLE
+service-worker/navigation-redirect-body.https.html, DISABLE
+service-worker/navigation-sets-cookie.https.html, DISABLE
+service-worker/registration-mime-types.https.html, DISABLE
+service-worker/registration-scope.https.html, DISABLE
+service-worker/update-bytecheck-cors-import.https.html, DISABLE
+service-worker/update-bytecheck.https.html, DISABLE
+service-worker/referrer-toplevel-script-fetch.https.html, DISABLE
+
+# b/264323329 TIMEOUT
+service-worker/install-event-type.https.html, DISABLE
+service-worker/oninstall-script-error.https.html, DISABLE
+service-worker/onactivate-script-error.https.html, DISABLE
+service-worker/postmessage-blob-url.https.html, DISABLE
+service-worker/unregister-immediately-before-installed.https.html, DISABLE
+service-worker/update-not-allowed.https.html, DISABLE
+
+# b/265981629
+service-worker/registration-service-worker-attributes.https.html, DISABLE
+
+# b/265983449
+service-worker/synced-state.https.html, DISABLE
+
+# websocket is not supported in Cobalt
+service-worker/websocket-in-service-worker.https.html, DISABLE
+
+# iframe is not supported in Cobalt
+service-worker/about-blank-replacement.https.html, DISABLE
+service-worker/activation.https.html, DISABLE
+service-worker/active.https.html, DISABLE
+service-worker/claim-affect-other-registration.https.html, DISABLE
+service-worker/claim-fetch.https.html, DISABLE
+service-worker/claim-not-using-registration.https.html, DISABLE
+service-worker/claim-shared-worker-fetch.https.html, DISABLE
+service-worker/claim-using-registration.https.html, DISABLE
+service-worker/claim-with-redirect.https.html, DISABLE
+service-worker/claim-worker-fetch.https.html, DISABLE
+service-worker/client-id.https.html, DISABLE
+service-worker/client-navigate.https.html, DISABLE
+service-worker/client-url-of-blob-url-worker.https.html, DISABLE
+service-worker/clients-get-client-types.https.html, DISABLE
+service-worker/clients-get-cross-origin.https.html, DISABLE
+service-worker/clients-get.https.html, DISABLE
+service-worker/clients-get-resultingClientId.https.html, DISABLE
+service-worker/clients-matchall-blob-url-worker.https.html, DISABLE
+service-worker/clients-matchall-client-types.https.html, DISABLE
+service-worker/clients-matchall-exact-controller.https.html, DISABLE
+service-worker/clients-matchall.https.html, DISABLE
+service-worker/clients-matchall-include-uncontrolled.https.html, DISABLE
+service-worker/clients-matchall-order.https.html, DISABLE
+service-worker/clients-matchall.https.html, DISABLE
+service-worker/controller-on-disconnect.https.html, DISABLE
+service-worker/controller-on-load.https.html, DISABLE
+service-worker/controller-on-reload.https.html, DISABLE
+service-worker/controller-with-no-fetch-event-handler.https.html, DISABLE
+service-worker/data-iframe.html, DISABLE
+service-worker/data-transfer-files.https.html, DISABLE
+service-worker/detached-context.https.html, DISABLE
+service-worker/embed-and-object-are-not-intercepted.https.html, DISABLE
+service-worker/extendable-event-async-waituntil.https.html, DISABLE
+service-worker/fetch-audio-tainting.https.html, DISABLE
+service-worker/fetch-canvas-tainting-double-write.https.html, DISABLE
+service-worker/fetch-canvas-tainting-image-cache.https.html, DISABLE
+service-worker/fetch-canvas-tainting-image.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video-cache.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video-with-range-request.https.html, DISABLE
+service-worker/fetch-cors-exposed-header-names.https.html, DISABLE
+service-worker/fetch-cors-xhr.https.html, DISABLE
+service-worker/fetch-csp.https.html, DISABLE
+service-worker/fetch-error.https.html, DISABLE
+service-worker/fetch-event-after-navigation-within-page.https.html, DISABLE
+service-worker/fetch-event-async-respond-with.https.html, DISABLE
+service-worker/fetch-event-handled.https.html, DISABLE
+service-worker/fetch-event.https.h2.html, DISABLE
+service-worker/fetch-event.https.html, DISABLE
+service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-network-error.https.html, DISABLE
+service-worker/fetch-event-redirect.https.html, DISABLE
+service-worker/fetch-event-referrer-policy.https.html, DISABLE
+service-worker/fetch-event-respond-with-argument.https.html, DISABLE
+service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-custom-response.https.html, DISABLE
+service-worker/fetch-event-respond-with-partial-stream.https.html, DISABLE
+service-worker/fetch-event-respond-with-readable-stream-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-readable-stream.https.html, DISABLE
+service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-stops-propagation.https.html, DISABLE
+service-worker/fetch-event-throws-after-respond-with.https.html, DISABLE
+service-worker/fetch-event-within-sw.https.html, DISABLE
+service-worker/fetch-event-within-sw-manual.https.html, DISABLE
+service-worker/fetch-frame-resource.https.html, DISABLE
+service-worker/fetch-header-visibility.https.html, DISABLE
+service-worker/fetch-mixed-content-to-inscope.https.html, DISABLE
+service-worker/fetch-mixed-content-to-outscope.https.html, DISABLE
+service-worker/fetch-request-css-base-url.https.html, DISABLE
+service-worker/fetch-request-css-cross-origin.https.html, DISABLE
+service-worker/fetch-request-css-images.https.html, DISABLE
+service-worker/fetch-request-fallback.https.html, DISABLE
+service-worker/fetch-request-no-freshness-headers.https.html, DISABLE
+service-worker/fetch-request-redirect.https.html, DISABLE
+service-worker/fetch-request-resources.https.html, DISABLE
+service-worker/fetch-request-xhr.https.html, DISABLE
+service-worker/fetch-request-xhr-sync-error.https.window.js, DISABLE
+service-worker/fetch-request-xhr-sync.https.html, DISABLE
+service-worker/fetch-request-xhr-sync-on-worker.https.html, DISABLE
+service-worker/fetch-response-taint.https.html, DISABLE
+service-worker/fetch-response-xhr.https.html, DISABLE
+service-worker/fetch-waits-for-activate.https.html, DISABLE
+service-worker/getregistration.https.html, DISABLE
+service-worker/getregistrations.https.html, DISABLE
+service-worker/global-serviceworker.https.any.js, DISABLE
+service-worker/historical.https.any.js, DISABLE
+service-worker/http-to-https-redirect-and-register.https.html, DISABLE
+service-worker/installing.https.html, DISABLE
+service-worker/invalid-blobtype.https.html, DISABLE
+service-worker/invalid-header.https.html, DISABLE
+service-worker/iso-latin1-header.https.html, DISABLE
+service-worker/local-url-inherit-controller.https.html, DISABLE
+service-worker/mime-sniffing.https.html, DISABLE
+service-worker/multi-globals, DISABLE
+service-worker/multipart-image.https.html, DISABLE
+service-worker/multiple-register.https.html, DISABLE
+service-worker/navigation-headers.https.html, DISABLE
+service-worker/navigation-preload, DISABLE
+service-worker/navigation-redirect.https.html, DISABLE
+service-worker/navigation-redirect-resolution.https.html, DISABLE
+service-worker/navigation-redirect-to-http.https.html, DISABLE
+service-worker/navigation-timing-extended.https.html, DISABLE
+service-worker/navigation-timing.https.html, DISABLE
+service-worker/nested-blob-url-workers.https.html, DISABLE
+service-worker/next-hop-protocol.https.html, DISABLE
+service-worker/no-dynamic-import.any.js, DISABLE
+service-worker/no-dynamic-import-in-module.any.js, DISABLE
+service-worker/opaque-response-preloaded.https.html, DISABLE
+service-worker/opaque-script.https.html, DISABLE
+service-worker/partitioned-claim.tentative.https.html, DISABLE
+service-worker/partitioned-getRegistrations.tentative.https.html, DISABLE
+service-worker/partitioned-matchAll.tentative.https.html, DISABLE
+service-worker/partitioned.tentative.https.html, DISABLE
+service-worker/performance-timeline.https.html, DISABLE
+service-worker/postmessage-from-waiting-serviceworker.https.html, DISABLE
+service-worker/postmessage-msgport-to-client.https.html, DISABLE
+service-worker/postmessage-to-client.https.html, DISABLE
+service-worker/postmessage-to-client-message-queue.https.html, DISABLE
+service-worker/ready.https.window.js, DISABLE
+service-worker/redirected-response.https.html, DISABLE
+service-worker/referer.https.html, DISABLE
+service-worker/referrer-policy-header.https.html, DISABLE
+service-worker/register-closed-window.https.html, DISABLE
+service-worker/register-same-scope-different-script-url.https.html, DISABLE
+service-worker/registration-iframe.https.html, DISABLE
+service-worker/registration-updateviacache.https.html, DISABLE
+service-worker/request-end-to-end.https.html, DISABLE
+service-worker/resource-timing-bodySize.https.html, DISABLE
+service-worker/resource-timing-cross-origin.https.html, DISABLE
+service-worker/resource-timing-fetch-variants.https.html, DISABLE
+service-worker/resource-timing.sub.https.html, DISABLE
+service-worker/respond-with-body-accessed-response.https.html, DISABLE
+service-worker/sandboxed-iframe-fetch-event.https.html, DISABLE
+service-worker/sandboxed-iframe-navigator-serviceworker.https.html, DISABLE
+service-worker/secure-context.https.html, DISABLE
+service-worker/serviceworker-message-event-historical.https.html, DISABLE
+service-worker/skip-waiting.https.html, DISABLE
+service-worker/skip-waiting-installed.https.html, DISABLE
+service-worker/skip-waiting-using-registration.https.html, DISABLE
+service-worker/skip-waiting-without-using-registration.https.html, DISABLE
+service-worker/svg-target-reftest.https.html, DISABLE
+service-worker/unregister-controller.https.html, DISABLE
+service-worker/unregister-immediately-during-extendable-events.https.html, DISABLE
+service-worker/unregister-immediately.https.html, DISABLE
+service-worker/unregister-then-register.https.html, DISABLE
+service-worker/unregister-then-register-new-script.https.html, DISABLE
+service-worker/update-after-navigation-fetch-event.https.html, DISABLE
+service-worker/update-after-navigation-redirect.https.html, DISABLE
+service-worker/update-after-oneday.https.html, DISABLE
+service-worker/update.https.html, DISABLE
+service-worker/update-import-scripts.https.html, DISABLE
+service-worker/pdate-on-navigation.https.html, DISABLE
+service-worker/update-recovery.https.html, DISABLE
+service-worker/waiting.https.html, DISABLE
+service-worker/websocket.https.html, DISABLE
+service-worker/webvtt-cross-origin.https.html, DISABLE
+service-worker/windowclient-navigate.https.html, DISABLE
+service-worker/worker-client-id.https.html, DISABLE
+service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html, DISABLE
+service-worker/worker-interception.https.html, DISABLE
+service-worker/worker-interception-redirect.https.html, DISABLE
+service-worker/xhr-response-url.https.html, DISABLE
+service-worker/xsl-base-url.https.html, DISABLE
diff --git a/cobalt/layout_tests/testdata/web-platform-tests/workers/web_platform_tests.txt b/cobalt/layout_tests/testdata/web-platform-tests/workers/web_platform_tests.txt
index 0c20d9a..7845616 100644
--- a/cobalt/layout_tests/testdata/web-platform-tests/workers/web_platform_tests.txt
+++ b/cobalt/layout_tests/testdata/web-platform-tests/workers/web_platform_tests.txt
@@ -5,7 +5,9 @@
 # features that are expected to currently work.
 
 Worker_basic.htm, PASS
-Worker_dispatchEvent_ErrorEvent.htm, PASS
+
+# b/266711887 SIGSEGV
+Worker_dispatchEvent_ErrorEvent.htm, DISABLE
 
 # b/225037465
 Worker_cross_origin_security_err.htm, DISABLE
diff --git a/cobalt/layout_tests/web_platform_tests.cc b/cobalt/layout_tests/web_platform_tests.cc
index fda4f74..860755a 100644
--- a/cobalt/layout_tests/web_platform_tests.cc
+++ b/cobalt/layout_tests/web_platform_tests.cc
@@ -25,6 +25,8 @@
 #include "base/strings/string_util.h"
 #include "base/test/scoped_task_environment.h"
 #include "base/values.h"
+#include "cobalt/browser/service_worker_registry.h"
+#include "cobalt/browser/user_agent_platform_info.h"
 #include "cobalt/browser/web_module.h"
 #include "cobalt/cssom/viewport_size.h"
 #include "cobalt/layout_tests/test_utils.h"
@@ -34,6 +36,7 @@
 #include "cobalt/network/network_module.h"
 #include "cobalt/render_tree/resource_provider_stub.h"
 #include "cobalt/web/csp_delegate_factory.h"
+#include "cobalt/web/web_settings.h"
 #include "starboard/window.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
@@ -200,6 +203,7 @@
 
   // Media module
   FakeResourceProviderStub resource_provider;
+  web::WebSettingsImpl web_settings;
   std::unique_ptr<media::MediaModule> media_module(
       new media::MediaModule(NULL, &resource_provider));
   std::unique_ptr<media::CanPlayTypeHandler> can_play_type_handler(
@@ -215,6 +219,7 @@
   // we take advantage of the convenience of inline script tags.
   web_module_options.enable_inline_script_warnings = false;
 
+  web_module_options.web_options.web_settings = &web_settings;
   web_module_options.web_options.network_module = &network_module;
 
   // Prepare a slot for our results to be placed when ready.
@@ -223,8 +228,16 @@
 
   // Create the WebModule and wait for a layout to occur.
   browser::WebModule web_module("RunWebPlatformTest");
+
+  // Create Service Worker Registry
+  browser::ServiceWorkerRegistry* service_worker_registry =
+      new browser::ServiceWorkerRegistry(&web_settings, &network_module,
+                                         new browser::UserAgentPlatformInfo());
+  web_module_options.web_options.service_worker_jobs =
+      service_worker_registry->service_worker_jobs();
+
   web_module.Run(
-      url, base::kApplicationStateStarted,
+      url, base::kApplicationStateStarted, nullptr /* scroll_engine */,
       base::Bind(&WebModuleOnRenderTreeProducedCallback, &results),
       base::Bind(&WebModuleErrorCallback, &run_loop,
                  base::MessageLoop::current()),
diff --git a/cobalt/loader/fetch_interceptor_coordinator.cc b/cobalt/loader/fetch_interceptor_coordinator.cc
index fc2e8e8..f4db96c 100644
--- a/cobalt/loader/fetch_interceptor_coordinator.cc
+++ b/cobalt/loader/fetch_interceptor_coordinator.cc
@@ -38,21 +38,30 @@
 
 void FetchInterceptorCoordinator::TryIntercept(
     const GURL& url,
-    std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
-        callback,
-    std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+    base::OnceCallback<void(std::unique_ptr<std::string>)> callback,
+    base::OnceCallback<void(const net::LoadTimingInfo&)>
         report_load_timing_info,
-    std::unique_ptr<base::OnceClosure> fallback) {
+    base::OnceClosure fallback) {
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#handle-fetch
-  // Steps 17 to 23
-  // TODO: add should skip event handling
-  // (https://w3c.github.io/ServiceWorker/#should-skip-event).
-  // TODO: determine |shouldSoftUpdate| and if true run soft update in parallel
-  // (https://w3c.github.io/ServiceWorker/#soft-update).
+
   if (!fetch_interceptor_) {
-    std::move(*fallback).Run();
+    std::move(fallback).Run();
     return;
   }
+  // Steps 17 to 23
+  // TODO: add should skip event handling
+  // (https://www.w3.org/TR/2022/CRD-service-workers-20220712/#should-skip-event).
+
+  // 18. Let shouldSoftUpdate be true if any of the following are true, and
+  //     false otherwise:
+  //      . request is a non-subresource request.
+  // Note: check if destination is: "document", "embed", "frame", "iframe",
+  // "object", "report", "serviceworker", "sharedworker", or "worker".
+  //
+  //      . request is a subresource request and registration is stale.
+  // Note: check if destination is "audio", "audioworklet", "font", "image",
+  // "manifest", "paintworklet", "script", "style", "track", "video", "xslt"
+
   // TODO: test interception once registered service workers are persisted.
   //       Consider moving the interception out of the ServiceWorkerGlobalScope
   //       to avoid a race condition. Fetches should be able to be intercepted
diff --git a/cobalt/loader/fetch_interceptor_coordinator.h b/cobalt/loader/fetch_interceptor_coordinator.h
index 13eb373..d8b0bbd 100644
--- a/cobalt/loader/fetch_interceptor_coordinator.h
+++ b/cobalt/loader/fetch_interceptor_coordinator.h
@@ -34,11 +34,10 @@
  public:
   virtual void StartFetch(
       const GURL& url,
-      std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
-          callback,
-      std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+      base::OnceCallback<void(std::unique_ptr<std::string>)> callback,
+      base::OnceCallback<void(const net::LoadTimingInfo&)>
           report_load_timing_info,
-      std::unique_ptr<base::OnceClosure> fallback) = 0;
+      base::OnceClosure fallback) = 0;
 };
 
 // NetFetcher is for fetching data from the network.
@@ -54,11 +53,10 @@
 
   void TryIntercept(
       const GURL& url,
-      std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
-          callback,
-      std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+      base::OnceCallback<void(std::unique_ptr<std::string>)> callback,
+      base::OnceCallback<void(const net::LoadTimingInfo&)>
           report_load_timing_info,
-      std::unique_ptr<base::OnceClosure> fallback);
+      base::OnceClosure fallback);
 
  private:
   friend struct base::DefaultSingletonTraits<FetchInterceptorCoordinator>;
diff --git a/cobalt/loader/net_fetcher.cc b/cobalt/loader/net_fetcher.cc
index e8bcf89..4fa52ec 100644
--- a/cobalt/loader/net_fetcher.cc
+++ b/cobalt/loader/net_fetcher.cc
@@ -153,15 +153,11 @@
     }
     FetchInterceptorCoordinator::GetInstance()->TryIntercept(
         original_url,
-        std::make_unique<
-            base::OnceCallback<void(std::unique_ptr<std::string>)>>(
-            base::BindOnce(&NetFetcher::OnFetchIntercepted,
-                           base::Unretained(this))),
-        std::make_unique<base::OnceCallback<void(const net::LoadTimingInfo&)>>(
-            base::BindOnce(&NetFetcher::ReportLoadTimingInfo,
-                           base::Unretained(this))),
-        std::make_unique<base::OnceClosure>(base::BindOnce(
-            &net::URLFetcher::Start, base::Unretained(url_fetcher_.get()))));
+        base::BindOnce(&NetFetcher::OnFetchIntercepted, base::Unretained(this)),
+        base::BindOnce(&NetFetcher::ReportLoadTimingInfo,
+                       base::Unretained(this)),
+        base::BindOnce(&net::URLFetcher::Start,
+                       base::Unretained(url_fetcher_.get())));
 
   } else {
     std::string msg(base::StringPrintf("URL %s rejected by security policy.",
@@ -205,9 +201,9 @@
   if ((handler()->OnResponseStarted(this, source->GetResponseHeaders()) ==
        kLoadResponseAbort) ||
       (!IsResponseCodeSuccess(source->GetResponseCode()))) {
-    std::string msg(
-        base::StringPrintf("Handler::OnResponseStarted aborted URL %s",
-                           source->GetURL().spec().c_str()));
+    std::string msg(base::StringPrintf("URL %s aborted or failed with code %d",
+                                       source->GetURL().spec().c_str(),
+                                       source->GetResponseCode()));
     return HandleError(msg).InvalidateThis();
   }
 
diff --git a/cobalt/media/BUILD.gn b/cobalt/media/BUILD.gn
index d1abff9..1b27130 100644
--- a/cobalt/media/BUILD.gn
+++ b/cobalt/media/BUILD.gn
@@ -43,17 +43,18 @@
     "base/playback_statistics.h",
     "base/sbplayer_bridge.cc",
     "base/sbplayer_bridge.h",
+    "base/sbplayer_interface.cc",
+    "base/sbplayer_interface.h",
     "base/sbplayer_pipeline.cc",
     "base/sbplayer_set_bounds_helper.cc",
     "base/sbplayer_set_bounds_helper.h",
     "decoder_buffer_allocator.cc",
     "decoder_buffer_allocator.h",
     "decoder_buffer_memory_info.h",
-    "fetcher_buffered_data_source.cc",
-    "fetcher_buffered_data_source.h",
+    "file_data_source.cc",
+    "file_data_source.h",
     "media_module.cc",
     "media_module.h",
-    "player/buffered_data_source.h",
     "player/web_media_player_impl.cc",
     "player/web_media_player_impl.h",
     "player/web_media_player_proxy.cc",
@@ -76,6 +77,8 @@
     "progressive/progressive_parser.h",
     "progressive/rbsp_stream.cc",
     "progressive/rbsp_stream.h",
+    "url_fetcher_data_source.cc",
+    "url_fetcher_data_source.h",
   ]
 
   configs -= [ "//starboard/build/config:size" ]
@@ -108,6 +111,7 @@
   testonly = true
 
   sources = [
+    "file_data_source_test.cc",
     "progressive/demuxer_extension_wrapper_test.cc",
     "progressive/mock_data_source_reader.h",
     "progressive/mp4_map_unittest.cc",
@@ -119,10 +123,13 @@
 
   deps = [
     ":media",
+    "//base/test:test_support",
     "//cobalt/base",
     "//cobalt/test:run_all_unittests",
     "//testing/gmock",
     "//testing/gtest",
     "//third_party/chromium/media:media",
   ]
+
+  data_deps = [ "//cobalt/media/testing:cobalt_media_download_test_data" ]
 }
diff --git a/cobalt/media/base/data_source.h b/cobalt/media/base/data_source.h
index c415f30..0af37b9 100644
--- a/cobalt/media/base/data_source.h
+++ b/cobalt/media/base/data_source.h
@@ -18,6 +18,7 @@
  public:
   typedef base::Callback<void(int64_t, int64_t)> StatusCallback;
   typedef base::Callback<void(int)> ReadCB;
+  typedef base::Callback<void(bool)> DownloadingStatusCB;
 
   enum { kReadError = -1, kAborted = -2 };
 
@@ -41,13 +42,9 @@
   // retrieved.
   virtual bool GetSize(int64_t* size_out) = 0;
 
-  // Returns true if we are performing streaming. In this case seeking is
-  // not possible.
-  virtual bool IsStreaming() = 0;
-
-  // Notify the DataSource of the bitrate of the media.
-  // Values of |bitrate| <= 0 are invalid and should be ignored.
-  virtual void SetBitrate(int bitrate) = 0;
+  // Sets a callback to receive downloading status.
+  virtual void SetDownloadingStatusCB(
+      const DownloadingStatusCB& downloading_status_cb) = 0;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(DataSource);
diff --git a/cobalt/media/base/pipeline.h b/cobalt/media/base/pipeline.h
index 9f9995e..ccb3a1b 100644
--- a/cobalt/media/base/pipeline.h
+++ b/cobalt/media/base/pipeline.h
@@ -23,6 +23,7 @@
 #include "base/message_loop/message_loop.h"
 #include "cobalt/media/base/decode_target_provider.h"
 #include "cobalt/media/base/media_export.h"
+#include "cobalt/media/base/sbplayer_interface.h"
 #include "starboard/drm.h"
 #include "starboard/window.h"
 #include "third_party/chromium/media/base/demuxer.h"
@@ -92,12 +93,12 @@
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
   static scoped_refptr<Pipeline> Create(
-      PipelineWindow window,
+      SbPlayerInterface* interface, PipelineWindow window,
       const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
       const GetDecodeTargetGraphicsContextProviderFunc&
           get_decode_target_graphics_context_provider_func,
-      bool allow_resume_after_suspend, MediaLog* media_log,
-      DecodeTargetProvider* decode_target_provider);
+      bool allow_resume_after_suspend, bool allow_batched_sample_write,
+      MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
 
   virtual ~Pipeline() {}
 
diff --git a/cobalt/media/base/sbplayer_bridge.cc b/cobalt/media/base/sbplayer_bridge.cc
index 1135217..6598dc8 100644
--- a/cobalt/media/base/sbplayer_bridge.cc
+++ b/cobalt/media/base/sbplayer_bridge.cc
@@ -100,6 +100,7 @@
 
 #if SB_HAS(PLAYER_WITH_URL)
 SbPlayerBridge::SbPlayerBridge(
+    SbPlayerInterface* interface,
     const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
     const std::string& url, SbWindow window, Host* host,
     SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
@@ -108,6 +109,7 @@
         on_encrypted_media_init_data_encountered_cb,
     DecodeTargetProvider* const decode_target_provider)
     : url_(url),
+      sbplayer_interface_(interface),
       task_runner_(task_runner),
       callback_helper_(
           new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
@@ -134,6 +136,7 @@
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
 SbPlayerBridge::SbPlayerBridge(
+    SbPlayerInterface* interface,
     const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
     const GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
@@ -144,7 +147,8 @@
     bool prefer_decode_to_texture,
     DecodeTargetProvider* const decode_target_provider,
     const std::string& max_video_capabilities)
-    : task_runner_(task_runner),
+    : sbplayer_interface_(interface),
+      task_runner_(task_runner),
       get_decode_target_graphics_context_provider_func_(
           get_decode_target_graphics_context_provider_func),
       callback_helper_(
@@ -202,7 +206,7 @@
   decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
 
   if (SbPlayerIsValid(player_)) {
-    SbPlayerDestroy(player_);
+    sbplayer_interface_->Destroy(player_);
   }
 }
 
@@ -245,24 +249,26 @@
   LOG(INFO) << "Converted to SbMediaVideoSampleInfo -- " << video_sample_info_;
 }
 
-void SbPlayerBridge::WriteBuffer(DemuxerStream::Type type,
-                                 const scoped_refptr<DecoderBuffer>& buffer) {
+void SbPlayerBridge::WriteBuffers(
+    DemuxerStream::Type type,
+    const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
   DCHECK(task_runner_->BelongsToCurrentThread());
-  DCHECK(buffer);
 #if SB_HAS(PLAYER_WITH_URL)
   DCHECK(!is_url_based_);
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
   if (allow_resume_after_suspend_) {
-    decoder_buffer_cache_.AddBuffer(type, buffer);
-
-    if (state_ != kSuspended) {
-      WriteNextBufferFromCache(type);
+    for (const auto& buffer : buffers) {
+      DCHECK(buffer);
+      decoder_buffer_cache_.AddBuffer(type, buffer);
     }
-
+    if (state_ != kSuspended) {
+      WriteNextBuffersFromCache(type, buffers.size());
+    }
     return;
   }
-  WriteBufferInternal(type, buffer);
+
+  WriteBuffersInternal(type, buffers);
 }
 
 void SbPlayerBridge::SetBounds(int z_index, const gfx::Rect& rect) {
@@ -288,7 +294,7 @@
   }
 
   ++ticket_;
-  SbPlayerSetPlaybackRate(player_, 0.f);
+  sbplayer_interface_->SetPlaybackRate(player_, 0.f);
 }
 
 void SbPlayerBridge::Seek(base::TimeDelta time) {
@@ -297,6 +303,9 @@
   decoder_buffer_cache_.ClearAll();
   seek_pending_ = false;
 
+  pending_audio_eos_buffer_ = false;
+  pending_video_eos_buffer_ = false;
+
   if (state_ == kSuspended) {
     preroll_timestamp_ = time;
     return;
@@ -311,9 +320,9 @@
   DCHECK(SbPlayerIsValid(player_));
 
   ++ticket_;
-  SbPlayerSeek2(player_, time.InMicroseconds(), ticket_);
+  sbplayer_interface_->Seek(player_, time.InMicroseconds(), ticket_);
 
-  SbPlayerSetPlaybackRate(player_, playback_rate_);
+  sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
 }
 
 void SbPlayerBridge::SetVolume(float volume) {
@@ -326,7 +335,7 @@
   }
 
   DCHECK(SbPlayerIsValid(player_));
-  SbPlayerSetVolume(player_, volume);
+  sbplayer_interface_->SetVolume(player_, volume);
 }
 
 void SbPlayerBridge::SetPlaybackRate(double playback_rate) {
@@ -342,7 +351,7 @@
     return;
   }
 
-  SbPlayerSetPlaybackRate(player_, playback_rate);
+  sbplayer_interface_->SetPlaybackRate(player_, playback_rate);
 }
 
 void SbPlayerBridge::GetInfo(uint32* video_frames_decoded,
@@ -369,7 +378,7 @@
   DCHECK(SbPlayerIsValid(player_));
 
   SbUrlPlayerExtraInfo url_player_info;
-  SbUrlPlayerGetExtraInfo(player_, &url_player_info);
+  sbplayer_interface_->GetUrlPlayerExtraInfo(player_, &url_player_info);
 
   if (buffer_start_time) {
     *buffer_start_time = base::TimeDelta::FromMicroseconds(
@@ -395,7 +404,7 @@
   DCHECK(SbPlayerIsValid(player_));
 
   SbPlayerInfo2 out_player_info;
-  SbPlayerGetInfo2(player_, &out_player_info);
+  sbplayer_interface_->GetInfo(player_, &out_player_info);
 
   video_sample_info_.frame_width = out_player_info.frame_width;
   video_sample_info_.frame_height = out_player_info.frame_height;
@@ -414,7 +423,7 @@
   DCHECK(SbPlayerIsValid(player_));
 
   SbPlayerInfo2 info;
-  SbPlayerGetInfo2(player_, &info);
+  sbplayer_interface_->GetInfo(player_, &info);
   if (info.duration == SB_PLAYER_NO_DURATION) {
     // URL-based player may not have loaded asset yet, so map no duration to 0.
     return base::TimeDelta();
@@ -432,7 +441,7 @@
   DCHECK(SbPlayerIsValid(player_));
 
   SbPlayerInfo2 info;
-  SbPlayerGetInfo2(player_, &info);
+  sbplayer_interface_->GetInfo(player_, &info);
   return base::TimeDelta::FromMicroseconds(info.start_date);
 }
 
@@ -440,7 +449,7 @@
   DCHECK(is_url_based_);
 
   drm_system_ = drm_system;
-  SbUrlPlayerSetDrmSystem(player_, drm_system);
+  sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system);
 }
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
@@ -454,7 +463,7 @@
 
   DCHECK(SbPlayerIsValid(player_));
 
-  SbPlayerSetPlaybackRate(player_, 0.0);
+  sbplayer_interface_->SetPlaybackRate(player_, 0.0);
 
   set_bounds_helper_->SetPlayerBridge(NULL);
 
@@ -468,7 +477,7 @@
       DecodeTargetProvider::kOutputModeInvalid);
   decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
 
-  SbPlayerDestroy(player_);
+  sbplayer_interface_->Destroy(player_);
 
   player_ = kSbPlayerInvalid;
 }
@@ -490,7 +499,7 @@
   if (is_url_based_) {
     CreateUrlPlayer(url_);
     if (SbDrmSystemIsValid(drm_system_)) {
-      SbUrlPlayerSetDrmSystem(player_, drm_system_);
+      sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system_);
     }
   } else {
     CreatePlayer();
@@ -549,10 +558,10 @@
 
   player_creation_time_ = SbTimeGetMonotonicNow();
 
-  player_ =
-      SbUrlPlayerCreate(url.c_str(), window_, &SbPlayerBridge::PlayerStatusCB,
-                        &SbPlayerBridge::EncryptedMediaInitDataEncounteredCB,
-                        &SbPlayerBridge::PlayerErrorCB, this);
+  player_ = sbplayer_interface_->CreateUrlPlayer(
+      url.c_str(), window_, &SbPlayerBridge::PlayerStatusCB,
+      &SbPlayerBridge::EncryptedMediaInitDataEncounteredCB,
+      &SbPlayerBridge::PlayerErrorCB, this);
   DCHECK(SbPlayerIsValid(player_));
 
   if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
@@ -597,8 +606,9 @@
     creation_param.video_sample_info.codec = kSbMediaVideoCodecNone;
   }
   creation_param.output_mode = output_mode_;
-  DCHECK_EQ(SbPlayerGetPreferredOutputMode(&creation_param), output_mode_);
-  player_ = SbPlayerCreate(
+  DCHECK_EQ(sbplayer_interface_->GetPreferredOutputMode(&creation_param),
+            output_mode_);
+  player_ = sbplayer_interface_->Create(
       window_, &creation_param, &SbPlayerBridge::DeallocateSampleCB,
       &SbPlayerBridge::DecoderStatusCB, &SbPlayerBridge::PlayerStatusCB,
       &SbPlayerBridge::PlayerErrorCB, this,
@@ -624,90 +634,145 @@
   UpdateBounds_Locked();
 }
 
-void SbPlayerBridge::WriteNextBufferFromCache(DemuxerStream::Type type) {
+void SbPlayerBridge::WriteNextBuffersFromCache(DemuxerStream::Type type,
+                                               int max_buffers_per_write) {
   DCHECK(state_ != kSuspended);
 #if SB_HAS(PLAYER_WITH_URL)
   DCHECK(!is_url_based_);
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
-  const scoped_refptr<DecoderBuffer>& buffer =
-      decoder_buffer_cache_.GetBuffer(type);
-  DCHECK(buffer);
-  decoder_buffer_cache_.AdvanceToNextBuffer(type);
-
   DCHECK(SbPlayerIsValid(player_));
 
-  WriteBufferInternal(type, buffer);
+  std::vector<scoped_refptr<DecoderBuffer>> buffers;
+  buffers.reserve(max_buffers_per_write);
+
+  // TODO: DecoderBufferCache doesn't respect config change during resume
+  // b/243308409
+  for (int i = 0; i < max_buffers_per_write; i++) {
+    const scoped_refptr<DecoderBuffer>& buffer =
+        decoder_buffer_cache_.GetBuffer(type);
+    if (!buffer) {
+      break;
+    }
+    decoder_buffer_cache_.AdvanceToNextBuffer(type);
+    buffers.push_back(buffer);
+  }
+
+  WriteBuffersInternal(type, buffers);
 }
 
-void SbPlayerBridge::WriteBufferInternal(
-    DemuxerStream::Type type, const scoped_refptr<DecoderBuffer>& buffer) {
+void SbPlayerBridge::WriteBuffersInternal(
+    DemuxerStream::Type type,
+    const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
 #if SB_HAS(PLAYER_WITH_URL)
   DCHECK(!is_url_based_);
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
-  if (buffer->end_of_stream()) {
-    SbPlayerWriteEndOfStream(player_, DemuxerStreamTypeToSbMediaType(type));
-    return;
-  }
-
-  DecodingBuffers::iterator iter = decoding_buffers_.find(buffer->data());
-  if (iter == decoding_buffers_.end()) {
-    decoding_buffers_[buffer->data()] = std::make_pair(buffer, 1);
-  } else {
-    ++iter->second.second;
-  }
-
   auto sample_type = DemuxerStreamTypeToSbMediaType(type);
 
-  if (sample_type == kSbMediaTypeAudio && first_audio_sample_time_ == 0) {
-    first_audio_sample_time_ = SbTimeGetMonotonicNow();
-  } else if (sample_type == kSbMediaTypeVideo &&
-             first_video_sample_time_ == 0) {
-    first_video_sample_time_ = SbTimeGetMonotonicNow();
+  std::vector<SbPlayerSampleInfo> gathered_sbplayer_sample_infos;
+  std::vector<SbDrmSampleInfo> gathered_sbplayer_sample_infos_drm_info;
+  std::vector<SbDrmSubSampleMapping>
+      gathered_sbplayer_sample_infos_subsample_mapping;
+  std::vector<SbPlayerSampleSideData> gathered_sbplayer_sample_infos_side_data;
+
+  gathered_sbplayer_sample_infos.reserve(buffers.size());
+  gathered_sbplayer_sample_infos_drm_info.reserve(buffers.size());
+  gathered_sbplayer_sample_infos_subsample_mapping.reserve(buffers.size());
+  gathered_sbplayer_sample_infos_side_data.reserve(buffers.size());
+
+  for (int i = 0; i < buffers.size(); i++) {
+    const auto& buffer = buffers[i];
+    if (buffer->end_of_stream()) {
+      DCHECK_EQ(i, buffers.size() - 1);
+      if (buffers.size() > 1) {
+        if (type == DemuxerStream::AUDIO) {
+          pending_audio_eos_buffer_ = true;
+        } else {
+          pending_video_eos_buffer_ = true;
+        }
+
+        DCHECK(!gathered_sbplayer_sample_infos.empty());
+        sbplayer_interface_->WriteSample(player_, sample_type,
+                                         gathered_sbplayer_sample_infos.data(),
+                                         gathered_sbplayer_sample_infos.size());
+      } else {
+        sbplayer_interface_->WriteEndOfStream(
+            player_, DemuxerStreamTypeToSbMediaType(type));
+      }
+      return;
+    }
+
+    DecodingBuffers::iterator iter = decoding_buffers_.find(buffer->data());
+    if (iter == decoding_buffers_.end()) {
+      decoding_buffers_[buffer->data()] = std::make_pair(buffer, 1);
+    } else {
+      ++iter->second.second;
+    }
+
+    if (sample_type == kSbMediaTypeAudio && first_audio_sample_time_ == 0) {
+      first_audio_sample_time_ = SbTimeGetMonotonicNow();
+    } else if (sample_type == kSbMediaTypeVideo &&
+               first_video_sample_time_ == 0) {
+      first_video_sample_time_ = SbTimeGetMonotonicNow();
+    }
+
+    gathered_sbplayer_sample_infos_drm_info.push_back(SbDrmSampleInfo());
+    SbDrmSampleInfo* drm_info = &gathered_sbplayer_sample_infos_drm_info[i];
+
+    gathered_sbplayer_sample_infos_subsample_mapping.push_back(
+        SbDrmSubSampleMapping());
+    SbDrmSubSampleMapping* subsample_mapping =
+        &gathered_sbplayer_sample_infos_subsample_mapping[i];
+
+    drm_info->subsample_count = 0;
+    if (buffer->decrypt_config()) {
+      FillDrmSampleInfo(buffer, drm_info, subsample_mapping);
+    }
+
+    gathered_sbplayer_sample_infos_side_data.push_back(
+        SbPlayerSampleSideData());
+    SbPlayerSampleSideData* side_data =
+        &gathered_sbplayer_sample_infos_side_data[i];
+
+    SbPlayerSampleInfo sample_info = {};
+    sample_info.type = sample_type;
+    sample_info.buffer = buffer->data();
+    sample_info.buffer_size = buffer->data_size();
+    sample_info.timestamp = buffer->timestamp().InMicroseconds();
+
+    if (buffer->side_data_size() > 0) {
+      // We only support at most one side data currently.
+      side_data->data = buffer->side_data();
+      side_data->size = buffer->side_data_size();
+      sample_info.side_data = side_data;
+      sample_info.side_data_count = 1;
+    }
+
+    if (sample_type == kSbMediaTypeAudio) {
+      sample_info.audio_sample_info = audio_sample_info_;
+    } else {
+      DCHECK(sample_type == kSbMediaTypeVideo);
+      sample_info.video_sample_info = video_sample_info_;
+      sample_info.video_sample_info.is_key_frame = buffer->is_key_frame();
+    }
+    if (drm_info->subsample_count > 0) {
+      sample_info.drm_info = drm_info;
+    } else {
+      sample_info.drm_info = NULL;
+    }
+    gathered_sbplayer_sample_infos.push_back(sample_info);
   }
 
-  SbDrmSampleInfo drm_info;
-  SbDrmSubSampleMapping subsample_mapping;
-  drm_info.subsample_count = 0;
-  if (buffer->decrypt_config()) {
-    FillDrmSampleInfo(buffer, &drm_info, &subsample_mapping);
+  if (!gathered_sbplayer_sample_infos.empty()) {
+    sbplayer_interface_->WriteSample(player_, sample_type,
+                                     gathered_sbplayer_sample_infos.data(),
+                                     gathered_sbplayer_sample_infos.size());
   }
-
-  DCHECK_GT(SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, sample_type), 0);
-
-  SbPlayerSampleSideData side_data = {};
-  SbPlayerSampleInfo sample_info = {};
-  sample_info.type = sample_type;
-  sample_info.buffer = buffer->data();
-  sample_info.buffer_size = buffer->data_size();
-  sample_info.timestamp = buffer->timestamp().InMicroseconds();
-
-  if (buffer->side_data_size() > 0) {
-    // We only support at most one side data currently.
-    side_data.data = buffer->side_data();
-    side_data.size = buffer->side_data_size();
-    sample_info.side_data = &side_data;
-    sample_info.side_data_count = 1;
-  }
-
-  if (sample_type == kSbMediaTypeAudio) {
-    sample_info.audio_sample_info = audio_sample_info_;
-  } else {
-    DCHECK(sample_type == kSbMediaTypeVideo);
-    sample_info.video_sample_info = video_sample_info_;
-    sample_info.video_sample_info.is_key_frame = buffer->is_key_frame();
-  }
-  if (drm_info.subsample_count > 0) {
-    sample_info.drm_info = &drm_info;
-  } else {
-    sample_info.drm_info = NULL;
-  }
-  SbPlayerWriteSample2(player_, sample_type, &sample_info, 1);
 }
 
 SbDecodeTarget SbPlayerBridge::GetCurrentSbDecodeTarget() {
-  return SbPlayerGetCurrentFrame(player_);
+  return sbplayer_interface_->GetCurrentFrame(player_);
 }
 
 SbPlayerOutputMode SbPlayerBridge::GetSbPlayerOutputMode() {
@@ -734,7 +799,7 @@
   DCHECK(SbPlayerIsValid(player_));
 
   SbPlayerInfo2 info;
-  SbPlayerGetInfo2(player_, &info);
+  sbplayer_interface_->GetInfo(player_, &info);
 
   if (media_time) {
     *media_time =
@@ -757,8 +822,8 @@
   }
 
   auto& rect = *set_bounds_rect_;
-  SbPlayerSetBounds(player_, *set_bounds_z_index_, rect.x(), rect.y(),
-                    rect.width(), rect.height());
+  sbplayer_interface_->SetBounds(player_, *set_bounds_z_index_, rect.x(),
+                                 rect.y(), rect.width(), rect.height());
 }
 
 void SbPlayerBridge::ClearDecoderBufferCache() {
@@ -796,11 +861,24 @@
       break;
   }
 
+  DemuxerStream::Type stream_type =
+      ::media::SbMediaTypeToDemuxerStreamType(type);
+
+  if (stream_type == DemuxerStream::AUDIO && pending_audio_eos_buffer_) {
+    SbPlayerWriteEndOfStream(player_, type);
+    pending_audio_eos_buffer_ = false;
+    return;
+  } else if (stream_type == DemuxerStream::VIDEO && pending_video_eos_buffer_) {
+    SbPlayerWriteEndOfStream(player_, type);
+    pending_video_eos_buffer_ = false;
+    return;
+  }
+
+  auto max_number_of_samples_to_write =
+      SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, type);
   if (state_ == kResuming) {
-    DemuxerStream::Type stream_type =
-        ::media::SbMediaTypeToDemuxerStreamType(type);
     if (decoder_buffer_cache_.GetBuffer(stream_type)) {
-      WriteNextBufferFromCache(stream_type);
+      WriteNextBuffersFromCache(stream_type, max_number_of_samples_to_write);
       return;
     }
     if (!decoder_buffer_cache_.GetBuffer(DemuxerStream::AUDIO) &&
@@ -809,7 +887,7 @@
     }
   }
 
-  host_->OnNeedData(::media::SbMediaTypeToDemuxerStreamType(type));
+  host_->OnNeedData(stream_type, max_number_of_samples_to_write);
 }
 
 void SbPlayerBridge::OnPlayerStatus(SbPlayer player, SbPlayerState state,
@@ -835,9 +913,10 @@
     if (sb_player_state_initialized_time_ == 0) {
       sb_player_state_initialized_time_ = SbTimeGetMonotonicNow();
     }
-    SbPlayerSeek2(player_, preroll_timestamp_.InMicroseconds(), ticket_);
+    sbplayer_interface_->Seek(player_, preroll_timestamp_.InMicroseconds(),
+                              ticket_);
     SetVolume(volume_);
-    SbPlayerSetPlaybackRate(player_, playback_rate_);
+    sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
     return;
   }
   if (state == kSbPlayerStatePrerolling &&
@@ -942,18 +1021,19 @@
 }
 
 #if SB_HAS(PLAYER_WITH_URL)
-// static
 SbPlayerOutputMode SbPlayerBridge::ComputeSbUrlPlayerOutputMode(
     bool prefer_decode_to_texture) {
   // Try to choose the output mode according to the passed in value of
   // |prefer_decode_to_texture|.  If the preferred output mode is unavailable
   // though, fallback to an output mode that is available.
   SbPlayerOutputMode output_mode = kSbPlayerOutputModeInvalid;
-  if (SbUrlPlayerOutputModeSupported(kSbPlayerOutputModePunchOut)) {
+  if (sbplayer_interface_->GetUrlPlayerOutputModeSupported(
+          kSbPlayerOutputModePunchOut)) {
     output_mode = kSbPlayerOutputModePunchOut;
   }
   if ((prefer_decode_to_texture || output_mode == kSbPlayerOutputModeInvalid) &&
-      SbUrlPlayerOutputModeSupported(kSbPlayerOutputModeDecodeToTexture)) {
+      sbplayer_interface_->GetUrlPlayerOutputModeSupported(
+          kSbPlayerOutputModeDecodeToTexture)) {
     output_mode = kSbPlayerOutputModeDecodeToTexture;
   }
   CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
@@ -962,7 +1042,6 @@
 }
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
-// static
 SbPlayerOutputMode SbPlayerBridge::ComputeSbPlayerOutputMode(
     bool prefer_decode_to_texture) const {
   SbPlayerCreationParam creation_param = {};
@@ -977,7 +1056,8 @@
   } else {
     creation_param.output_mode = kSbPlayerOutputModePunchOut;
   }
-  auto output_mode = SbPlayerGetPreferredOutputMode(&creation_param);
+  auto output_mode =
+      sbplayer_interface_->GetPreferredOutputMode(&creation_param);
   CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
   return output_mode;
 }
diff --git a/cobalt/media/base/sbplayer_bridge.h b/cobalt/media/base/sbplayer_bridge.h
index e3768b8..1ea4cf6 100644
--- a/cobalt/media/base/sbplayer_bridge.h
+++ b/cobalt/media/base/sbplayer_bridge.h
@@ -18,6 +18,7 @@
 #include <map>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "base/memory/ref_counted.h"
 #include "base/message_loop/message_loop.h"
@@ -25,6 +26,7 @@
 #include "base/time/time.h"
 #include "cobalt/media/base/decode_target_provider.h"
 #include "cobalt/media/base/decoder_buffer_cache.h"
+#include "cobalt/media/base/sbplayer_interface.h"
 #include "cobalt/media/base/sbplayer_set_bounds_helper.h"
 #include "starboard/media.h"
 #include "starboard/player.h"
@@ -34,10 +36,6 @@
 #include "third_party/chromium/media/base/video_decoder_config.h"
 #include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
 
-#if SB_HAS(PLAYER_WITH_URL)
-#include SB_URL_PLAYER_INCLUDE_PATH
-#endif  // SB_HAS(PLAYER_WITH_URL)
-
 namespace cobalt {
 namespace media {
 
@@ -51,7 +49,8 @@
  public:
   class Host {
    public:
-    virtual void OnNeedData(DemuxerStream::Type type) = 0;
+    virtual void OnNeedData(DemuxerStream::Type type,
+                            int max_number_of_buffers_to_write) = 0;
     virtual void OnPlayerStatus(SbPlayerState state) = 0;
     virtual void OnPlayerError(SbPlayerError error,
                                const std::string& message) = 0;
@@ -68,7 +67,8 @@
   typedef base::Callback<void(const char*, const unsigned char*, unsigned)>
       OnEncryptedMediaInitDataEncounteredCB;
   // Create an SbPlayerBridge with url-based player.
-  SbPlayerBridge(const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+  SbPlayerBridge(SbPlayerInterface* interface,
+                 const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
                  const std::string& url, SbWindow window, Host* host,
                  SbPlayerSetBoundsHelper* set_bounds_helper,
                  bool allow_resume_after_suspend, bool prefer_decode_to_texture,
@@ -77,7 +77,8 @@
                  DecodeTargetProvider* const decode_target_provider);
 #endif  // SB_HAS(PLAYER_WITH_URL)
   // Create a SbPlayerBridge with normal player
-  SbPlayerBridge(const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+  SbPlayerBridge(SbPlayerInterface* interface,
+                 const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
                  const GetDecodeTargetGraphicsContextProviderFunc&
                      get_decode_target_graphics_context_provider_func,
                  const AudioDecoderConfig& audio_config,
@@ -99,8 +100,8 @@
   void UpdateVideoConfig(const VideoDecoderConfig& video_config,
                          const std::string& mime_type);
 
-  void WriteBuffer(DemuxerStream::Type type,
-                   const scoped_refptr<DecoderBuffer>& buffer);
+  void WriteBuffers(DemuxerStream::Type type,
+                    const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
 
   void SetBounds(int z_index, const gfx::Rect& rect);
 
@@ -192,9 +193,11 @@
 #endif  // SB_HAS(PLAYER_WITH_URL)
   void CreatePlayer();
 
-  void WriteNextBufferFromCache(DemuxerStream::Type type);
-  void WriteBufferInternal(DemuxerStream::Type type,
-                           const scoped_refptr<DecoderBuffer>& buffer);
+  void WriteNextBuffersFromCache(DemuxerStream::Type type,
+                                 int max_buffers_per_write);
+  void WriteBuffersInternal(
+      DemuxerStream::Type type,
+      const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
 
   void GetInfo_Locked(uint32* video_frames_decoded,
                       uint32* video_frames_dropped,
@@ -220,7 +223,7 @@
                                  const void* sample_buffer);
 
 #if SB_HAS(PLAYER_WITH_URL)
-  static SbPlayerOutputMode ComputeSbUrlPlayerOutputMode(
+  SbPlayerOutputMode ComputeSbUrlPlayerOutputMode(
       bool prefer_decode_to_texture);
 #endif  // SB_HAS(PLAYER_WITH_URL)
   // Returns the output mode that should be used for a video with the given
@@ -234,6 +237,7 @@
 #if SB_HAS(PLAYER_WITH_URL)
   std::string url_;
 #endif  // SB_HAS(PLAYER_WITH_URL)
+  SbPlayerInterface* sbplayer_interface_;
   const scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
   const GetDecodeTargetGraphicsContextProviderFunc
       get_decode_target_graphics_context_provider_func_;
@@ -299,6 +303,10 @@
 #if SB_HAS(PLAYER_WITH_URL)
   const bool is_url_based_;
 #endif  // SB_HAS(PLAYER_WITH_URL)
+
+  // Used for Gathered Sample Write.
+  bool pending_audio_eos_buffer_ = false;
+  bool pending_video_eos_buffer_ = false;
 };
 
 }  // namespace media
diff --git a/cobalt/media/base/sbplayer_interface.cc b/cobalt/media/base/sbplayer_interface.cc
new file mode 100644
index 0000000..7c44dae
--- /dev/null
+++ b/cobalt/media/base/sbplayer_interface.cc
@@ -0,0 +1,113 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/base/sbplayer_interface.h"
+
+namespace cobalt {
+namespace media {
+
+SbPlayer DefaultSbPlayerInterface::Create(
+    SbWindow window, const SbPlayerCreationParam* creation_param,
+    SbPlayerDeallocateSampleFunc sample_deallocate_func,
+    SbPlayerDecoderStatusFunc decoder_status_func,
+    SbPlayerStatusFunc player_status_func, SbPlayerErrorFunc player_error_func,
+    void* context, SbDecodeTargetGraphicsContextProvider* context_provider) {
+  return SbPlayerCreate(window, creation_param, sample_deallocate_func,
+                        decoder_status_func, player_status_func,
+                        player_error_func, context, context_provider);
+}
+
+SbPlayerOutputMode DefaultSbPlayerInterface::GetPreferredOutputMode(
+    const SbPlayerCreationParam* creation_param) {
+  return SbPlayerGetPreferredOutputMode(creation_param);
+}
+
+void DefaultSbPlayerInterface::Destroy(SbPlayer player) {
+  SbPlayerDestroy(player);
+}
+
+void DefaultSbPlayerInterface::Seek(SbPlayer player, SbTime seek_to_timestamp,
+                                    int ticket) {
+  SbPlayerSeek2(player, seek_to_timestamp, ticket);
+}
+
+void DefaultSbPlayerInterface::WriteSample(
+    SbPlayer player, SbMediaType sample_type,
+    const SbPlayerSampleInfo* sample_infos, int number_of_sample_infos) {
+  SbPlayerWriteSample2(player, sample_type, sample_infos,
+                       number_of_sample_infos);
+}
+
+int DefaultSbPlayerInterface::GetMaximumNumberOfSamplesPerWrite(
+    SbPlayer player, SbMediaType sample_type) {
+  return SbPlayerGetMaximumNumberOfSamplesPerWrite(player, sample_type);
+}
+
+void DefaultSbPlayerInterface::WriteEndOfStream(SbPlayer player,
+                                                SbMediaType stream_type) {
+  SbPlayerWriteEndOfStream(player, stream_type);
+}
+
+void DefaultSbPlayerInterface::SetBounds(SbPlayer player, int z_index, int x,
+                                         int y, int width, int height) {
+  SbPlayerSetBounds(player, z_index, x, y, width, height);
+}
+
+bool DefaultSbPlayerInterface::SetPlaybackRate(SbPlayer player,
+                                               double playback_rate) {
+  return SbPlayerSetPlaybackRate(player, playback_rate);
+}
+
+void DefaultSbPlayerInterface::SetVolume(SbPlayer player, double volume) {
+  SbPlayerSetVolume(player, volume);
+}
+
+void DefaultSbPlayerInterface::GetInfo(SbPlayer player,
+                                       SbPlayerInfo2* out_player_info2) {
+  SbPlayerGetInfo2(player, out_player_info2);
+}
+
+SbDecodeTarget DefaultSbPlayerInterface::GetCurrentFrame(SbPlayer player) {
+  return SbPlayerGetCurrentFrame(player);
+}
+
+#if SB_HAS(PLAYER_WITH_URL)
+SbPlayer DefaultSbPlayerInterface::CreateUrlPlayer(
+    const char* url, SbWindow window, SbPlayerStatusFunc player_status_func,
+    SbPlayerEncryptedMediaInitDataEncounteredCB
+        encrypted_media_init_data_encountered_cb,
+    SbPlayerErrorFunc player_error_func, void* context) {
+  return SbUrlPlayerCreate(url, window, player_status_func,
+                           encrypted_media_init_data_encountered_cb,
+                           player_error_func, context);
+}
+
+void DefaultSbPlayerInterface::SetUrlPlayerDrmSystem(SbPlayer player,
+                                                     SbDrmSystem drm_system) {
+  SbUrlPlayerSetDrmSystem(player, drm_system);
+}
+
+bool DefaultSbPlayerInterface::GetUrlPlayerOutputModeSupported(
+    SbPlayerOutputMode output_mode) {
+  return SbUrlPlayerOutputModeSupported(output_mode);
+}
+
+void DefaultSbPlayerInterface::GetUrlPlayerExtraInfo(
+    SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) {
+  SbUrlPlayerGetExtraInfo(player, out_url_player_info);
+}
+#endif  // SB_HAS(PLAYER_WITH_URL)
+
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media/base/sbplayer_interface.h b/cobalt/media/base/sbplayer_interface.h
new file mode 100644
index 0000000..d240e0b
--- /dev/null
+++ b/cobalt/media/base/sbplayer_interface.h
@@ -0,0 +1,115 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
+#define COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
+
+#include "starboard/player.h"
+
+#if SB_HAS(PLAYER_WITH_URL)
+#include SB_URL_PLAYER_INCLUDE_PATH
+#endif  // SB_HAS(PLAYER_WITH_URL)
+
+namespace cobalt {
+namespace media {
+
+class SbPlayerInterface {
+ public:
+  virtual ~SbPlayerInterface() {}
+
+  virtual SbPlayer Create(
+      SbWindow window, const SbPlayerCreationParam* creation_param,
+      SbPlayerDeallocateSampleFunc sample_deallocate_func,
+      SbPlayerDecoderStatusFunc decoder_status_func,
+      SbPlayerStatusFunc player_status_func,
+      SbPlayerErrorFunc player_error_func, void* context,
+      SbDecodeTargetGraphicsContextProvider* context_provider) = 0;
+  virtual SbPlayerOutputMode GetPreferredOutputMode(
+      const SbPlayerCreationParam* creation_param) = 0;
+  virtual void Destroy(SbPlayer player) = 0;
+  virtual void Seek(SbPlayer player, SbTime seek_to_timestamp, int ticket) = 0;
+  virtual void WriteSample(SbPlayer player, SbMediaType sample_type,
+                           const SbPlayerSampleInfo* sample_infos,
+                           int number_of_sample_infos) = 0;
+  virtual int GetMaximumNumberOfSamplesPerWrite(SbPlayer player,
+                                                SbMediaType sample_type) = 0;
+  virtual void WriteEndOfStream(SbPlayer player, SbMediaType stream_type) = 0;
+  virtual void SetBounds(SbPlayer player, int z_index, int x, int y, int width,
+                         int height) = 0;
+  virtual bool SetPlaybackRate(SbPlayer player, double playback_rate) = 0;
+  virtual void SetVolume(SbPlayer player, double volume) = 0;
+  virtual void GetInfo(SbPlayer player, SbPlayerInfo2* out_player_info2) = 0;
+  virtual SbDecodeTarget GetCurrentFrame(SbPlayer player) = 0;
+
+#if SB_HAS(PLAYER_WITH_URL)
+  virtual SbPlayer CreateUrlPlayer(const char* url, SbWindow window,
+                                   SbPlayerStatusFunc player_status_func,
+                                   SbPlayerEncryptedMediaInitDataEncounteredCB
+                                       encrypted_media_init_data_encountered_cb,
+                                   SbPlayerErrorFunc player_error_func,
+                                   void* context) = 0;
+  virtual void SetUrlPlayerDrmSystem(SbPlayer player,
+                                     SbDrmSystem drm_system) = 0;
+  virtual bool GetUrlPlayerOutputModeSupported(
+      SbPlayerOutputMode output_mode) = 0;
+  virtual void GetUrlPlayerExtraInfo(
+      SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) = 0;
+#endif  // SB_HAS(PLAYER_WITH_URL)
+};
+
+class DefaultSbPlayerInterface final : public SbPlayerInterface {
+ public:
+  SbPlayer Create(
+      SbWindow window, const SbPlayerCreationParam* creation_param,
+      SbPlayerDeallocateSampleFunc sample_deallocate_func,
+      SbPlayerDecoderStatusFunc decoder_status_func,
+      SbPlayerStatusFunc player_status_func,
+      SbPlayerErrorFunc player_error_func, void* context,
+      SbDecodeTargetGraphicsContextProvider* context_provider) override;
+  SbPlayerOutputMode GetPreferredOutputMode(
+      const SbPlayerCreationParam* creation_param) override;
+  void Destroy(SbPlayer player) override;
+  void Seek(SbPlayer player, SbTime seek_to_timestamp, int ticket) override;
+  void WriteSample(SbPlayer player, SbMediaType sample_type,
+                   const SbPlayerSampleInfo* sample_infos,
+                   int number_of_sample_infos) override;
+  int GetMaximumNumberOfSamplesPerWrite(SbPlayer player,
+                                        SbMediaType sample_type) override;
+  void WriteEndOfStream(SbPlayer player, SbMediaType stream_type) override;
+  void SetBounds(SbPlayer player, int z_index, int x, int y, int width,
+                 int height) override;
+  bool SetPlaybackRate(SbPlayer player, double playback_rate) override;
+  void SetVolume(SbPlayer player, double volume) override;
+  void GetInfo(SbPlayer player, SbPlayerInfo2* out_player_info2) override;
+  SbDecodeTarget GetCurrentFrame(SbPlayer player) override;
+
+#if SB_HAS(PLAYER_WITH_URL)
+  SbPlayer CreateUrlPlayer(const char* url, SbWindow window,
+                           SbPlayerStatusFunc player_status_func,
+                           SbPlayerEncryptedMediaInitDataEncounteredCB
+                               encrypted_media_init_data_encountered_cb,
+                           SbPlayerErrorFunc player_error_func,
+                           void* context) override;
+  void SetUrlPlayerDrmSystem(SbPlayer player, SbDrmSystem drm_system) override;
+  bool GetUrlPlayerOutputModeSupported(SbPlayerOutputMode output_mode) override;
+  void GetUrlPlayerExtraInfo(
+      SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) override;
+#endif  // SB_HAS(PLAYER_WITH_URL)
+};
+
+
+}  // namespace media
+}  // namespace cobalt
+
+#endif  // COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc
index 6c158bc..0d69a83 100644
--- a/cobalt/media/base/sbplayer_pipeline.cc
+++ b/cobalt/media/base/sbplayer_pipeline.cc
@@ -98,12 +98,12 @@
  public:
   // Constructs a media pipeline that will execute on |task_runner|.
   SbPlayerPipeline(
-      PipelineWindow window,
+      SbPlayerInterface* interface, PipelineWindow window,
       const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
       const GetDecodeTargetGraphicsContextProviderFunc&
           get_decode_target_graphics_context_provider_func,
-      bool allow_resume_after_suspend, MediaLog* media_log,
-      DecodeTargetProvider* decode_target_provider);
+      bool allow_resume_after_suspend, bool allow_batched_sample_write,
+      MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
   ~SbPlayerPipeline() override;
 
   void Suspend() override;
@@ -175,18 +175,20 @@
   void OnDemuxerInitialized(PipelineStatus status);
   void OnDemuxerSeeked(PipelineStatus status);
   void OnDemuxerStopped();
-  void OnDemuxerStreamRead(DemuxerStream::Type type,
-                           DemuxerStream::Status status,
-                           scoped_refptr<DecoderBuffer> buffer);
+  void OnDemuxerStreamRead(
+      DemuxerStream::Type type, int max_number_buffers_to_read,
+      DemuxerStream::Status status,
+      const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
   // SbPlayerBridge::Host implementation.
-  void OnNeedData(DemuxerStream::Type type) override;
+  void OnNeedData(DemuxerStream::Type type,
+                  int max_number_of_buffers_to_write) override;
   void OnPlayerStatus(SbPlayerState state) override;
   void OnPlayerError(SbPlayerError error, const std::string& message) override;
 
   // Used to make a delayed call to OnNeedData() if |audio_read_delayed_| is
   // true. If |audio_read_delayed_| is false, that means the delayed call has
   // been cancelled due to a seek.
-  void DelayedNeedData();
+  void DelayedNeedData(int max_number_of_buffers_to_write);
 
   void UpdateDecoderConfig(DemuxerStream* stream);
   void CallSeekCB(PipelineStatus status, const std::string& error_message);
@@ -208,16 +210,26 @@
 
   void RunSetDrmSystemReadyCB(DrmSystemReadyCB drm_system_ready_cb);
 
+  void SetReadInProgress(DemuxerStream::Type type, bool in_progress);
+  bool GetReadInProgress(DemuxerStream::Type type) const;
+
   // An identifier string for the pipeline, used in CVal to identify multiple
   // pipelines.
   const std::string pipeline_identifier_;
 
+  // A wrapped interface of starboard player functions, which will be used in
+  // underlying SbPlayerBridge.
+  SbPlayerInterface* sbplayer_interface_;
+
   // Message loop used to execute pipeline tasks.  It is thread-safe.
   scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
 
   // Whether we should save DecoderBuffers for resume after suspend.
   const bool allow_resume_after_suspend_;
 
+  // Whether we enable batched sample write functionality.
+  const bool allow_batched_sample_write_;
+
   // The window this player associates with.  It should only be assigned in the
   // dtor and accessed once by SbPlayerCreate().
   PipelineWindow window_;
@@ -340,16 +352,18 @@
 };
 
 SbPlayerPipeline::SbPlayerPipeline(
-    PipelineWindow window,
+    SbPlayerInterface* interface, PipelineWindow window,
     const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
     const GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
-    bool allow_resume_after_suspend, MediaLog* media_log,
-    DecodeTargetProvider* decode_target_provider)
+    bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    MediaLog* media_log, DecodeTargetProvider* decode_target_provider)
     : pipeline_identifier_(
           base::StringPrintf("%X", g_pipeline_identifier_counter++)),
+      sbplayer_interface_(interface),
       task_runner_(task_runner),
       allow_resume_after_suspend_(allow_resume_after_suspend),
+      allow_batched_sample_write_(allow_batched_sample_write),
       window_(window),
       get_decode_target_graphics_context_provider_func_(
           get_decode_target_graphics_context_provider_func),
@@ -910,8 +924,9 @@
     base::AutoLock auto_lock(lock_);
     LOG(INFO) << "Creating SbPlayerBridge with url: " << source_url;
     player_bridge_.reset(new SbPlayerBridge(
-        task_runner_, source_url, window_, this, set_bounds_helper_.get(),
-        allow_resume_after_suspend_, *decode_to_texture_output_mode_,
+        sbplayer_interface_, task_runner_, source_url, window_, this,
+        set_bounds_helper_.get(), allow_resume_after_suspend_,
+        *decode_to_texture_output_mode_,
         on_encrypted_media_init_data_encountered_cb_, decode_target_provider_));
     if (player_bridge_->IsValid()) {
       SetPlaybackRateTask(playback_rate_);
@@ -1012,9 +1027,10 @@
     player_bridge_.reset();
     LOG(INFO) << "Creating SbPlayerBridge.";
     player_bridge_.reset(new SbPlayerBridge(
-        task_runner_, get_decode_target_graphics_context_provider_func_,
-        audio_config, audio_mime_type, video_config, video_mime_type, window_,
-        drm_system, this, set_bounds_helper_.get(), allow_resume_after_suspend_,
+        sbplayer_interface_, task_runner_,
+        get_decode_target_graphics_context_provider_func_, audio_config,
+        audio_mime_type, video_config, video_mime_type, window_, drm_system,
+        this, set_bounds_helper_.get(), allow_resume_after_suspend_,
         *decode_to_texture_output_mode_, decode_target_provider_,
         max_video_capabilities_));
     if (player_bridge_->IsValid()) {
@@ -1160,8 +1176,9 @@
 }
 
 void SbPlayerPipeline::OnDemuxerStreamRead(
-    DemuxerStream::Type type, DemuxerStream::Status status,
-    scoped_refptr<DecoderBuffer> buffer) {
+    DemuxerStream::Type type, int max_number_buffers_to_read,
+    DemuxerStream::Status status,
+    const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
 #if SB_HAS(PLAYER_WITH_URL)
   DCHECK(!is_url_based_);
 #endif  // SB_HAS(PLAYER_WITH_URL)
@@ -1170,8 +1187,9 @@
 
   if (!task_runner_->BelongsToCurrentThread()) {
     task_runner_->PostTask(
-        FROM_HERE, base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
-                                  type, status, buffer));
+        FROM_HERE,
+        base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type,
+                       max_number_buffers_to_read, status, buffers));
     return;
   }
 
@@ -1185,13 +1203,9 @@
   }
 
   if (status == DemuxerStream::kAborted) {
-    if (type == DemuxerStream::AUDIO) {
-      DCHECK(audio_read_in_progress_);
-      audio_read_in_progress_ = false;
-    } else {
-      DCHECK(video_read_in_progress_);
-      video_read_in_progress_ = false;
-    }
+    DCHECK(GetReadInProgress(type));
+    SetReadInProgress(type, false);
+
     if (!seek_cb_.is_null()) {
       CallSeekCB(::media::PIPELINE_OK, "");
     }
@@ -1200,26 +1214,31 @@
 
   if (status == DemuxerStream::kConfigChanged) {
     UpdateDecoderConfig(stream);
-    stream->Read(
-        base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type));
+    stream->Read(max_number_buffers_to_read,
+                 base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
+                                type, max_number_buffers_to_read));
     return;
   }
 
   if (type == DemuxerStream::AUDIO) {
-    audio_read_in_progress_ = false;
-    playback_statistics_.OnAudioAU(buffer);
-    if (!buffer->end_of_stream()) {
-      timestamp_of_last_written_audio_ = buffer->timestamp().ToSbTime();
+    for (const auto& buffer : buffers) {
+      playback_statistics_.OnAudioAU(buffer);
+      if (!buffer->end_of_stream()) {
+        timestamp_of_last_written_audio_ = buffer->timestamp().ToSbTime();
+      }
     }
   } else {
-    video_read_in_progress_ = false;
-    playback_statistics_.OnVideoAU(buffer);
+    for (const auto& buffer : buffers) {
+      playback_statistics_.OnVideoAU(buffer);
+    }
   }
+  SetReadInProgress(type, false);
 
-  player_bridge_->WriteBuffer(type, buffer);
+  player_bridge_->WriteBuffers(type, buffers);
 }
 
-void SbPlayerPipeline::OnNeedData(DemuxerStream::Type type) {
+void SbPlayerPipeline::OnNeedData(DemuxerStream::Type type,
+                                  int max_number_of_buffers_to_write) {
 #if SB_HAS(PLAYER_WITH_URL)
   DCHECK(!is_url_based_);
 #endif  // SB_HAS(PLAYER_WITH_URL)
@@ -1230,15 +1249,18 @@
     return;
   }
 
+  int max_buffers =
+      allow_batched_sample_write_ ? max_number_of_buffers_to_write : 1;
+
+  if (GetReadInProgress(type)) return;
+
   if (type == DemuxerStream::AUDIO) {
     if (!audio_stream_) {
       LOG(WARNING)
           << "Calling OnNeedData() for audio data during audioless playback";
       return;
     }
-    if (audio_read_in_progress_) {
-      return;
-    }
+
     // If we haven't checked the media time recently, update it now.
     if (SbTimeGetNow() - last_time_media_time_retrieved_ >
         kMediaTimeCheckInterval) {
@@ -1257,7 +1279,8 @@
           timestamp_of_last_written_audio_ - last_media_time_;
       if (time_ahead_of_playback > (kAudioLimit + kMediaTimeCheckInterval)) {
         task_runner_->PostDelayedTask(
-            FROM_HERE, base::Bind(&SbPlayerPipeline::DelayedNeedData, this),
+            FROM_HERE,
+            base::Bind(&SbPlayerPipeline::DelayedNeedData, this, max_buffers),
             base::TimeDelta::FromMicroseconds(kMediaTimeCheckInterval));
         audio_read_delayed_ = true;
         return;
@@ -1268,16 +1291,15 @@
     audio_read_in_progress_ = true;
   } else {
     DCHECK_EQ(type, DemuxerStream::VIDEO);
-    if (video_read_in_progress_) {
-      return;
-    }
     video_read_in_progress_ = true;
   }
   DemuxerStream* stream =
       type == DemuxerStream::AUDIO ? audio_stream_ : video_stream_;
   DCHECK(stream);
-  stream->Read(
-      base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type));
+
+  stream->Read(max_buffers,
+               base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
+                              type, max_buffers));
 }
 
 void SbPlayerPipeline::OnPlayerStatus(SbPlayerState state) {
@@ -1373,10 +1395,10 @@
   }
 }
 
-void SbPlayerPipeline::DelayedNeedData() {
+void SbPlayerPipeline::DelayedNeedData(int max_number_of_buffers_to_write) {
   DCHECK(task_runner_->BelongsToCurrentThread());
   if (audio_read_delayed_) {
-    OnNeedData(DemuxerStream::AUDIO);
+    OnNeedData(DemuxerStream::AUDIO, max_number_of_buffers_to_write);
   }
 }
 
@@ -1545,19 +1567,34 @@
   set_drm_system_ready_cb_.Run(drm_system_ready_cb);
 }
 
+void SbPlayerPipeline::SetReadInProgress(DemuxerStream::Type type,
+                                         bool in_progress) {
+  if (type == DemuxerStream::AUDIO)
+    audio_read_in_progress_ = in_progress;
+  else
+    video_read_in_progress_ = in_progress;
+}
+
+bool SbPlayerPipeline::GetReadInProgress(DemuxerStream::Type type) const {
+  if (type == DemuxerStream::AUDIO) return audio_read_in_progress_;
+  return video_read_in_progress_;
+}
+
 }  // namespace
 
 // static
 scoped_refptr<Pipeline> Pipeline::Create(
-    PipelineWindow window,
+    SbPlayerInterface* interface, PipelineWindow window,
     const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
     const GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
-    bool allow_resume_after_suspend, MediaLog* media_log,
-    DecodeTargetProvider* decode_target_provider) {
-  return new SbPlayerPipeline(
-      window, task_runner, get_decode_target_graphics_context_provider_func,
-      allow_resume_after_suspend, media_log, decode_target_provider);
+    bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    MediaLog* media_log, DecodeTargetProvider* decode_target_provider) {
+  return new SbPlayerPipeline(interface, window, task_runner,
+                              get_decode_target_graphics_context_provider_func,
+                              allow_resume_after_suspend,
+                              allow_batched_sample_write, media_log,
+                              decode_target_provider);
 }
 
 }  // namespace media
diff --git a/cobalt/media/decoder_buffer_allocator.cc b/cobalt/media/decoder_buffer_allocator.cc
index 57b9974..db38a23 100644
--- a/cobalt/media/decoder_buffer_allocator.cc
+++ b/cobalt/media/decoder_buffer_allocator.cc
@@ -162,6 +162,48 @@
   }
 }
 
+int DecoderBufferAllocator::GetAudioBufferBudget() const {
+  return SbMediaGetAudioBufferBudget();
+}
+
+int DecoderBufferAllocator::GetBufferAlignment() const {
+#if SB_API_VERSION >= 14
+  return SbMediaGetBufferAlignment();
+#else   // SB_API_VERSION >= 14
+  return std::max(SbMediaGetBufferAlignment(kSbMediaTypeAudio),
+                  SbMediaGetBufferAlignment(kSbMediaTypeVideo));
+#endif  // SB_API_VERSION >= 14
+}
+
+int DecoderBufferAllocator::GetBufferPadding() const {
+#if SB_API_VERSION >= 14
+  return SbMediaGetBufferPadding();
+#else   // SB_API_VERSION >= 14
+  return std::max(SbMediaGetBufferPadding(kSbMediaTypeAudio),
+                  SbMediaGetBufferPadding(kSbMediaTypeVideo));
+#endif  // SB_API_VERSION >= 14
+}
+
+SbTime DecoderBufferAllocator::GetBufferGarbageCollectionDurationThreshold()
+    const {
+  return SbMediaGetBufferGarbageCollectionDurationThreshold();
+}
+
+int DecoderBufferAllocator::GetProgressiveBufferBudget(
+    SbMediaVideoCodec codec, int resolution_width, int resolution_height,
+    int bits_per_pixel) const {
+  return SbMediaGetProgressiveBufferBudget(codec, resolution_width,
+                                           resolution_height, bits_per_pixel);
+}
+
+int DecoderBufferAllocator::GetVideoBufferBudget(SbMediaVideoCodec codec,
+                                                 int resolution_width,
+                                                 int resolution_height,
+                                                 int bits_per_pixel) const {
+  return SbMediaGetVideoBufferBudget(codec, resolution_width, resolution_height,
+                                     bits_per_pixel);
+}
+
 size_t DecoderBufferAllocator::GetAllocatedMemory() const {
   if (!using_memory_pool_) {
     return sbmemory_bytes_used_.load();
diff --git a/cobalt/media/decoder_buffer_allocator.h b/cobalt/media/decoder_buffer_allocator.h
index 31c3df4..e001547 100644
--- a/cobalt/media/decoder_buffer_allocator.h
+++ b/cobalt/media/decoder_buffer_allocator.h
@@ -43,6 +43,17 @@
   void* Allocate(size_t size, size_t alignment) override;
   void Free(void* p, size_t size) override;
 
+  int GetAudioBufferBudget() const override;
+  int GetBufferAlignment() const override;
+  int GetBufferPadding() const override;
+  SbTime GetBufferGarbageCollectionDurationThreshold() const override;
+  int GetProgressiveBufferBudget(SbMediaVideoCodec codec, int resolution_width,
+                                 int resolution_height,
+                                 int bits_per_pixel) const override;
+  int GetVideoBufferBudget(SbMediaVideoCodec codec, int resolution_width,
+                           int resolution_height,
+                           int bits_per_pixel) const override;
+
   // DecoderBufferMemoryInfo methods.
   size_t GetAllocatedMemory() const override;
   size_t GetCurrentMemoryCapacity() const override;
diff --git a/cobalt/media/fetcher_buffered_data_source.cc b/cobalt/media/fetcher_buffered_data_source.cc
deleted file mode 100644
index c8a9595..0000000
--- a/cobalt/media/fetcher_buffered_data_source.cc
+++ /dev/null
@@ -1,520 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/media/fetcher_buffered_data_source.h"
-
-#include <algorithm>
-#include <memory>
-#include <string>
-#include <utility>
-
-#include "base/bind.h"
-#include "base/callback_helpers.h"
-#include "base/strings/string_number_conversions.h"
-#include "cobalt/base/polymorphic_downcast.h"
-#include "cobalt/loader/cors_preflight.h"
-#include "cobalt/loader/url_fetcher_string_writer.h"
-#include "net/http/http_response_headers.h"
-#include "net/http/http_status_code.h"
-
-namespace cobalt {
-namespace media {
-
-namespace {
-
-const uint32 kBackwardBytes = 256 * 1024;
-const uint32 kInitialForwardBytes = 3 * 256 * 1024;
-const uint32 kInitialBufferCapacity = kBackwardBytes + kInitialForwardBytes;
-
-}  // namespace
-
-using base::CircularBufferShell;
-
-FetcherBufferedDataSource::FetcherBufferedDataSource(
-    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
-    const GURL& url, const csp::SecurityCallback& security_callback,
-    network::NetworkModule* network_module, loader::RequestMode request_mode,
-    loader::Origin origin)
-    : task_runner_(task_runner),
-      url_(url),
-      network_module_(network_module),
-      is_downloading_(false),
-      buffer_(kInitialBufferCapacity, CircularBufferShell::kReserve),
-      buffer_offset_(0),
-      error_occured_(false),
-      last_request_offset_(0),
-      last_request_size_(0),
-      last_read_position_(0),
-      pending_read_position_(0),
-      pending_read_size_(0),
-      pending_read_data_(NULL),
-      security_callback_(security_callback),
-      request_mode_(request_mode),
-      document_origin_(origin),
-      is_origin_safe_(false) {
-  DCHECK(task_runner_);
-  DCHECK(network_module);
-}
-
-FetcherBufferedDataSource::~FetcherBufferedDataSource() {
-  if (cancelable_create_fetcher_closure_) {
-    cancelable_create_fetcher_closure_->Cancel();
-  }
-}
-
-void FetcherBufferedDataSource::Read(int64 position, int size, uint8* data,
-                                     const ReadCB& read_cb) {
-  DCHECK_GE(position, 0);
-  DCHECK_GE(size, 0);
-
-  if (position < 0 || size < 0) {
-    read_cb.Run(kInvalidSize);
-    return;
-  }
-
-  base::AutoLock auto_lock(lock_);
-  Read_Locked(static_cast<uint64>(position), static_cast<size_t>(size), data,
-              read_cb);
-}
-
-void FetcherBufferedDataSource::Stop() {
-  {
-    base::AutoLock auto_lock(lock_);
-
-    if (!pending_read_cb_.is_null()) {
-      base::ResetAndReturn(&pending_read_cb_).Run(0);
-    }
-    // From this moment on, any call to Read() should be treated as an error.
-    // Note that we cannot reset |fetcher_| here because of:
-    // 1. Fetcher has to be destroyed on the thread that it is created, however
-    //    Stop() is usually called from the pipeline thread where |fetcher_| is
-    //    created on the web thread.
-    // 2. We cannot post a task to the web thread to destroy |fetcher_| as the
-    //    web thread is blocked by WMPI::Destroy().
-    // Once error_occured_ is set to true, the fetcher callbacks return
-    // immediately so it is safe to destroy |fetcher_| inside the dtor.
-    error_occured_ = true;
-  }
-}
-
-bool FetcherBufferedDataSource::GetSize(int64* size_out) {
-  base::AutoLock auto_lock(lock_);
-
-  if (total_size_of_resource_) {
-    *size_out = static_cast<int64>(total_size_of_resource_.value());
-    DCHECK_GE(*size_out, 0);
-  } else {
-    *size_out = kInvalidSize;
-  }
-  return *size_out != kInvalidSize;
-}
-
-void FetcherBufferedDataSource::SetDownloadingStatusCB(
-    const DownloadingStatusCB& downloading_status_cb) {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-
-  DCHECK(!downloading_status_cb.is_null());
-  DCHECK(downloading_status_cb_.is_null());
-  downloading_status_cb_ = downloading_status_cb;
-}
-
-void FetcherBufferedDataSource::OnURLFetchResponseStarted(
-    const net::URLFetcher* source) {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-
-  base::AutoLock auto_lock(lock_);
-
-  if (fetcher_.get() != source || error_occured_) {
-    return;
-  }
-
-  if (!source->GetStatus().is_success()) {
-    // The error will be handled on OnURLFetchComplete()
-    error_occured_ = true;
-    return;
-  } else if (source->GetResponseCode() == -1) {
-    // Could be a file URL, so we won't expect headers.
-    return;
-  }
-
-  // In the event of a redirect, re-check the security policy.
-  if (source->GetURL() != source->GetOriginalURL()) {
-    if (!security_callback_.is_null() &&
-        !security_callback_.Run(source->GetURL(), true /*did redirect*/)) {
-      error_occured_ = true;
-      if (!pending_read_cb_.is_null()) {
-        base::ResetAndReturn(&pending_read_cb_).Run(-1);
-      }
-      return;
-    }
-  }
-
-  scoped_refptr<net::HttpResponseHeaders> headers =
-      source->GetResponseHeaders();
-  DCHECK(headers);
-
-  if (!is_origin_safe_) {
-    if (loader::CORSPreflight::CORSCheck(
-            *headers, document_origin_.SerializedOrigin(),
-            request_mode_ == loader::kCORSModeIncludeCredentials)) {
-      is_origin_safe_ = true;
-    } else {
-      error_occured_ = true;
-      if (!pending_read_cb_.is_null()) {
-        base::ResetAndReturn(&pending_read_cb_).Run(-1);
-      }
-      return;
-    }
-  }
-
-  uint64 first_byte_offset = 0;
-
-  if (headers->response_code() == net::HTTP_PARTIAL_CONTENT) {
-    int64 first_byte_position = -1;
-    int64 last_byte_position = -1;
-    int64 instance_length = -1;
-    bool is_range_valid = headers && headers->GetContentRangeFor206(
-                                         &first_byte_position,
-                                         &last_byte_position, &instance_length);
-    if (is_range_valid) {
-      if (first_byte_position >= 0) {
-        first_byte_offset = static_cast<uint64>(first_byte_position);
-      }
-      if (!total_size_of_resource_ && instance_length > 0) {
-        total_size_of_resource_ = static_cast<uint64>(instance_length);
-      }
-    }
-  }
-
-  DCHECK_LE(first_byte_offset, last_request_offset_);
-
-  if (first_byte_offset < last_request_offset_) {
-    last_request_size_ += last_request_offset_ - first_byte_offset;
-    last_request_offset_ = first_byte_offset;
-  }
-}
-
-void FetcherBufferedDataSource::OnURLFetchDownloadProgress(
-    const net::URLFetcher* source, int64_t current, int64_t total,
-    int64_t current_network_bytes) {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-  auto* download_data_writer =
-      base::polymorphic_downcast<loader::URLFetcherStringWriter*>(
-          source->GetResponseWriter());
-  std::string downloaded_data;
-  download_data_writer->GetAndResetData(&downloaded_data);
-  size_t size = downloaded_data.size();
-  if (size == 0) {
-    return;
-  }
-  const uint8* data = reinterpret_cast<const uint8*>(downloaded_data.data());
-  base::AutoLock auto_lock(lock_);
-
-  if (fetcher_.get() != source || error_occured_) {
-    return;
-  }
-
-  size = static_cast<size_t>(std::min<uint64>(size, last_request_size_));
-
-  if (size == 0 || size > buffer_.GetMaxCapacity()) {
-    // The server side doesn't support range request.  Delete the fetcher to
-    // stop the current request.
-    LOG(ERROR)
-        << "FetcherBufferedDataSource::OnURLFetchDownloadProgress: server "
-        << "doesn't support range requests (e.g. Python SimpleHTTPServer). "
-        << "Please use a server that supports range requests (e.g. Flask).";
-    error_occured_ = true;
-    fetcher_.reset();
-    ProcessPendingRead_Locked();
-    UpdateDownloadingStatus(/* is_downloading = */ false);
-    return;
-  }
-
-  // Because we can only append data into the buffer_.  We just check if the
-  // position of the first byte of the newly received data is overlapped with
-  // the range of the buffer_.  If not, we can discard all data in the buffer_
-  // as there is no way to represent a gap or to prepend data.
-  if (last_request_offset_ < buffer_offset_ ||
-      last_request_offset_ > buffer_offset_ + buffer_.GetLength()) {
-    buffer_.Clear();
-    buffer_offset_ = last_request_offset_;
-  }
-
-  // If there is any overlapping, modify data/size accordingly.
-  if (buffer_offset_ + buffer_.GetLength() > last_request_offset_) {
-    uint64 difference =
-        buffer_offset_ + buffer_.GetLength() - last_request_offset_;
-    difference = std::min<uint64>(difference, size);
-    data += difference;
-    size -= difference;
-    last_request_offset_ += difference;
-  }
-
-  // If we are overflow, remove some data from the front of the buffer_.
-  if (buffer_.GetLength() + size > buffer_.GetMaxCapacity()) {
-    size_t bytes_skipped;
-    buffer_.Skip(buffer_.GetLength() + size - buffer_.GetMaxCapacity(),
-                 &bytes_skipped);
-    // "+ 0" converts buffer_.GetMaxCapacity() into a r-value to avoid link
-    // error.
-    DCHECK_EQ(buffer_.GetLength() + size, buffer_.GetMaxCapacity() + 0);
-    buffer_offset_ += bytes_skipped;
-  }
-
-  size_t bytes_written;
-  bool result = buffer_.Write(data, size, &bytes_written);
-  DCHECK(result);
-  DCHECK_EQ(size, bytes_written);
-
-  last_request_offset_ += bytes_written;
-  last_request_size_ -= bytes_written;
-
-  ProcessPendingRead_Locked();
-}
-
-void FetcherBufferedDataSource::OnURLFetchComplete(
-    const net::URLFetcher* source) {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-
-  base::AutoLock auto_lock(lock_);
-
-  if (fetcher_.get() != source || error_occured_) {
-    return;
-  }
-
-  const net::URLRequestStatus& status = source->GetStatus();
-  if (status.is_success()) {
-    if (!total_size_of_resource_ && last_request_size_ != 0) {
-      total_size_of_resource_ = buffer_offset_ + buffer_.GetLength();
-    }
-  } else {
-    LOG(ERROR)
-        << "FetcherBufferedDataSource::OnURLFetchComplete called with error "
-        << status.error();
-    error_occured_ = true;
-    buffer_.Clear();
-  }
-
-  fetcher_.reset();
-
-  ProcessPendingRead_Locked();
-  UpdateDownloadingStatus(/* is_downloading = */ false);
-}
-
-void FetcherBufferedDataSource::CreateNewFetcher() {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-
-  base::AutoLock auto_lock(lock_);
-
-  DCHECK(!fetcher_);
-  fetcher_to_be_destroyed_.reset();
-
-  DCHECK_GE(static_cast<int64>(last_request_offset_), 0);
-  DCHECK_GE(static_cast<int64>(last_request_size_), 0);
-
-  // Check if there was an error or if the request is blocked by csp.
-  if (error_occured_ ||
-      (!security_callback_.is_null() && !security_callback_.Run(url_, false))) {
-    error_occured_ = true;
-    if (!pending_read_cb_.is_null()) {
-      base::ResetAndReturn(&pending_read_cb_).Run(-1);
-    }
-    UpdateDownloadingStatus(/* is_downloading = */ false);
-    return;
-  }
-
-  fetcher_ =
-      std::move(net::URLFetcher::Create(url_, net::URLFetcher::GET, this));
-  fetcher_->SetRequestContext(
-      network_module_->url_request_context_getter().get());
-  std::unique_ptr<loader::URLFetcherStringWriter> download_data_writer(
-      new loader::URLFetcherStringWriter());
-  fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
-
-  std::string range_request =
-      "Range: bytes=" + base::NumberToString(last_request_offset_) + "-" +
-      base::NumberToString(last_request_offset_ + last_request_size_ - 1);
-  fetcher_->AddExtraRequestHeader(range_request);
-  if (!is_origin_safe_) {
-    if (request_mode_ != loader::kNoCORSMode &&
-        document_origin_ != loader::Origin(url_) && !url_.SchemeIs("data")) {
-      fetcher_->AddExtraRequestHeader("Origin:" +
-                                      document_origin_.SerializedOrigin());
-    } else {
-      is_origin_safe_ = true;
-    }
-  }
-  fetcher_->Start();
-  UpdateDownloadingStatus(/* is_downloading = */ true);
-}
-
-void FetcherBufferedDataSource::UpdateDownloadingStatus(bool is_downloading) {
-  DCHECK(task_runner_->BelongsToCurrentThread());
-
-  if (is_downloading_ == is_downloading) {
-    return;
-  }
-
-  is_downloading_ = is_downloading;
-  if (!downloading_status_cb_.is_null()) {
-    downloading_status_cb_.Run(is_downloading_);
-  }
-}
-
-void FetcherBufferedDataSource::Read_Locked(uint64 position, size_t size,
-                                            uint8* data,
-                                            const ReadCB& read_cb) {
-  lock_.AssertAcquired();
-
-  DCHECK(data);
-  DCHECK(!read_cb.is_null());
-  DCHECK(pending_read_cb_.is_null());  // One read operation at the same time.
-
-  if (error_occured_) {
-    read_cb.Run(-1);
-    return;
-  }
-
-  // Clamp the request to valid range of the resource if its size is known.
-  if (total_size_of_resource_) {
-    position = std::min(position, total_size_of_resource_.value());
-    if (size + position > total_size_of_resource_.value()) {
-      size = static_cast<size_t>(total_size_of_resource_.value() - position);
-    }
-  }
-
-  last_read_position_ = position;
-
-  if (size == 0) {
-    read_cb.Run(0);
-    return;
-  }
-
-  // Fulfill the read request now if we have the data.
-  if (position >= buffer_offset_ &&
-      position + size <= buffer_offset_ + buffer_.GetLength()) {
-    // All data is available
-    size_t bytes_peeked;
-    buffer_.Peek(data, size, static_cast<size_t>(position - buffer_offset_),
-                 &bytes_peeked);
-    DCHECK_EQ(bytes_peeked, size);
-    DCHECK_GE(static_cast<int>(bytes_peeked), 0);
-    read_cb.Run(static_cast<int>(bytes_peeked));
-    // If we have a large buffer size, it could be ideal if we can keep sending
-    // small requests when the read offset is far from the beginning of the
-    // buffer.  However as the ProgressiveDemuxer will cache many frames and the
-    // buffer we are using is usually small, we will just avoid sending requests
-    // here to make code simple.
-    return;
-  }
-
-  // Save the read request as we are unable to fulfill it now.
-  pending_read_cb_ = read_cb;
-  pending_read_position_ = position;
-  pending_read_size_ = size;
-  pending_read_data_ = data;
-
-  // Combine the range of the buffer and any ongoing fetch to see if the read is
-  // overlapped with it.
-  if (fetcher_) {
-    uint64 begin = last_request_offset_;
-    uint64 end = last_request_offset_ + last_request_size_;
-    if (last_request_offset_ >= buffer_offset_ &&
-        last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
-      begin = buffer_offset_;
-    }
-    if (position >= begin && position < end) {
-      // The read is overlapped with existing request, just wait.
-      return;
-    }
-  }
-
-  // Now we have to issue a new fetch and we no longer care about the range of
-  // the current fetch in progress if there is any.  Ideally the request range
-  // starts at |last_read_position_ - kBackwardBytes| with length of
-  // buffer_.GetMaxCapacity().
-  if (last_read_position_ > kBackwardBytes) {
-    last_request_offset_ = last_read_position_ - kBackwardBytes;
-  } else {
-    last_request_offset_ = 0;
-  }
-
-  size_t required_size = static_cast<size_t>(
-      last_read_position_ - last_request_offset_ + pending_read_size_);
-  if (required_size > buffer_.GetMaxCapacity()) {
-    // The capacity of the current buffer is not large enough to hold the
-    // pending read.
-    size_t new_capacity =
-        std::max<size_t>(buffer_.GetMaxCapacity() * 2, required_size);
-    buffer_.IncreaseMaxCapacityTo(new_capacity);
-  }
-
-  last_request_size_ = buffer_.GetMaxCapacity();
-
-  if (last_request_offset_ >= buffer_offset_ &&
-      last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
-    // Part of the Read() can be fulfilled by the current buffer and current
-    // request but cannot be fulfilled by the current request but we have to
-    // send another request to retrieve the rest.
-    last_request_size_ -=
-        buffer_offset_ + buffer_.GetLength() - last_request_offset_;
-    last_request_offset_ = buffer_offset_ + buffer_.GetLength();
-  }
-
-  if (cancelable_create_fetcher_closure_) {
-    cancelable_create_fetcher_closure_->Cancel();
-  }
-  base::Closure create_fetcher_closure = base::Bind(
-      &FetcherBufferedDataSource::CreateNewFetcher, base::Unretained(this));
-  cancelable_create_fetcher_closure_ =
-      new CancelableClosure(create_fetcher_closure);
-  fetcher_to_be_destroyed_.reset(fetcher_.release());
-  task_runner_->PostTask(FROM_HERE,
-                         cancelable_create_fetcher_closure_->AsClosure());
-}
-
-void FetcherBufferedDataSource::ProcessPendingRead_Locked() {
-  lock_.AssertAcquired();
-  if (!pending_read_cb_.is_null()) {
-    Read_Locked(pending_read_position_, pending_read_size_, pending_read_data_,
-                base::ResetAndReturn(&pending_read_cb_));
-  }
-}
-
-FetcherBufferedDataSource::CancelableClosure::CancelableClosure(
-    const base::Closure& closure)
-    : closure_(closure) {
-  DCHECK(!closure.is_null());
-}
-
-void FetcherBufferedDataSource::CancelableClosure::Cancel() {
-  base::AutoLock auto_lock(lock_);
-  closure_.Reset();
-}
-
-base::Closure FetcherBufferedDataSource::CancelableClosure::AsClosure() {
-  return base::Bind(&CancelableClosure::Call, this);
-}
-
-void FetcherBufferedDataSource::CancelableClosure::Call() {
-  base::AutoLock auto_lock(lock_);
-  // closure_.Run() has to be called when the lock is acquired to avoid race
-  // condition.
-  if (!closure_.is_null()) {
-    base::ResetAndReturn(&closure_).Run();
-  }
-}
-
-}  // namespace media
-}  // namespace cobalt
diff --git a/cobalt/media/fetcher_buffered_data_source.h b/cobalt/media/fetcher_buffered_data_source.h
deleted file mode 100644
index fb589a1..0000000
--- a/cobalt/media/fetcher_buffered_data_source.h
+++ /dev/null
@@ -1,163 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
-#define COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
-
-#include <memory>
-#include <string>
-
-#include "base/callback.h"
-#include "base/compiler_specific.h"
-#include "base/memory/ref_counted.h"
-#include "base/message_loop/message_loop.h"
-#include "base/optional.h"
-#include "base/synchronization/lock.h"
-#include "cobalt/base/circular_buffer_shell.h"
-#include "cobalt/csp/content_security_policy.h"
-#include "cobalt/loader/fetcher.h"
-#include "cobalt/loader/origin.h"
-#include "cobalt/loader/url_fetcher_string_writer.h"
-#include "cobalt/media/player/buffered_data_source.h"
-#include "cobalt/network/network_module.h"
-#include "net/url_request/url_fetcher.h"
-#include "net/url_request/url_fetcher_delegate.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace media {
-
-// TODO: This class requires a large block of memory.
-
-// A BufferedDataSource based on net::URLFetcher that can be used to retrieve
-// progressive videos from both local and network sources.
-// It uses a fixed size circular buffer so we may not be able to store all data
-// into this buffer.  It is based on the following assumptions/strategies:
-// 1. It assumes that the buffer is large enough to fulfill one Read() request.
-//    So any outstanding request only requires at most one request.
-// 2. It will do one initial request to retrieve the target resource.  If the
-//    whole resource can be fit into the buffer, no further request will be
-//    fired.
-// 3. If the resource doesn't fit into the buffer.  The class will store
-//    kBackwardBytes bytes before the last read offset(LRO) and kForwardBytes
-//    after LRO.  Note that if LRO is less than kBackwardBytes, then data starts
-//    from offset 0 will be cached.
-// 4. It assumes that the server supports range request.
-// 5. All data stored are continuous.
-class FetcherBufferedDataSource : public BufferedDataSource,
-                                  private net::URLFetcherDelegate {
- public:
-  static const int64 kInvalidSize = -1;
-
-  // Because the Fetchers have to be created and destroyed on the same thread,
-  // we use the task_runner passed in to create and destroy Fetchers.
-  FetcherBufferedDataSource(
-      const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
-      const GURL& url, const csp::SecurityCallback& security_callback,
-      network::NetworkModule* network_module, loader::RequestMode request_mode,
-      loader::Origin origin);
-  ~FetcherBufferedDataSource() override;
-
-  // DataSource methods.
-  void Read(int64 position, int size, uint8* data,
-            const ReadCB& read_cb) override;
-  void Stop() override;
-  bool GetSize(int64* size_out) override;
-  bool IsStreaming() override { return false; }
-  void SetBitrate(int bitrate) override {}
-
-  // BufferedDataSource methods.
-  void SetDownloadingStatusCB(
-      const DownloadingStatusCB& downloading_status_cb) override;
-
- private:
-  class CancelableClosure
-      : public base::RefCountedThreadSafe<CancelableClosure> {
-   public:
-    explicit CancelableClosure(const base::Closure& closure);
-
-    void Cancel();
-    base::Closure AsClosure();
-
-   private:
-    void Call();
-
-    base::Lock lock_;
-    base::Closure closure_;
-  };
-
-  // net::URLFetcherDelegate methods
-  void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
-  void OnURLFetchDownloadProgress(const net::URLFetcher* source,
-                                  int64_t current, int64_t total,
-                                  int64_t current_network_bytes) override;
-  void OnURLFetchComplete(const net::URLFetcher* source) override;
-
-  void CreateNewFetcher();
-  void UpdateDownloadingStatus(bool is_downloading);
-  void Read_Locked(uint64 position, size_t size, uint8* data,
-                   const ReadCB& read_cb);
-  void ProcessPendingRead_Locked();
-  void TryToSendRequest_Locked();
-
-  base::Lock lock_;
-  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
-  GURL url_;
-  network::NetworkModule* network_module_;
-  std::unique_ptr<net::URLFetcher> fetcher_;
-
-  bool is_downloading_;
-  DownloadingStatusCB downloading_status_cb_;
-
-  // |fetcher_| has to be destroyed on the thread it's created.  So it cannot be
-  // safely destroyed inside Read_Locked().  Save |fetcher_| into
-  // |fetcher_to_be_destroyed_| to ensure that it is properly destroyed either
-  // inside CreateNewFetcher() or in the dtor while still allow |fetcher_| to be
-  // set to NULL to invalidate outstanding read.
-  std::unique_ptr<net::URLFetcher> fetcher_to_be_destroyed_;
-
-  // |buffer_| stores a continuous block of data of target resource starts from
-  // |buffer_offset_|.  When the target resource can be fit into |buffer_|,
-  // |buffer_offset_| will always be 0.
-  base::CircularBufferShell buffer_;
-  uint64 buffer_offset_;
-
-  base::Optional<uint64> total_size_of_resource_;
-  bool error_occured_;
-
-  uint64 last_request_offset_;
-  uint64 last_request_size_;
-
-  // This is usually the same as pending_read_position_.  Represent it
-  // explicitly using a separate variable.
-  uint64 last_read_position_;
-
-  ReadCB pending_read_cb_;
-  uint64 pending_read_position_;
-  size_t pending_read_size_;
-  uint8* pending_read_data_;
-
-  csp::SecurityCallback security_callback_;
-  scoped_refptr<CancelableClosure> cancelable_create_fetcher_closure_;
-
-  loader::RequestMode request_mode_;
-  loader::Origin document_origin_;
-  // True if the origin is allowed to fetch resource data.
-  bool is_origin_safe_;
-};
-
-}  // namespace media
-}  // namespace cobalt
-
-#endif  // COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
diff --git a/cobalt/media/file_data_source.cc b/cobalt/media/file_data_source.cc
new file mode 100644
index 0000000..81eb1ae
--- /dev/null
+++ b/cobalt/media/file_data_source.cc
@@ -0,0 +1,89 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/file_data_source.h"
+
+#include <string>
+#include <utility>
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/path_service.h"
+#include "cobalt/base/cobalt_paths.h"
+
+namespace cobalt {
+namespace media {
+namespace {
+
+using base::File;
+using base::FilePath;
+using base::PathService;
+
+File OpenFile(const FilePath& file_path) {
+  int path_keys[] = {paths::DIR_COBALT_WEB_ROOT, base::DIR_TEST_DATA};
+
+  for (auto path_key : path_keys) {
+    FilePath root_path;
+    bool result = PathService::Get(path_key, &root_path);
+    SB_DCHECK(result);
+
+    File file(root_path.Append(file_path), File::FLAG_OPEN | File::FLAG_READ);
+    if (file.IsValid()) {
+      return std::move(file);
+    }
+  }
+
+  return File();
+}
+
+}  // namespace
+
+FileDataSource::FileDataSource(const GURL& file_url) {
+  DCHECK(file_url.is_valid());
+  DCHECK(file_url.SchemeIsFile());
+
+  std::string path = file_url.path();
+  if (path.empty() || path[0] != '/') {
+    return;
+  }
+
+  file_ = std::move(OpenFile(base::FilePath(path.substr(1))));
+}
+
+void FileDataSource::Read(int64 position, int size, uint8* data,
+                          const ReadCB& read_cb) {
+  DCHECK_GE(position, 0);
+  DCHECK_GE(size, 0);
+
+  if (!file_.IsValid()) {
+    read_cb.Run(kReadError);
+    return;
+  }
+
+  auto bytes_read = file_.Read(position, reinterpret_cast<char*>(data), size);
+  if (bytes_read == size) {
+    read_cb.Run(bytes_read);
+  } else {
+    read_cb.Run(kReadError);
+  }
+}
+
+bool FileDataSource::GetSize(int64* size_out) {
+  *size_out = file_.IsValid() ? file_.GetLength() : kInvalidSize;
+
+  return *size_out >= 0;
+}
+
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media/file_data_source.h b/cobalt/media/file_data_source.h
new file mode 100644
index 0000000..dab3bcd
--- /dev/null
+++ b/cobalt/media/file_data_source.h
@@ -0,0 +1,53 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_FILE_DATA_SOURCE_H_
+#define COBALT_MEDIA_FILE_DATA_SOURCE_H_
+
+#include "base/files/file.h"
+#include "cobalt/media/base/data_source.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace media {
+
+// A file based DataSource used to retrieve progressive videos from local files.
+// The class is for testing purposes only, and shouldn't be used in production
+// environment.
+// Its member functions can be called from multiple threads.  However, this
+// class doesn't synchronize the calls.  It's the responsibility of its user to
+// ensure that such calls are synchronized.
+class FileDataSource : public DataSource {
+ public:
+  static constexpr int64 kInvalidSize = -1;
+
+  explicit FileDataSource(const GURL& file_url);
+
+  // DataSource methods.
+  void Read(int64 position, int size, uint8* data,
+            const ReadCB& read_cb) override;
+  void Stop() override {}
+  void Abort() override {}
+  bool GetSize(int64* size_out) override;
+  void SetDownloadingStatusCB(
+      const DownloadingStatusCB& downloading_status_cb) override {}
+
+ private:
+  base::File file_;
+};
+
+}  // namespace media
+}  // namespace cobalt
+
+#endif  // COBALT_MEDIA_FILE_DATA_SOURCE_H_
diff --git a/cobalt/media/file_data_source_test.cc b/cobalt/media/file_data_source_test.cc
new file mode 100644
index 0000000..4fe7fe7
--- /dev/null
+++ b/cobalt/media/file_data_source_test.cc
@@ -0,0 +1,82 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/file_data_source.h"
+
+#include <string.h>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/test/scoped_task_environment.h"
+#include "starboard/common/log.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace media {
+
+void OnReadFinished(int* bytes_read_out, int bytes_read_in) {
+  SB_CHECK(bytes_read_out);
+  *bytes_read_out = bytes_read_in;
+}
+
+TEST(FileDataSourceTest, SunnyDay) {
+  base::test::ScopedTaskEnvironment scoped_task_environment_;
+  FileDataSource data_source(
+      GURL("file:///cobalt/media/testing/data/"
+           "progressive_aac_44100_stereo_h264_1280_720.mp4"));
+
+  int64 file_size = -1;
+  ASSERT_TRUE(data_source.GetSize(&file_size));
+  ASSERT_GT(file_size, 0);
+
+  uint8 buffer[1024];
+  int bytes_read = 0;
+
+  // Read from the beginning
+  data_source.Read(0, 1024, buffer, base::Bind(OnReadFinished, &bytes_read));
+  scoped_task_environment_.RunUntilIdle();
+  EXPECT_EQ(bytes_read, 1024);
+  EXPECT_EQ(memcmp(buffer + 4, "ftyp", 4), 0);
+
+  // Read from the end
+  data_source.Read(file_size - 1024, 1024, buffer,
+                   base::Bind(OnReadFinished, &bytes_read));
+  scoped_task_environment_.RunUntilIdle();
+  EXPECT_EQ(bytes_read, 1024);
+
+  // Read beyond the end
+  data_source.Read(file_size - 512, 1024, buffer,
+                   base::Bind(OnReadFinished, &bytes_read));
+  scoped_task_environment_.RunUntilIdle();
+  EXPECT_EQ(bytes_read, FileDataSource::kReadError);
+}
+
+TEST(FileDataSourceTest, RainyDay) {
+  base::test::ScopedTaskEnvironment scoped_task_environment_;
+  FileDataSource data_source(
+      GURL("file:///cobalt/media/testing/data/do_not_exist.invalid"));
+
+  int64 size = -1;
+  ASSERT_FALSE(data_source.GetSize(&size));
+
+  uint8 buffer[1024];
+  int bytes_read = 0;
+  data_source.Read(0, 1024, buffer, base::Bind(OnReadFinished, &bytes_read));
+
+  scoped_task_environment_.RunUntilIdle();
+  EXPECT_EQ(bytes_read, FileDataSource::kReadError);
+}
+
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media/media_module.cc b/cobalt/media/media_module.cc
index 11bcbd7..6159298 100644
--- a/cobalt/media/media_module.cc
+++ b/cobalt/media/media_module.cc
@@ -183,6 +183,16 @@
 
 }  // namespace
 
+bool MediaModule::SetConfiguration(const std::string& name, int32 value) {
+  if (name == "EnableBatchedSampleWrite") {
+    allow_batched_sample_write_ = value;
+    LOG(INFO) << (allow_batched_sample_write_ ? "Enabling" : "Disabling")
+              << " batched sample write.";
+    return true;
+  }
+  return false;
+}
+
 std::unique_ptr<WebMediaPlayer> MediaModule::CreateWebMediaPlayer(
     WebMediaPlayerClient* client) {
   TRACK_MEMORY_SCOPE("Media");
@@ -192,10 +202,11 @@
   }
 
   return std::unique_ptr<WebMediaPlayer>(new media::WebMediaPlayerImpl(
-      window,
+      sbplayer_interface_.get(), window,
       base::Bind(&MediaModule::GetSbDecodeTargetGraphicsContextProvider,
                  base::Unretained(this)),
-      client, this, options_.allow_resume_after_suspend, &media_log_));
+      client, this, options_.allow_resume_after_suspend,
+      allow_batched_sample_write_, &media_log_));
 }
 
 void MediaModule::Suspend() {
diff --git a/cobalt/media/media_module.h b/cobalt/media/media_module.h
index e79db38..6ca47b8 100644
--- a/cobalt/media/media_module.h
+++ b/cobalt/media/media_module.h
@@ -25,6 +25,7 @@
 #include "base/memory/ref_counted.h"
 #include "base/optional.h"
 #include "cobalt/math/size.h"
+#include "cobalt/media/base/sbplayer_interface.h"
 #include "cobalt/media/can_play_type_handler.h"
 #include "cobalt/media/decoder_buffer_allocator.h"
 #include "cobalt/media/player/web_media_player_delegate.h"
@@ -57,10 +58,16 @@
   MediaModule(system_window::SystemWindow* system_window,
               render_tree::ResourceProvider* resource_provider,
               const Options& options = Options())
-      : options_(options),
+      : sbplayer_interface_(new DefaultSbPlayerInterface),
+        options_(options),
         system_window_(system_window),
         resource_provider_(resource_provider) {}
 
+  // Returns true when the setting is set successfully or if the setting has
+  // already been set to the expected value.  Returns false when the setting is
+  // invalid or not set to the expected value.
+  bool SetConfiguration(const std::string& name, int32 value);
+
   const DecoderBufferAllocator* GetDecoderBufferAllocator() const {
     return &decoder_buffer_allocator_;
   }
@@ -101,6 +108,7 @@
   // paused by us.
   typedef std::map<WebMediaPlayer*, bool> Players;
 
+  std::unique_ptr<SbPlayerInterface> sbplayer_interface_;
   const Options options_;
   system_window::SystemWindow* system_window_;
   cobalt::render_tree::ResourceProvider* resource_provider_;
@@ -113,6 +121,8 @@
   Players players_;
   bool suspended_ = false;
 
+  bool allow_batched_sample_write_ = false;
+
   DecoderBufferAllocator decoder_buffer_allocator_;
 };
 
diff --git a/cobalt/media/player/buffered_data_source.h b/cobalt/media/player/buffered_data_source.h
deleted file mode 100644
index 03267e2..0000000
--- a/cobalt/media/player/buffered_data_source.h
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
-#define COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
-
-#include "base/basictypes.h"
-#include "base/callback.h"
-#include "base/message_loop/message_loop.h"
-#include "cobalt/media/base/data_source.h"
-#include "starboard/types.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace media {
-
-enum Preload {
-  kPreloadNone,
-  kPreloadMetaData,
-  kPreloadAuto,
-};
-
-// TODO: Investigate if we still need BufferedDataSource.
-class BufferedDataSource : public DataSource {
- public:
-  typedef base::Callback<void(bool)> DownloadingStatusCB;
-
-  virtual void SetDownloadingStatusCB(
-      const DownloadingStatusCB& downloading_status_cb) {
-  }
-  virtual void SetPreload(Preload preload) {
-  }
-  virtual bool HasSingleOrigin() { return true; }
-  virtual bool DidPassCORSAccessCheck() const { return true; }
-  virtual void Abort() {}
-};
-
-}  // namespace media
-}  // namespace cobalt
-
-#endif  // COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
diff --git a/cobalt/media/player/web_media_player.h b/cobalt/media/player/web_media_player.h
index 68be9b6..1322473 100644
--- a/cobalt/media/player/web_media_player.h
+++ b/cobalt/media/player/web_media_player.h
@@ -19,8 +19,9 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
+#include "cobalt/media/base/data_source.h"
 #include "cobalt/media/base/decode_target_provider.h"
-#include "cobalt/media/player/buffered_data_source.h"
+#include "starboard/window.h"
 #include "url/gurl.h"
 
 namespace media {
@@ -100,8 +101,8 @@
   virtual void LoadUrl(const GURL& url) = 0;
 #endif  // SB_HAS(PLAYER_WITH_URL)
   virtual void LoadMediaSource() = 0;
-  virtual void LoadProgressive(
-      const GURL& url, std::unique_ptr<BufferedDataSource> data_source) = 0;
+  virtual void LoadProgressive(const GURL& url,
+                               std::unique_ptr<DataSource> data_source) = 0;
 
   virtual void CancelLoad() = 0;
 
@@ -148,9 +149,6 @@
 
   virtual bool DidLoadingProgress() const = 0;
 
-  virtual bool HasSingleSecurityOrigin() const = 0;
-  virtual bool DidPassCORSAccessCheck() const = 0;
-
   virtual float MediaTimeForTimeValue(float timeValue) const = 0;
 
   virtual PlayerStatistics GetStatistics() const = 0;
diff --git a/cobalt/media/player/web_media_player_impl.cc b/cobalt/media/player/web_media_player_impl.cc
index 4fa422e..1844a6a 100644
--- a/cobalt/media/player/web_media_player_impl.cc
+++ b/cobalt/media/player/web_media_player_impl.cc
@@ -112,11 +112,12 @@
     OnNeedKeyCB;
 
 WebMediaPlayerImpl::WebMediaPlayerImpl(
-    PipelineWindow window,
+    SbPlayerInterface* interface, PipelineWindow window,
     const Pipeline::GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
     WebMediaPlayerClient* client, WebMediaPlayerDelegate* delegate,
-    bool allow_resume_after_suspend, ::media::MediaLog* const media_log)
+    bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    ::media::MediaLog* const media_log)
     : pipeline_thread_("media_pipeline"),
       network_state_(WebMediaPlayer::kNetworkStateEmpty),
       ready_state_(WebMediaPlayer::kReadyStateHaveNothing),
@@ -124,6 +125,7 @@
       client_(client),
       delegate_(delegate),
       allow_resume_after_suspend_(allow_resume_after_suspend),
+      allow_batched_sample_write_(allow_batched_sample_write),
       proxy_(new WebMediaPlayerProxy(main_loop_->task_runner(), this)),
       media_log_(media_log),
       is_local_source_(false),
@@ -139,10 +141,11 @@
   media_log_->AddEvent<::media::MediaLogEvent::kWebMediaPlayerCreated>();
 
   pipeline_thread_.Start();
-  pipeline_ = Pipeline::Create(window, pipeline_thread_.task_runner(),
-                               get_decode_target_graphics_context_provider_func,
-                               allow_resume_after_suspend_, media_log_,
-                               decode_target_provider_.get());
+  pipeline_ =
+      Pipeline::Create(interface, window, pipeline_thread_.task_runner(),
+                       get_decode_target_graphics_context_provider_func,
+                       allow_resume_after_suspend_, allow_batched_sample_write_,
+                       media_log_, decode_target_provider_.get());
 
   // Also we want to be notified of |main_loop_| destruction.
   main_loop_->AddDestructionObserver(this);
@@ -259,7 +262,7 @@
 }
 
 void WebMediaPlayerImpl::LoadProgressive(
-    const GURL& url, std::unique_ptr<BufferedDataSource> data_source) {
+    const GURL& url, std::unique_ptr<DataSource> data_source) {
   TRACE_EVENT0("cobalt::media", "WebMediaPlayerImpl::LoadProgressive");
   DCHECK_EQ(main_loop_, base::MessageLoop::current());
 
@@ -513,9 +516,6 @@
 float WebMediaPlayerImpl::GetMaxTimeSeekable() const {
   DCHECK_EQ(main_loop_, base::MessageLoop::current());
 
-  // We don't support seeking in streaming media.
-  if (proxy_ && proxy_->data_source() && proxy_->data_source()->IsStreaming())
-    return 0.0f;
   return static_cast<float>(pipeline_->GetMediaDuration().InSecondsF());
 }
 
@@ -534,15 +534,6 @@
   return pipeline_->DidLoadingProgress();
 }
 
-bool WebMediaPlayerImpl::HasSingleSecurityOrigin() const {
-  if (proxy_) return proxy_->HasSingleOrigin();
-  return true;
-}
-
-bool WebMediaPlayerImpl::DidPassCORSAccessCheck() const {
-  return proxy_ && proxy_->DidPassCORSAccessCheck();
-}
-
 float WebMediaPlayerImpl::MediaTimeForTimeValue(float timeValue) const {
   return ConvertSecondsToTimestamp(timeValue).InSecondsF();
 }
diff --git a/cobalt/media/player/web_media_player_impl.h b/cobalt/media/player/web_media_player_impl.h
index 67d5405..b64f318 100644
--- a/cobalt/media/player/web_media_player_impl.h
+++ b/cobalt/media/player/web_media_player_impl.h
@@ -61,6 +61,7 @@
 #include "cobalt/math/size.h"
 #include "cobalt/media/base/decode_target_provider.h"
 #include "cobalt/media/base/pipeline.h"
+#include "cobalt/media/base/sbplayer_interface.h"
 #include "cobalt/media/player/web_media_player.h"
 #include "cobalt/media/player/web_media_player_delegate.h"
 #include "third_party/chromium/media/base/demuxer.h"
@@ -102,12 +103,13 @@
   // When calling this, the |audio_source_provider| and
   // |audio_renderer_sink| arguments should be the same object.
 
-  WebMediaPlayerImpl(PipelineWindow window,
+  WebMediaPlayerImpl(SbPlayerInterface* interface, PipelineWindow window,
                      const Pipeline::GetDecodeTargetGraphicsContextProviderFunc&
                          get_decode_target_graphics_context_provider_func,
                      WebMediaPlayerClient* client,
                      WebMediaPlayerDelegate* delegate,
                      bool allow_resume_after_suspend,
+                     bool allow_batched_sample_write,
                      ::media::MediaLog* const media_log);
   ~WebMediaPlayerImpl() override;
 
@@ -115,9 +117,8 @@
   void LoadUrl(const GURL& url) override;
 #endif  // SB_HAS(PLAYER_WITH_URL)
   void LoadMediaSource() override;
-  void LoadProgressive(
-      const GURL& url,
-      std::unique_ptr<BufferedDataSource> data_source) override;
+  void LoadProgressive(const GURL& url,
+                       std::unique_ptr<DataSource> data_source) override;
 
   void CancelLoad() override;
 
@@ -166,9 +167,6 @@
 
   bool DidLoadingProgress() const override;
 
-  bool HasSingleSecurityOrigin() const override;
-  bool DidPassCORSAccessCheck() const override;
-
   float MediaTimeForTimeValue(float timeValue) const override;
 
   PlayerStatistics GetStatistics() const override;
@@ -292,6 +290,7 @@
   WebMediaPlayerClient* const client_;
   WebMediaPlayerDelegate* const delegate_;
   const bool allow_resume_after_suspend_;
+  const bool allow_batched_sample_write_;
   scoped_refptr<DecodeTargetProvider> decode_target_provider_;
 
   scoped_refptr<WebMediaPlayerProxy> proxy_;
diff --git a/cobalt/media/player/web_media_player_proxy.cc b/cobalt/media/player/web_media_player_proxy.cc
index 350d71a..57a6742 100644
--- a/cobalt/media/player/web_media_player_proxy.cc
+++ b/cobalt/media/player/web_media_player_proxy.cc
@@ -21,16 +21,10 @@
 
 WebMediaPlayerProxy::~WebMediaPlayerProxy() { Detach(); }
 
-bool WebMediaPlayerProxy::HasSingleOrigin() {
+void WebMediaPlayerProxy::Detach() {
   DCHECK(render_loop_->BelongsToCurrentThread());
-  if (data_source_) return data_source_->HasSingleOrigin();
-  return true;
-}
-
-bool WebMediaPlayerProxy::DidPassCORSAccessCheck() const {
-  DCHECK(render_loop_->BelongsToCurrentThread());
-  if (data_source_) return data_source_->DidPassCORSAccessCheck();
-  return false;
+  webmediaplayer_ = NULL;
+  data_source_.reset();
 }
 
 void WebMediaPlayerProxy::AbortDataSource() {
@@ -38,11 +32,5 @@
   if (data_source_) data_source_->Abort();
 }
 
-void WebMediaPlayerProxy::Detach() {
-  DCHECK(render_loop_->BelongsToCurrentThread());
-  webmediaplayer_ = NULL;
-  data_source_.reset();
-}
-
 }  // namespace media
 }  // namespace cobalt
diff --git a/cobalt/media/player/web_media_player_proxy.h b/cobalt/media/player/web_media_player_proxy.h
index bfcffdd..7436f0d 100644
--- a/cobalt/media/player/web_media_player_proxy.h
+++ b/cobalt/media/player/web_media_player_proxy.h
@@ -6,10 +6,11 @@
 #define COBALT_MEDIA_PLAYER_WEB_MEDIA_PLAYER_PROXY_H_
 
 #include <memory>
+#include <utility>
 
 #include "base/memory/ref_counted.h"
 #include "base/message_loop/message_loop.h"
-#include "cobalt/media/player/buffered_data_source.h"
+#include "cobalt/media/base/data_source.h"
 
 namespace cobalt {
 namespace media {
@@ -24,15 +25,12 @@
   WebMediaPlayerProxy(
       const scoped_refptr<base::SingleThreadTaskRunner>& render_loop,
       WebMediaPlayerImpl* webmediaplayer);
-  BufferedDataSource* data_source() { return data_source_.get(); }
-  void set_data_source(std::unique_ptr<BufferedDataSource> data_source) {
+  DataSource* data_source() { return data_source_.get(); }
+  void set_data_source(std::unique_ptr<DataSource> data_source) {
     data_source_ = std::move(data_source);
   }
 
   void Detach();
-  bool HasSingleOrigin();
-  bool DidPassCORSAccessCheck() const;
-
   void AbortDataSource();
 
  private:
@@ -43,7 +41,7 @@
   scoped_refptr<base::SingleThreadTaskRunner> render_loop_;
   WebMediaPlayerImpl* webmediaplayer_;
 
-  std::unique_ptr<BufferedDataSource> data_source_;
+  std::unique_ptr<DataSource> data_source_;
 
   DISALLOW_IMPLICIT_CONSTRUCTORS(WebMediaPlayerProxy);
 };
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.cc b/cobalt/media/progressive/demuxer_extension_wrapper.cc
index f4c8139..03b8b8a 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper.cc
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.cc
@@ -21,7 +21,7 @@
 
 #include "base/task/post_task.h"
 #include "base/task_runner_util.h"
-#include "cobalt/extension/demuxer.h"
+#include "starboard/extension/demuxer.h"
 #include "starboard/system.h"
 #include "third_party/chromium/media/base/audio_codecs.h"
 #include "third_party/chromium/media/base/bind_to_current_loop.h"
@@ -189,14 +189,15 @@
       << "Audio config is not valid!";
 }
 
-void DemuxerExtensionStream::Read(ReadCB read_cb) {
+void DemuxerExtensionStream::Read(int max_number_of_buffers_to_read,
+                                  ReadCB read_cb) {
   DCHECK(!read_cb.is_null());
   base::AutoLock auto_lock(lock_);
   if (stopped_) {
     LOG(INFO) << "Already stopped.";
-    std::move(read_cb).Run(
-        DemuxerStream::kOk,
-        scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+    std::vector<scoped_refptr<DecoderBuffer>> buffers;
+    buffers.push_back(std::move(DecoderBuffer::CreateEOSBuffer()));
+    std::move(read_cb).Run(DemuxerStream::kOk, buffers);
     return;
   }
 
@@ -216,7 +217,9 @@
     buffer_queue_.pop_front();
   }
 
-  std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+  std::vector<scoped_refptr<DecoderBuffer>> buffers;
+  buffers.push_back(std::move(buffer));
+  std::move(read_cb).Run(DemuxerStream::kOk, buffers);
 }
 
 AudioDecoderConfig DemuxerExtensionStream::audio_decoder_config() {
@@ -277,7 +280,9 @@
   CHECK_EQ(buffer_queue_.size(), 0);
   ReadCB read_cb(std::move(read_queue_.front()));
   read_queue_.pop_front();
-  std::move(read_cb).Run(DemuxerStream::kOk, std::move(buffer));
+  std::vector<scoped_refptr<DecoderBuffer>> buffers;
+  buffers.push_back(std::move(buffer));
+  std::move(read_cb).Run(DemuxerStream::kOk, buffers);
 }
 
 void DemuxerExtensionStream::FlushBuffers() {
@@ -296,9 +301,9 @@
   last_buffer_timestamp_ = ::media::kNoTimestamp;
   // Fulfill any pending callbacks with EOS buffers set to end timestamp.
   for (auto& read_cb : read_queue_) {
-    std::move(read_cb).Run(
-        DemuxerStream::kOk,
-        scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+    std::vector<scoped_refptr<DecoderBuffer>> buffers;
+    buffers.push_back(std::move(DecoderBuffer::CreateEOSBuffer()));
+    std::move(read_cb).Run(DemuxerStream::kOk, buffers);
   }
   read_queue_.clear();
   stopped_ = true;
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.h b/cobalt/media/progressive/demuxer_extension_wrapper.h
index 138b922..c3f869a 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper.h
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.h
@@ -27,8 +27,8 @@
 #include "base/optional.h"
 #include "base/sequence_checker.h"
 #include "base/threading/thread.h"
-#include "cobalt/extension/demuxer.h"
 #include "cobalt/media/progressive/data_source_reader.h"
+#include "starboard/extension/demuxer.h"
 #include "third_party/chromium/media/base/audio_decoder_config.h"
 #include "third_party/chromium/media/base/decoder_buffer.h"
 #include "third_party/chromium/media/base/demuxer.h"
@@ -69,7 +69,7 @@
   size_t GetTotalBufferSize() const;
 
   // DemuxerStream implementation:
-  void Read(ReadCB read_cb) override;
+  void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
   ::media::AudioDecoderConfig audio_decoder_config() override;
   ::media::VideoDecoderConfig video_decoder_config() override;
   Type type() const override;
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper_test.cc b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
index 3aae16a..d199aa4 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
+++ b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
@@ -25,8 +25,8 @@
 #include "base/threading/platform_thread.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "base/time/time.h"
-#include "cobalt/extension/demuxer.h"
 #include "cobalt/media/decoder_buffer_allocator.h"
+#include "starboard/extension/demuxer.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/chromium/media/base/demuxer.h"
@@ -37,6 +37,7 @@
 
 using ::testing::_;
 using ::testing::AtMost;
+using ::testing::ElementsAre;
 using ::testing::ElementsAreArray;
 using ::testing::ExplainMatchResult;
 using ::testing::Invoke;
@@ -97,8 +98,8 @@
   MOCK_METHOD0(Stop, void());
   MOCK_METHOD0(Abort, void());
   MOCK_METHOD1(GetSize, bool(int64_t* size_out));
-  MOCK_METHOD0(IsStreaming, bool());
-  MOCK_METHOD1(SetBitrate, void(int bitrate));
+  MOCK_METHOD1(SetDownloadingStatusCB,
+               void(const DownloadingStatusCB& downloading_status_cb));
 };
 
 // Mock class for receiving calls to the Cobalt Extension demuxer. Based on the
@@ -548,14 +549,17 @@
                                                                 : streams[1];
 
   base::MockCallback<base::OnceCallback<void(
-      ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+      ::media::DemuxerStream::Status,
+      const std::vector<scoped_refptr<::media::DecoderBuffer>>&)>>
       read_cb;
   base::WaitableEvent read_done;
+  std::vector<std::vector<uint8_t>> buffers;
+  buffers.push_back(buffer_data);
   EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
-                           Pointee(BufferHasData(buffer_data))))
+                           ElementsAre(Pointee(BufferHasData(buffer_data)))))
       .WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
 
-  audio_stream->Read(read_cb.Get());
+  audio_stream->Read(1, read_cb.Get());
   EXPECT_TRUE(WaitForEvent(read_done));
 }
 
@@ -685,14 +689,17 @@
                                                                 : streams[1];
 
   base::MockCallback<base::OnceCallback<void(
-      ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+      ::media::DemuxerStream::Status,
+      const std::vector<scoped_refptr<::media::DecoderBuffer>>&)>>
       read_cb;
   base::WaitableEvent read_done;
+  std::vector<std::vector<uint8_t>> buffers;
+  buffers.push_back(buffer_data);
   EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
-                           Pointee(BufferHasData(buffer_data))))
+                           ElementsAre(Pointee(BufferHasData(buffer_data)))))
       .WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
 
-  video_stream->Read(read_cb.Get());
+  video_stream->Read(1, read_cb.Get());
   EXPECT_TRUE(WaitForEvent(read_done));
 }
 
diff --git a/cobalt/media/progressive/progressive_demuxer.cc b/cobalt/media/progressive/progressive_demuxer.cc
index 9470096..3a30c8d 100644
--- a/cobalt/media/progressive/progressive_demuxer.cc
+++ b/cobalt/media/progressive/progressive_demuxer.cc
@@ -50,7 +50,8 @@
   DCHECK(demuxer_);
 }
 
-void ProgressiveDemuxerStream::Read(ReadCB read_cb) {
+void ProgressiveDemuxerStream::Read(int max_number_of_buffers_to_read,
+                                    ReadCB read_cb) {
   TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Read()");
   DCHECK(!read_cb.is_null());
 
@@ -61,7 +62,7 @@
   if (stopped_) {
     TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Read() EOS sent.");
     std::move(read_cb).Run(DemuxerStream::kOk,
-                           DecoderBuffer::CreateEOSBuffer());
+                           {DecoderBuffer::CreateEOSBuffer()});
     return;
   }
 
@@ -79,7 +80,7 @@
       --total_buffer_count_;
       buffer_queue_.pop_front();
     }
-    std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+    std::move(read_cb).Run(DemuxerStream::kOk, {buffer});
   } else {
     TRACE_EVENT0("media_stack",
                  "ProgressiveDemuxerStream::Read() request queued.");
@@ -139,7 +140,7 @@
     DCHECK_EQ(buffer_queue_.size(), 0);
     ReadCB read_cb = std::move(read_queue_.front());
     read_queue_.pop_front();
-    std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+    std::move(read_cb).Run(DemuxerStream::kOk, {buffer});
   } else {
     // save the buffer for next read request
     buffer_queue_.push_back(buffer);
@@ -188,9 +189,7 @@
   for (ReadQueue::iterator it = read_queue_.begin(); it != read_queue_.end();
        ++it) {
     TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Stop() EOS sent.");
-    std::move(*it).Run(
-        DemuxerStream::kOk,
-        scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+    std::move(*it).Run(DemuxerStream::kOk, {DecoderBuffer::CreateEOSBuffer()});
   }
   read_queue_.clear();
   stopped_ = true;
@@ -289,11 +288,6 @@
   // IsConfigComplete() should guarantee we know the duration
   DCHECK(parser_->Duration() != ::media::kInfiniteDuration);
   host_->SetDuration(parser_->Duration());
-  // Bitrate may not be known, however
-  uint32 bitrate = parser_->BitsPerSecond();
-  if (bitrate > 0) {
-    data_source_->SetBitrate(bitrate);
-  }
 
   // successful parse of config data, inform the nonblocking demuxer thread
   DCHECK_EQ(status, ::media::PIPELINE_OK);
diff --git a/cobalt/media/progressive/progressive_demuxer.h b/cobalt/media/progressive/progressive_demuxer.h
index 73f3026..c79cf01 100644
--- a/cobalt/media/progressive/progressive_demuxer.h
+++ b/cobalt/media/progressive/progressive_demuxer.h
@@ -47,8 +47,12 @@
 
   ProgressiveDemuxerStream(ProgressiveDemuxer* demuxer, Type type);
 
-  // DemuxerStream implementation
+// DemuxerStream implementation
+#if defined(STARBOARD)
+  void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
+#else   // defined(STARBOARD)
   void Read(ReadCB read_cb) override;
+#endif  // defined(STARBOARD)
   AudioDecoderConfig audio_decoder_config() override;
   VideoDecoderConfig video_decoder_config() override;
   Type type() const override;
diff --git a/cobalt/media/progressive/progressive_parser.cc b/cobalt/media/progressive/progressive_parser.cc
index fe810b1..771e386 100644
--- a/cobalt/media/progressive/progressive_parser.cc
+++ b/cobalt/media/progressive/progressive_parser.cc
@@ -48,9 +48,7 @@
 }
 
 ProgressiveParser::ProgressiveParser(scoped_refptr<DataSourceReader> reader)
-    : reader_(reader),
-      duration_(::media::kInfiniteDuration),
-      bits_per_second_(0) {}
+    : reader_(reader), duration_(::media::kInfiniteDuration) {}
 
 ProgressiveParser::~ProgressiveParser() {}
 
diff --git a/cobalt/media/progressive/progressive_parser.h b/cobalt/media/progressive/progressive_parser.h
index f3d3e82..cd3fce7 100644
--- a/cobalt/media/progressive/progressive_parser.h
+++ b/cobalt/media/progressive/progressive_parser.h
@@ -69,8 +69,6 @@
   virtual bool IsConfigComplete();
   // time-duration of file, may return kInfiniteDuration() if unknown
   virtual base::TimeDelta Duration() { return duration_; }
-  // bits per second of media, if known, otherwise 0
-  virtual uint32 BitsPerSecond() { return bits_per_second_; }
   virtual const AudioDecoderConfig& AudioConfig() { return audio_config_; }
   virtual const VideoDecoderConfig& VideoConfig() { return video_config_; }
 
@@ -82,7 +80,6 @@
   AudioDecoderConfig audio_config_;
   VideoDecoderConfig video_config_;
   base::TimeDelta duration_;
-  uint32 bits_per_second_;
 };
 
 }  // namespace media
diff --git a/cobalt/media/sandbox/format_guesstimator.cc b/cobalt/media/sandbox/format_guesstimator.cc
index abac9f6..1225b5a 100644
--- a/cobalt/media/sandbox/format_guesstimator.cc
+++ b/cobalt/media/sandbox/format_guesstimator.cc
@@ -144,6 +144,15 @@
     return;
   }
   InitializeAsAdaptive(path, media_module);
+
+  if (!is_valid() && IsFormat(path_or_url, ".mp4")) {
+    // It's an mp4 file but not in DASH, let's try progressive again.
+    bool is_from_root = !path_or_url.empty() && path_or_url[0] == '/';
+    auto path_from_root = (is_from_root ? "" : "/") + path_or_url;
+    progressive_url_ = GURL("file://" + path_from_root);
+    SB_LOG(INFO) << progressive_url_.spec();
+    mime_type_ = "video/mp4; codecs=\"avc1.640028, mp4a.40.2\"";
+  }
 }
 
 void FormatGuesstimator::InitializeAsProgressive(const GURL& url) {
@@ -202,7 +211,6 @@
       // true format.
       continue;
     }
-
     // Succeeding |AppendData()| may be a false positive (i.e. the expected
     // configuration does not match with the configuration determined by the
     // ChunkDemuxer). To confirm, we check the decoder configuration determined
diff --git a/cobalt/media/sandbox/media2_sandbox.cc b/cobalt/media/sandbox/media2_sandbox.cc
index cb89b23..a81ed91 100644
--- a/cobalt/media/sandbox/media2_sandbox.cc
+++ b/cobalt/media/sandbox/media2_sandbox.cc
@@ -75,12 +75,15 @@
 
 void ReadDemuxerStream(DemuxerStream* demuxer_stream);
 
-void OnDemuxerStreamRead(DemuxerStream* demuxer_stream,
-                         DemuxerStream::Status status,
-                         scoped_refptr<::media::DecoderBuffer> decoder_buffer) {
-  if (!decoder_buffer->end_of_stream()) {
+void OnDemuxerStreamRead(
+    DemuxerStream* demuxer_stream, DemuxerStream::Status status,
+    const std::vector<scoped_refptr<::media::DecoderBuffer>>& decoder_buffers) {
+  if (status != DemuxerStream::kConfigChanged) {
+    DCHECK(decoder_buffers.size() > 0);
+  }
+  if (!decoder_buffers[0]->end_of_stream()) {
     LOG(INFO) << "Reading " << GetDemuxerStreamType(demuxer_stream)
-              << " buffer at " << decoder_buffer->timestamp();
+              << " buffer at " << decoder_buffers[0]->timestamp();
     ReadDemuxerStream(demuxer_stream);
   } else {
     LOG(INFO) << "Received " << GetDemuxerStreamType(demuxer_stream) << " EOS";
@@ -90,7 +93,7 @@
 void ReadDemuxerStream(DemuxerStream* demuxer_stream) {
   DCHECK(demuxer_stream);
   demuxer_stream->Read(
-      base::BindOnce(OnDemuxerStreamRead, base::Unretained(demuxer_stream)));
+      1, base::BindOnce(OnDemuxerStreamRead, base::Unretained(demuxer_stream)));
 }
 
 }  // namespace
diff --git a/cobalt/media/sandbox/web_media_player_helper.cc b/cobalt/media/sandbox/web_media_player_helper.cc
index 3b8088e..ab5c080 100644
--- a/cobalt/media/sandbox/web_media_player_helper.cc
+++ b/cobalt/media/sandbox/web_media_player_helper.cc
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "cobalt/media/sandbox/web_media_player_helper.h"
+
 #include <memory>
 #include <utility>
 
-#include "cobalt/media/sandbox/web_media_player_helper.h"
-
-#include "cobalt/media/fetcher_buffered_data_source.h"
+#include "cobalt/media/file_data_source.h"
+#include "cobalt/media/url_fetcher_data_source.h"
 #include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
 
 namespace cobalt {
@@ -80,11 +81,19 @@
     : client_(new WebMediaPlayerClientStub),
       player_(media_module->CreateWebMediaPlayer(client_)) {
   player_->SetRate(1.0);
-  std::unique_ptr<BufferedDataSource> data_source(new FetcherBufferedDataSource(
-      base::MessageLoop::current()->task_runner(), video_url,
-      csp::SecurityCallback(), fetcher_factory->network_module(),
-      loader::kNoCORSMode, loader::Origin()));
-  player_->LoadProgressive(video_url, std::move(data_source));
+
+  if (video_url.SchemeIsFile()) {
+    std::unique_ptr<DataSource> data_source(
+        new media::FileDataSource(video_url));
+    player_->LoadProgressive(video_url, std::move(data_source));
+  } else {
+    std::unique_ptr<DataSource> data_source(new URLFetcherDataSource(
+        base::MessageLoop::current()->task_runner(), video_url,
+        csp::SecurityCallback(), fetcher_factory->network_module(),
+        loader::kNoCORSMode, loader::Origin()));
+    player_->LoadProgressive(video_url, std::move(data_source));
+  }
+
   player_->Play();
 
   auto set_bounds_cb = player_->GetSetBoundsCB();
diff --git a/cobalt/media/testing/BUILD.gn b/cobalt/media/testing/BUILD.gn
new file mode 100644
index 0000000..0e0529c
--- /dev/null
+++ b/cobalt/media/testing/BUILD.gn
@@ -0,0 +1,55 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//cobalt/media/testing/data/sha1_files.gni")
+
+action("cobalt_media_download_test_data") {
+  install_content = true
+
+  script = "//tools/download_from_gcs.py"
+
+  sha_sources = []
+  foreach(sha1_file, sha1_files) {
+    sha_sources += [ string_join("/",
+                                 [
+                                   "data",
+                                   sha1_file,
+                                 ]) ]
+  }
+
+  sha_outputs = []
+  subdir = "cobalt/media/testing"
+  outdir = "$sb_static_contents_output_data_dir/test/$subdir"
+  foreach(sha_source, sha_sources) {
+    sha_outputs += [ string_join("/",
+                                 [
+                                   outdir,
+                                   string_replace(sha_source, ".sha1", ""),
+                                 ]) ]
+  }
+
+  sources = sha_sources
+  outputs = sha_outputs
+
+  sha1_dir = rebase_path("data", root_build_dir)
+
+  args = [
+    "--bucket",
+    "cobalt-static-storage",
+    "--sha1",
+    sha1_dir,
+    "--output",
+    rebase_path("$outdir/data", root_build_dir),
+  ]
+}
diff --git a/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1 b/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1
new file mode 100644
index 0000000..1b30031
--- /dev/null
+++ b/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1
@@ -0,0 +1 @@
+92fb29d98bded5091e93db4db9c610441d643ce8
diff --git a/cobalt/media/testing/data/sha1_files.gni b/cobalt/media/testing/data/sha1_files.gni
new file mode 100644
index 0000000..d42094a
--- /dev/null
+++ b/cobalt/media/testing/data/sha1_files.gni
@@ -0,0 +1,15 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+sha1_files = [ "progressive_aac_44100_stereo_h264_1280_720.mp4.sha1" ]
diff --git a/cobalt/media/url_fetcher_data_source.cc b/cobalt/media/url_fetcher_data_source.cc
new file mode 100644
index 0000000..4f25520
--- /dev/null
+++ b/cobalt/media/url_fetcher_data_source.cc
@@ -0,0 +1,521 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/url_fetcher_data_source.h"
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/strings/string_number_conversions.h"
+#include "cobalt/base/polymorphic_downcast.h"
+#include "cobalt/loader/cors_preflight.h"
+#include "cobalt/loader/url_fetcher_string_writer.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_status_code.h"
+
+namespace cobalt {
+namespace media {
+
+namespace {
+
+const uint32 kBackwardBytes = 256 * 1024;
+const uint32 kInitialForwardBytes = 3 * 256 * 1024;
+const uint32 kInitialBufferCapacity = kBackwardBytes + kInitialForwardBytes;
+
+}  // namespace
+
+using base::CircularBufferShell;
+
+URLFetcherDataSource::URLFetcherDataSource(
+    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+    const GURL& url, const csp::SecurityCallback& security_callback,
+    network::NetworkModule* network_module, loader::RequestMode request_mode,
+    loader::Origin origin)
+    : task_runner_(task_runner),
+      url_(url),
+      network_module_(network_module),
+      is_downloading_(false),
+      buffer_(kInitialBufferCapacity, CircularBufferShell::kReserve),
+      buffer_offset_(0),
+      error_occured_(false),
+      last_request_offset_(0),
+      last_request_size_(0),
+      last_read_position_(0),
+      pending_read_position_(0),
+      pending_read_size_(0),
+      pending_read_data_(NULL),
+      security_callback_(security_callback),
+      request_mode_(request_mode),
+      document_origin_(origin),
+      is_origin_safe_(false) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  DCHECK(task_runner_);
+  DCHECK(network_module);
+}
+
+URLFetcherDataSource::~URLFetcherDataSource() {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  if (cancelable_create_fetcher_closure_) {
+    cancelable_create_fetcher_closure_->Cancel();
+  }
+}
+
+void URLFetcherDataSource::Read(int64 position, int size, uint8* data,
+                                const ReadCB& read_cb) {
+  DCHECK_GE(position, 0);
+  DCHECK_GE(size, 0);
+
+  if (position < 0 || size < 0) {
+    read_cb.Run(kInvalidSize);
+    return;
+  }
+
+  base::AutoLock auto_lock(lock_);
+  Read_Locked(static_cast<uint64>(position), static_cast<size_t>(size), data,
+              read_cb);
+}
+
+void URLFetcherDataSource::Stop() {
+  {
+    base::AutoLock auto_lock(lock_);
+
+    if (!pending_read_cb_.is_null()) {
+      base::ResetAndReturn(&pending_read_cb_).Run(0);
+    }
+    // From this moment on, any call to Read() should be treated as an error.
+    // Note that we cannot reset |fetcher_| here because of:
+    // 1. URLFetcher has to be destroyed on the thread that it is created,
+    //    however Stop() is usually called from the pipeline thread where
+    //    |fetcher_| is created on the web thread.
+    // 2. We cannot post a task to the web thread to destroy |fetcher_| as the
+    //    web thread is blocked by WebMediaPlayerImpl::Destroy().
+    // Once |error_occured_| is set to true, the URLFetcher callbacks return
+    // immediately and it is safe to destroy |fetcher_| inside the dtor.
+    error_occured_ = true;
+  }
+}
+
+bool URLFetcherDataSource::GetSize(int64* size_out) {
+  base::AutoLock auto_lock(lock_);
+
+  if (total_size_of_resource_) {
+    *size_out = static_cast<int64>(total_size_of_resource_.value());
+    DCHECK_GE(*size_out, 0);
+  } else {
+    *size_out = kInvalidSize;
+  }
+  return *size_out != kInvalidSize;
+}
+
+void URLFetcherDataSource::SetDownloadingStatusCB(
+    const DownloadingStatusCB& downloading_status_cb) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  DCHECK(!downloading_status_cb.is_null());
+  DCHECK(downloading_status_cb_.is_null());
+  downloading_status_cb_ = downloading_status_cb;
+}
+
+void URLFetcherDataSource::OnURLFetchResponseStarted(
+    const net::URLFetcher* source) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  base::AutoLock auto_lock(lock_);
+
+  if (fetcher_.get() != source || error_occured_) {
+    return;
+  }
+
+  if (!source->GetStatus().is_success()) {
+    // The error will be handled on OnURLFetchComplete()
+    error_occured_ = true;
+    return;
+  } else if (source->GetResponseCode() == -1) {
+    // Could be a file URL, so we won't expect headers.
+    return;
+  }
+
+  // In the event of a redirect, re-check the security policy.
+  if (source->GetURL() != source->GetOriginalURL()) {
+    if (!security_callback_.is_null() &&
+        !security_callback_.Run(source->GetURL(), true /*did redirect*/)) {
+      error_occured_ = true;
+      if (!pending_read_cb_.is_null()) {
+        base::ResetAndReturn(&pending_read_cb_).Run(-1);
+      }
+      return;
+    }
+  }
+
+  scoped_refptr<net::HttpResponseHeaders> headers =
+      source->GetResponseHeaders();
+  DCHECK(headers);
+
+  if (!is_origin_safe_) {
+    if (loader::CORSPreflight::CORSCheck(
+            *headers, document_origin_.SerializedOrigin(),
+            request_mode_ == loader::kCORSModeIncludeCredentials)) {
+      is_origin_safe_ = true;
+    } else {
+      error_occured_ = true;
+      if (!pending_read_cb_.is_null()) {
+        base::ResetAndReturn(&pending_read_cb_).Run(-1);
+      }
+      return;
+    }
+  }
+
+  uint64 first_byte_offset = 0;
+
+  if (headers->response_code() == net::HTTP_PARTIAL_CONTENT) {
+    int64 first_byte_position = -1;
+    int64 last_byte_position = -1;
+    int64 instance_length = -1;
+    bool is_range_valid = headers && headers->GetContentRangeFor206(
+                                         &first_byte_position,
+                                         &last_byte_position, &instance_length);
+    if (is_range_valid) {
+      if (first_byte_position >= 0) {
+        first_byte_offset = static_cast<uint64>(first_byte_position);
+      }
+      if (!total_size_of_resource_ && instance_length > 0) {
+        total_size_of_resource_ = static_cast<uint64>(instance_length);
+      }
+    }
+  }
+
+  DCHECK_LE(first_byte_offset, last_request_offset_);
+
+  if (first_byte_offset < last_request_offset_) {
+    last_request_size_ += last_request_offset_ - first_byte_offset;
+    last_request_offset_ = first_byte_offset;
+  }
+}
+
+void URLFetcherDataSource::OnURLFetchDownloadProgress(
+    const net::URLFetcher* source, int64_t /*current*/, int64_t /*total*/,
+    int64_t /*current_network_bytes*/) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+  auto* download_data_writer =
+      base::polymorphic_downcast<loader::URLFetcherStringWriter*>(
+          source->GetResponseWriter());
+  std::string downloaded_data;
+  download_data_writer->GetAndResetData(&downloaded_data);
+  size_t size = downloaded_data.size();
+  if (size == 0) {
+    return;
+  }
+  const uint8* data = reinterpret_cast<const uint8*>(downloaded_data.data());
+  base::AutoLock auto_lock(lock_);
+
+  if (fetcher_.get() != source || error_occured_) {
+    return;
+  }
+
+  size = static_cast<size_t>(std::min<uint64>(size, last_request_size_));
+
+  if (size == 0 || size > buffer_.GetMaxCapacity()) {
+    // The server side doesn't support range request.  Delete |fetcher_| to
+    // stop the current request.
+    LOG(ERROR)
+        << "URLFetcherDataSource::OnURLFetchDownloadProgress: server "
+        << "doesn't support range requests (e.g. Python SimpleHTTPServer). "
+        << "Please use a server that supports range requests (e.g. Flask).";
+    error_occured_ = true;
+    fetcher_.reset();
+    ProcessPendingRead_Locked();
+    UpdateDownloadingStatus(/* is_downloading = */ false);
+    return;
+  }
+
+  // Because we can only append data into the buffer_.  We just check if the
+  // position of the first byte of the newly received data is overlapped with
+  // the range of the buffer_.  If not, we can discard all data in the buffer_
+  // as there is no way to represent a gap or to prepend data.
+  if (last_request_offset_ < buffer_offset_ ||
+      last_request_offset_ > buffer_offset_ + buffer_.GetLength()) {
+    buffer_.Clear();
+    buffer_offset_ = last_request_offset_;
+  }
+
+  // If there is any overlapping, modify data/size accordingly.
+  if (buffer_offset_ + buffer_.GetLength() > last_request_offset_) {
+    uint64 difference =
+        buffer_offset_ + buffer_.GetLength() - last_request_offset_;
+    difference = std::min<uint64>(difference, size);
+    data += difference;
+    size -= difference;
+    last_request_offset_ += difference;
+  }
+
+  // If we are overflow, remove some data from the front of the buffer_.
+  if (buffer_.GetLength() + size > buffer_.GetMaxCapacity()) {
+    size_t bytes_skipped;
+    buffer_.Skip(buffer_.GetLength() + size - buffer_.GetMaxCapacity(),
+                 &bytes_skipped);
+    // "+ 0" converts buffer_.GetMaxCapacity() into a r-value to avoid link
+    // error.
+    DCHECK_EQ(buffer_.GetLength() + size, buffer_.GetMaxCapacity() + 0);
+    buffer_offset_ += bytes_skipped;
+  }
+
+  size_t bytes_written;
+  bool result = buffer_.Write(data, size, &bytes_written);
+  DCHECK(result);
+  DCHECK_EQ(size, bytes_written);
+
+  last_request_offset_ += bytes_written;
+  last_request_size_ -= bytes_written;
+
+  ProcessPendingRead_Locked();
+}
+
+void URLFetcherDataSource::OnURLFetchComplete(const net::URLFetcher* source) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  base::AutoLock auto_lock(lock_);
+
+  if (fetcher_.get() != source || error_occured_) {
+    return;
+  }
+
+  const net::URLRequestStatus& status = source->GetStatus();
+  if (status.is_success()) {
+    if (!total_size_of_resource_ && last_request_size_ != 0) {
+      total_size_of_resource_ = buffer_offset_ + buffer_.GetLength();
+    }
+  } else {
+    LOG(ERROR) << "URLFetcherDataSource::OnURLFetchComplete called with error "
+               << status.error();
+    error_occured_ = true;
+    buffer_.Clear();
+  }
+
+  fetcher_.reset();
+
+  ProcessPendingRead_Locked();
+  UpdateDownloadingStatus(/* is_downloading = */ false);
+}
+
+void URLFetcherDataSource::CreateNewFetcher() {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  base::AutoLock auto_lock(lock_);
+
+  DCHECK(!fetcher_);
+  fetcher_to_be_destroyed_.reset();
+
+  DCHECK_GE(static_cast<int64>(last_request_offset_), 0);
+  DCHECK_GE(static_cast<int64>(last_request_size_), 0);
+
+  // Check if there was an error or if the request is blocked by csp.
+  if (error_occured_ ||
+      (!security_callback_.is_null() && !security_callback_.Run(url_, false))) {
+    error_occured_ = true;
+    if (!pending_read_cb_.is_null()) {
+      base::ResetAndReturn(&pending_read_cb_).Run(-1);
+    }
+    UpdateDownloadingStatus(/* is_downloading = */ false);
+    return;
+  }
+
+  fetcher_ =
+      std::move(net::URLFetcher::Create(url_, net::URLFetcher::GET, this));
+  fetcher_->SetRequestContext(
+      network_module_->url_request_context_getter().get());
+  std::unique_ptr<loader::URLFetcherStringWriter> download_data_writer(
+      new loader::URLFetcherStringWriter());
+  fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
+
+  std::string range_request =
+      "Range: bytes=" + base::NumberToString(last_request_offset_) + "-" +
+      base::NumberToString(last_request_offset_ + last_request_size_ - 1);
+  fetcher_->AddExtraRequestHeader(range_request);
+  if (!is_origin_safe_) {
+    if (request_mode_ != loader::kNoCORSMode &&
+        document_origin_ != loader::Origin(url_) && !url_.SchemeIs("data")) {
+      fetcher_->AddExtraRequestHeader("Origin:" +
+                                      document_origin_.SerializedOrigin());
+    } else {
+      is_origin_safe_ = true;
+    }
+  }
+  fetcher_->Start();
+  UpdateDownloadingStatus(/* is_downloading = */ true);
+}
+
+void URLFetcherDataSource::UpdateDownloadingStatus(bool is_downloading) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  if (is_downloading_ == is_downloading) {
+    return;
+  }
+
+  is_downloading_ = is_downloading;
+  if (!downloading_status_cb_.is_null()) {
+    downloading_status_cb_.Run(is_downloading_);
+  }
+}
+
+void URLFetcherDataSource::Read_Locked(uint64 position, size_t size,
+                                       uint8* data, const ReadCB& read_cb) {
+  lock_.AssertAcquired();
+
+  DCHECK(data);
+  DCHECK(!read_cb.is_null());
+  DCHECK(pending_read_cb_.is_null());  // One read operation at the same time.
+
+  if (error_occured_) {
+    read_cb.Run(-1);
+    return;
+  }
+
+  // Clamp the request to valid range of the resource if its size is known.
+  if (total_size_of_resource_) {
+    position = std::min(position, total_size_of_resource_.value());
+    if (size + position > total_size_of_resource_.value()) {
+      size = static_cast<size_t>(total_size_of_resource_.value() - position);
+    }
+  }
+
+  last_read_position_ = position;
+
+  if (size == 0) {
+    read_cb.Run(0);
+    return;
+  }
+
+  // Fulfill the read request now if we have the data.
+  if (position >= buffer_offset_ &&
+      position + size <= buffer_offset_ + buffer_.GetLength()) {
+    // All data is available
+    size_t bytes_peeked;
+    buffer_.Peek(data, size, static_cast<size_t>(position - buffer_offset_),
+                 &bytes_peeked);
+    DCHECK_EQ(bytes_peeked, size);
+    DCHECK_GE(static_cast<int>(bytes_peeked), 0);
+    read_cb.Run(static_cast<int>(bytes_peeked));
+    // If we have a large buffer size, it could be ideal if we can keep sending
+    // small requests when the read offset is far from the beginning of the
+    // buffer.  However as the ProgressiveDemuxer will cache many frames and the
+    // buffer we are using is usually small, we will just avoid sending requests
+    // here to make code simple.
+    return;
+  }
+
+  // Save the read request as we are unable to fulfill it now.
+  pending_read_cb_ = read_cb;
+  pending_read_position_ = position;
+  pending_read_size_ = size;
+  pending_read_data_ = data;
+
+  // Combine the range of the buffer and any ongoing fetch to see if the read is
+  // overlapped with it.
+  if (fetcher_) {
+    uint64 begin = last_request_offset_;
+    uint64 end = last_request_offset_ + last_request_size_;
+    if (last_request_offset_ >= buffer_offset_ &&
+        last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
+      begin = buffer_offset_;
+    }
+    if (position >= begin && position < end) {
+      // The read is overlapped with existing request, just wait.
+      return;
+    }
+  }
+
+  // Now we have to issue a new fetch and we no longer care about the range of
+  // the current fetch in progress if there is any.  Ideally the request range
+  // starts at |last_read_position_ - kBackwardBytes| with length of
+  // buffer_.GetMaxCapacity().
+  if (last_read_position_ > kBackwardBytes) {
+    last_request_offset_ = last_read_position_ - kBackwardBytes;
+  } else {
+    last_request_offset_ = 0;
+  }
+
+  size_t required_size = static_cast<size_t>(
+      last_read_position_ - last_request_offset_ + pending_read_size_);
+  if (required_size > buffer_.GetMaxCapacity()) {
+    // The capacity of the current buffer is not large enough to hold the
+    // pending read.
+    size_t new_capacity =
+        std::max<size_t>(buffer_.GetMaxCapacity() * 2, required_size);
+    buffer_.IncreaseMaxCapacityTo(new_capacity);
+  }
+
+  last_request_size_ = buffer_.GetMaxCapacity();
+
+  if (last_request_offset_ >= buffer_offset_ &&
+      last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
+    // Part of the Read() can be fulfilled by the current buffer and current
+    // request but cannot be fulfilled by the current request but we have to
+    // send another request to retrieve the rest.
+    last_request_size_ -=
+        buffer_offset_ + buffer_.GetLength() - last_request_offset_;
+    last_request_offset_ = buffer_offset_ + buffer_.GetLength();
+  }
+
+  if (cancelable_create_fetcher_closure_) {
+    cancelable_create_fetcher_closure_->Cancel();
+  }
+  base::Closure create_fetcher_closure = base::Bind(
+      &URLFetcherDataSource::CreateNewFetcher, base::Unretained(this));
+  cancelable_create_fetcher_closure_ =
+      new CancelableClosure(create_fetcher_closure);
+  fetcher_to_be_destroyed_.reset(fetcher_.release());
+  task_runner_->PostTask(FROM_HERE,
+                         cancelable_create_fetcher_closure_->AsClosure());
+}
+
+void URLFetcherDataSource::ProcessPendingRead_Locked() {
+  lock_.AssertAcquired();
+  if (!pending_read_cb_.is_null()) {
+    Read_Locked(pending_read_position_, pending_read_size_, pending_read_data_,
+                base::ResetAndReturn(&pending_read_cb_));
+  }
+}
+
+URLFetcherDataSource::CancelableClosure::CancelableClosure(
+    const base::Closure& closure)
+    : closure_(closure) {
+  DCHECK(!closure.is_null());
+}
+
+void URLFetcherDataSource::CancelableClosure::Cancel() {
+  base::AutoLock auto_lock(lock_);
+  closure_.Reset();
+}
+
+base::Closure URLFetcherDataSource::CancelableClosure::AsClosure() {
+  return base::Bind(&CancelableClosure::Call, this);
+}
+
+void URLFetcherDataSource::CancelableClosure::Call() {
+  base::AutoLock auto_lock(lock_);
+  // closure_.Run() has to be called when the lock is acquired to avoid race
+  // condition.
+  if (!closure_.is_null()) {
+    base::ResetAndReturn(&closure_).Run();
+  }
+}
+
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media/url_fetcher_data_source.h b/cobalt/media/url_fetcher_data_source.h
new file mode 100644
index 0000000..8cea653
--- /dev/null
+++ b/cobalt/media/url_fetcher_data_source.h
@@ -0,0 +1,162 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
+#define COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/message_loop/message_loop.h"
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+#include "cobalt/base/circular_buffer_shell.h"
+#include "cobalt/csp/content_security_policy.h"
+#include "cobalt/loader/fetcher.h"
+#include "cobalt/loader/origin.h"
+#include "cobalt/loader/url_fetcher_string_writer.h"
+#include "cobalt/media/base/data_source.h"
+#include "cobalt/network/network_module.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace media {
+
+// TODO: This class requires a large block of memory.
+
+// A DataSource based on net::URLFetcher that can be used to retrieve
+// progressive videos from both local and network sources.
+// It uses a fixed size circular buffer so we may not be able to store all data
+// into this buffer.  It is based on the following assumptions/strategies:
+// 1. It assumes that the buffer is large enough to fulfill one Read() request.
+//    So any outstanding request only requires at most one request.
+// 2. It will do one initial request to retrieve the target resource.  If the
+//    whole resource can be fit into the buffer, no further request will be
+//    fired.
+// 3. If the resource doesn't fit into the buffer.  The class will store
+//    kBackwardBytes bytes before the last read offset(LRO) and kForwardBytes
+//    after LRO.  Note that if LRO is less than kBackwardBytes, then data starts
+//    from offset 0 will be cached.
+// 4. It assumes that the server supports range request.
+// 5. All data stored are continuous.
+class URLFetcherDataSource : public DataSource,
+                             private net::URLFetcherDelegate {
+ public:
+  static const int64 kInvalidSize = -1;
+
+  // Because the URLFetchers have to be created and destroyed on the same
+  // thread, we use the |task_runner| passed in to ensure that.  Note that the
+  // ctor and dtor are also called from the |task_runner|, which is checked in
+  // the ctor and dtor.
+  URLFetcherDataSource(
+      const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+      const GURL& url, const csp::SecurityCallback& security_callback,
+      network::NetworkModule* network_module, loader::RequestMode request_mode,
+      loader::Origin origin);
+  ~URLFetcherDataSource() override;
+
+  // DataSource methods.
+  void Read(int64 position, int size, uint8* data,
+            const ReadCB& read_cb) override;
+  void Stop() override;
+  void Abort() override {}
+  bool GetSize(int64* size_out) override;
+  void SetDownloadingStatusCB(
+      const DownloadingStatusCB& downloading_status_cb) override;
+
+ private:
+  class CancelableClosure
+      : public base::RefCountedThreadSafe<CancelableClosure> {
+   public:
+    explicit CancelableClosure(const base::Closure& closure);
+
+    void Cancel();
+    base::Closure AsClosure();
+
+   private:
+    void Call();
+
+    base::Lock lock_;
+    base::Closure closure_;
+  };
+
+  // net::URLFetcherDelegate methods
+  void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
+  void OnURLFetchDownloadProgress(const net::URLFetcher* source,
+                                  int64_t current, int64_t total,
+                                  int64_t current_network_bytes) override;
+  void OnURLFetchComplete(const net::URLFetcher* source) override;
+
+  void CreateNewFetcher();
+  void UpdateDownloadingStatus(bool is_downloading);
+  void Read_Locked(uint64 position, size_t size, uint8* data,
+                   const ReadCB& read_cb);
+  void ProcessPendingRead_Locked();
+  void TryToSendRequest_Locked();
+
+  base::Lock lock_;
+  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
+  GURL url_;
+  network::NetworkModule* network_module_;
+  std::unique_ptr<net::URLFetcher> fetcher_;
+
+  bool is_downloading_;
+  DownloadingStatusCB downloading_status_cb_;
+
+  // |fetcher_| has to be destroyed on the thread it's created.  So it cannot be
+  // safely destroyed inside Read_Locked().  Save |fetcher_| into
+  // |fetcher_to_be_destroyed_| to ensure that it is properly destroyed either
+  // inside CreateNewFetcher() or in the dtor while still allow |fetcher_| to be
+  // set to NULL to invalidate outstanding read.
+  std::unique_ptr<net::URLFetcher> fetcher_to_be_destroyed_;
+
+  // |buffer_| stores a continuous block of data of target resource starts from
+  // |buffer_offset_|.  When the target resource can be fit into |buffer_|,
+  // |buffer_offset_| will always be 0.
+  base::CircularBufferShell buffer_;
+  uint64 buffer_offset_;
+
+  base::Optional<uint64> total_size_of_resource_;
+  bool error_occured_;
+
+  uint64 last_request_offset_;
+  uint64 last_request_size_;
+
+  // This is usually the same as pending_read_position_.  Represent it
+  // explicitly using a separate variable.
+  uint64 last_read_position_;
+
+  ReadCB pending_read_cb_;
+  uint64 pending_read_position_;
+  size_t pending_read_size_;
+  uint8* pending_read_data_;
+
+  csp::SecurityCallback security_callback_;
+  scoped_refptr<CancelableClosure> cancelable_create_fetcher_closure_;
+
+  loader::RequestMode request_mode_;
+  loader::Origin document_origin_;
+  // True if the origin is allowed to fetch resource data.
+  bool is_origin_safe_;
+};
+
+}  // namespace media
+}  // namespace cobalt
+
+#endif  // COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
diff --git a/cobalt/media_session/media_session.cc b/cobalt/media_session/media_session.cc
index 2b8dd33..d0d412c 100644
--- a/cobalt/media_session/media_session.cc
+++ b/cobalt/media_session/media_session.cc
@@ -26,6 +26,10 @@
       last_position_updated_time_(0) {}
 
 MediaSession::~MediaSession() {
+  // Stop the media session client first. This avoids possible access to media
+  // session during destruction.
+  media_session_client_.reset();
+
   ActionMap::iterator it;
   for (it = action_map_.begin(); it != action_map_.end(); ++it) {
     delete it->second;
@@ -98,9 +102,7 @@
   }
   is_change_task_queued_ = true;
   task_runner_->PostDelayedTask(
-      FROM_HERE,
-      base::Bind(&MediaSession::OnChanged, this),
-      delay);
+      FROM_HERE, base::Bind(&MediaSession::OnChanged, this), delay);
 }
 
 void MediaSession::OnChanged() {
diff --git a/cobalt/media_session/media_session.h b/cobalt/media_session/media_session.h
index c83b1df..4814c9e 100644
--- a/cobalt/media_session/media_session.h
+++ b/cobalt/media_session/media_session.h
@@ -16,6 +16,7 @@
 #define COBALT_MEDIA_SESSION_MEDIA_SESSION_H_
 
 #include <map>
+#include <memory>
 
 #include "base/containers/small_map.h"
 #include "base/location.h"
diff --git a/cobalt/media_session/media_session_client.cc b/cobalt/media_session/media_session_client.cc
index c8290cf..ab740de 100644
--- a/cobalt/media_session/media_session_client.cc
+++ b/cobalt/media_session/media_session_client.cc
@@ -91,6 +91,9 @@
     } else if (extension_->RegisterMediaSessionCallbacks != nullptr) {
       extension_->RegisterMediaSessionCallbacks(
           this, &InvokeActionCallback, &UpdatePlatformPlaybackStateCallback);
+      DCHECK(extension_->DestroyMediaSessionClientCallback)
+          << "Possible heap use-after-free if platform does not handle media "
+          << "session DestroyMediaSessionClientCallback()";
     }
   }
 }
@@ -186,8 +189,8 @@
 void MediaSessionClient::PostDelayedTaskForMaybeFreezeCallback() {
   media_session_->task_runner_->PostDelayedTask(
       FROM_HERE,
-      base::Bind(&MediaSessionClient::RunMaybeFreezeCallback,
-                 base::Unretained(this), ++sequence_number_),
+      base::Bind(&MediaSessionClient::RunMaybeFreezeCallback, AsWeakPtr(),
+                 ++sequence_number_),
       kMaybeFreezeDelay);
 }
 
@@ -197,7 +200,7 @@
   if (!media_session_->task_runner_->BelongsToCurrentThread()) {
     media_session_->task_runner_->PostTask(
         FROM_HERE, base::Bind(&MediaSessionClient::UpdatePlatformPlaybackState,
-                              base::Unretained(this), state));
+                              AsWeakPtr(), state));
     return;
   }
 
@@ -235,7 +238,7 @@
   if (!media_session_->task_runner_->BelongsToCurrentThread()) {
     media_session_->task_runner_->PostTask(
         FROM_HERE, base::Bind(&MediaSessionClient::InvokeActionInternal,
-                              base::Unretained(this), base::Passed(&details)));
+                              AsWeakPtr(), base::Passed(&details)));
     return;
   }
 
diff --git a/cobalt/media_session/media_session_client.h b/cobalt/media_session/media_session_client.h
index 35a9a31..54817e4 100644
--- a/cobalt/media_session/media_session_client.h
+++ b/cobalt/media_session/media_session_client.h
@@ -17,13 +17,15 @@
 
 #include <bitset>
 #include <memory>
+#include <utility>
 
+#include "base/memory/weak_ptr.h"
 #include "base/threading/thread_checker.h"
-#include "cobalt/extension/media_session.h"
 #include "cobalt/media/web_media_player_factory.h"
 #include "cobalt/media_session/media_session.h"
 #include "cobalt/media_session/media_session_action_details.h"
 #include "cobalt/media_session/media_session_state.h"
+#include "starboard/extension/media_session.h"
 #include "starboard/time.h"
 
 namespace cobalt {
@@ -31,11 +33,11 @@
 
 // Base class for a platform-level implementation of MediaSession.
 // Platforms should subclass this to connect MediaSession to their platform.
-class MediaSessionClient {
+class MediaSessionClient : public base::SupportsWeakPtr<MediaSessionClient> {
   friend class MediaSession;
 
  public:
-  MediaSessionClient(): MediaSessionClient(nullptr) {}
+  MediaSessionClient() : MediaSessionClient(nullptr) {}
   // Injectable MediaSession for tests.
   explicit MediaSessionClient(MediaSession* media_session);
 
@@ -83,7 +85,7 @@
   // media session playback state.
   bool is_active() {
     return session_state_.actual_playback_state() !=
-        kMediaSessionPlaybackStateNone;
+           kMediaSessionPlaybackStateNone;
   }
 
   // Set maybe freeze callback.
diff --git a/cobalt/media_session/media_session_test.cc b/cobalt/media_session/media_session_test.cc
index 1a56e80..43f34ed 100644
--- a/cobalt/media_session/media_session_test.cc
+++ b/cobalt/media_session/media_session_test.cc
@@ -20,12 +20,12 @@
 #include "base/message_loop/message_loop.h"
 #include "base/run_loop.h"
 #include "cobalt/bindings/testing/script_object_owner.h"
-#include "cobalt/extension/media_session.h"
 #include "cobalt/media_session/media_session_client.h"
 #include "cobalt/script/callback_function.h"
 #include "cobalt/script/script_value.h"
 #include "cobalt/script/testing/fake_script_value.h"
 #include "cobalt/script/wrappable.h"
+#include "starboard/extension/media_session.h"
 #include "starboard/thread.h"
 #include "starboard/time.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -56,12 +56,11 @@
 
 class MockMediaSessionClient : public MediaSessionClient {
  public:
-  explicit MockMediaSessionClient(MediaSession* media_session) :
-      MediaSessionClient(media_session) {
-  }
+  explicit MockMediaSessionClient(MediaSession* media_session)
+      : MediaSessionClient(media_session) {}
 
-  void OnMediaSessionStateChanged(const MediaSessionState& session_state)
-      override {
+  void OnMediaSessionStateChanged(
+      const MediaSessionState& session_state) override {
     session_state_ = session_state;
     ++session_change_count_;
     MediaSessionClient::OnMediaSessionStateChanged(session_state);
@@ -109,9 +108,8 @@
 TEST(MediaSessionTest, MediaSessionTest) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   EXPECT_EQ(kMediaSessionPlaybackStateNone, session->playback_state());
@@ -120,7 +118,8 @@
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStatePlaying, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                   ->GetMediaSessionState()
+                                                   .actual_playback_state());
 
   EXPECT_EQ(session->mock_session_client()->GetMediaSessionChangeCount(), 1);
 }
@@ -128,9 +127,8 @@
 TEST(MediaSessionTest, ActualPlaybackState) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   // Trigger a session state change without impacting playback state.
@@ -139,46 +137,53 @@
   EXPECT_EQ(session->mock_session_client()->GetMediaSessionChangeCount(), 1);
 
   EXPECT_EQ(kMediaSessionPlaybackStateNone, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                ->GetMediaSessionState()
+                                                .actual_playback_state());
 
   session->mock_session_client()->UpdatePlatformPlaybackState(
       kCobaltExtensionMediaSessionPlaying);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStatePlaying, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                   ->GetMediaSessionState()
+                                                   .actual_playback_state());
 
   session->set_playback_state(kMediaSessionPlaybackStatePlaying);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStatePlaying, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                   ->GetMediaSessionState()
+                                                   .actual_playback_state());
 
   session->set_playback_state(kMediaSessionPlaybackStatePaused);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStatePlaying, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                   ->GetMediaSessionState()
+                                                   .actual_playback_state());
 
   session->mock_session_client()->UpdatePlatformPlaybackState(
       kCobaltExtensionMediaSessionPaused);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStatePaused, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                  ->GetMediaSessionState()
+                                                  .actual_playback_state());
 
   session->set_playback_state(kMediaSessionPlaybackStateNone);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStateNone, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                ->GetMediaSessionState()
+                                                .actual_playback_state());
 
   session->mock_session_client()->UpdatePlatformPlaybackState(
       kCobaltExtensionMediaSessionNone);
 
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(kMediaSessionPlaybackStateNone, session->mock_session_client()
-      ->GetMediaSessionState().actual_playback_state());
+                                                ->GetMediaSessionState()
+                                                .actual_playback_state());
 
   EXPECT_GE(session->mock_session_client()->GetMediaSessionChangeCount(), 2);
 }
@@ -186,9 +191,8 @@
 TEST(MediaSessionTest, NullActionClears) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   // Trigger a session state change without impacting playback state.
@@ -212,14 +216,18 @@
   session->SetActionHandler(kMediaSessionActionPlay, holder);
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(1, session->mock_session_client()
-      ->GetMediaSessionState().available_actions().to_ulong());
+                   ->GetMediaSessionState()
+                   .available_actions()
+                   .to_ulong());
   session->mock_session_client()->InvokeAction(
       kCobaltExtensionMediaSessionActionPlay);
 
   session->SetActionHandler(kMediaSessionActionPlay, null_holder);
   session->mock_session_client()->WaitForSessionStateChange();
   EXPECT_EQ(0, session->mock_session_client()
-      ->GetMediaSessionState().available_actions().to_ulong());
+                   ->GetMediaSessionState()
+                   .available_actions()
+                   .to_ulong());
   session->mock_session_client()->InvokeAction(
       kCobaltExtensionMediaSessionActionPlay);
 
@@ -231,9 +239,8 @@
 
   MediaSessionState state;
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   // Trigger a session state change without impacting playback state.
@@ -253,15 +260,13 @@
 
   session->mock_session_client()->WaitForSessionStateChange();
   state = session->mock_session_client()->GetMediaSessionState();
-  EXPECT_EQ(1 << kMediaSessionActionPlay,
-            state.available_actions().to_ulong());
+  EXPECT_EQ(1 << kMediaSessionActionPlay, state.available_actions().to_ulong());
 
   session->SetActionHandler(kMediaSessionActionPause, holder);
 
   session->mock_session_client()->WaitForSessionStateChange();
   state = session->mock_session_client()->GetMediaSessionState();
-  EXPECT_EQ(1 << kMediaSessionActionPlay,
-            state.available_actions().to_ulong());
+  EXPECT_EQ(1 << kMediaSessionActionPlay, state.available_actions().to_ulong());
 
   session->SetActionHandler(kMediaSessionActionSeekto, holder);
 
@@ -339,9 +344,8 @@
 TEST(MediaSessionTest, InvokeAction) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   MockCallbackFunction cf;
@@ -360,9 +364,8 @@
 TEST(MediaSessionTest, SeekDetails) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   MockCallbackFunction cf;
@@ -410,9 +413,8 @@
 TEST(MediaSessionTest, PositionState) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
 
   MediaSessionState state;
@@ -515,9 +517,8 @@
 TEST(MediaSessionTest, Metadata) {
   base::MessageLoop message_loop(base::MessageLoop::TYPE_DEFAULT);
 
-  scoped_refptr<MockMediaSession> session =
-      scoped_refptr<MockMediaSession>(new MockMediaSession(
-          new MockMediaSessionClient(nullptr)));
+  scoped_refptr<MockMediaSession> session = scoped_refptr<MockMediaSession>(
+      new MockMediaSession(new MockMediaSessionClient(nullptr)));
   session->media_session_client()->set_media_session(session);
   MediaSessionState state;
 
diff --git a/cobalt/network/BUILD.gn b/cobalt/network/BUILD.gn
index 53821e9..4af6239 100644
--- a/cobalt/network/BUILD.gn
+++ b/cobalt/network/BUILD.gn
@@ -86,7 +86,6 @@
   sources = [
     "//cobalt/content/ssl/certs/002c0b4f.0",
     "//cobalt/content/ssl/certs/02265526.0",
-    "//cobalt/content/ssl/certs/03179a64.0",
     "//cobalt/content/ssl/certs/062cdee6.0",
     "//cobalt/content/ssl/certs/064e0aa9.0",
     "//cobalt/content/ssl/certs/06dc52d5.0",
diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc
index 822a5d3..9de6143 100644
--- a/cobalt/persistent_storage/persistent_settings.cc
+++ b/cobalt/persistent_storage/persistent_settings.cc
@@ -48,10 +48,9 @@
   if (!thread_.Start()) return;
   DCHECK(message_loop());
 
-  std::vector<char> storage_dir(kSbFileMaxPath + 1, 0);
-  SbSystemGetPath(kSbSystemPathStorageDirectory, storage_dir.data(),
+  std::vector<char> storage_dir(kSbFileMaxPath, 0);
+  SbSystemGetPath(kSbSystemPathCacheDirectory, storage_dir.data(),
                   kSbFileMaxPath);
-
   persistent_settings_file_ =
       std::string(storage_dir.data()) + kSbFileSepString + file_name;
   LOG(INFO) << "Persistent settings file path: " << persistent_settings_file_;
diff --git a/cobalt/persistent_storage/persistent_settings.h b/cobalt/persistent_storage/persistent_settings.h
index 47cd446..0cf789f 100644
--- a/cobalt/persistent_storage/persistent_settings.h
+++ b/cobalt/persistent_storage/persistent_settings.h
@@ -34,7 +34,8 @@
 namespace persistent_storage {
 
 // PersistentSettings manages Cobalt settings that persist from one application
-// lifecycle to another as a JSON file in kSbSystemPathStorageDirectory.
+// lifecycle to another as a JSON file in kSbSystemPathCacheDirectory. The
+// persistent settings will persist until the cache is cleared.
 // PersistentSettings uses JsonPrefStore for most of its functionality.
 // JsonPrefStore maintains thread safety by requiring that all access occurs on
 // the Sequence that created it.
diff --git a/cobalt/persistent_storage/persistent_settings_test.cc b/cobalt/persistent_storage/persistent_settings_test.cc
index 4a3f3a7..3f88397 100644
--- a/cobalt/persistent_storage/persistent_settings_test.cc
+++ b/cobalt/persistent_storage/persistent_settings_test.cc
@@ -44,7 +44,7 @@
             base::test::ScopedTaskEnvironment::MainThreadType::DEFAULT,
             base::test::ScopedTaskEnvironment::ExecutionMode::ASYNC) {
     std::vector<char> storage_dir(kSbFileMaxPath + 1, 0);
-    SbSystemGetPath(kSbSystemPathStorageDirectory, storage_dir.data(),
+    SbSystemGetPath(kSbSystemPathCacheDirectory, storage_dir.data(),
                     kSbFileMaxPath);
 
     persistent_settings_file_ = std::string(storage_dir.data()) +
@@ -69,7 +69,8 @@
 };
 
 TEST_F(PersistentSettingTest, GetDefaultBool) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -84,15 +85,15 @@
             persistent_settings->GetPersistentSettingAsInt("key", true));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(4.2), std::move(closure));
   test_done_.Wait();
-  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetSetBool) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   base::OnceClosure closure = base::BindOnce(
@@ -104,7 +105,7 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(true), std::move(closure));
@@ -120,17 +121,17 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(false), std::move(closure));
 
   test_done_.Wait();
-  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetDefaultInt) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -145,7 +146,7 @@
         ASSERT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(4.2), std::move(closure));
@@ -153,7 +154,8 @@
 }
 
 TEST_F(PersistentSettingTest, GetSetInt) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   base::OnceClosure closure = base::BindOnce(
@@ -162,7 +164,7 @@
         ASSERT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(-1), std::move(closure));
   test_done_.Wait();
@@ -174,7 +176,7 @@
         ASSERT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(0), std::move(closure));
   test_done_.Wait();
@@ -186,14 +188,15 @@
         ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(42), std::move(closure));
   test_done_.Wait();
 }
 
 TEST_F(PersistentSettingTest, GetDefaultString) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -215,15 +218,15 @@
                                "key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(4.2), std::move(closure));
   test_done_.Wait();
-  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetSetString) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   base::OnceClosure closure = base::BindOnce(
@@ -233,7 +236,7 @@
                           "key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(""), std::move(closure));
@@ -248,7 +251,7 @@
             persistent_settings->GetPersistentSettingAsString("key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>("hello there"), std::move(closure));
@@ -262,7 +265,7 @@
                             "key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>("42"), std::move(closure));
   test_done_.Wait();
@@ -275,7 +278,7 @@
                             "key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>("\n"), std::move(closure));
   test_done_.Wait();
@@ -288,15 +291,178 @@
                              "key", "hello"));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>("\\n"), std::move(closure));
   test_done_.Wait();
   test_done_.Reset();
 }
 
+TEST_F(PersistentSettingTest, GetSetList) {
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+  persistent_settings->ValidatePersistentSettings();
+
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+        ASSERT_FALSE(test_list.empty());
+        ASSERT_EQ(1, test_list.size());
+        ASSERT_TRUE(test_list[0].is_string());
+        ASSERT_EQ("hello", test_list[0].GetString());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  std::vector<base::Value> list;
+  list.emplace_back("hello");
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(list), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+        ASSERT_FALSE(test_list.empty());
+        ASSERT_EQ(2, test_list.size());
+        ASSERT_TRUE(test_list[0].is_string());
+        ASSERT_EQ("hello", test_list[0].GetString());
+        ASSERT_TRUE(test_list[1].is_string());
+        ASSERT_EQ("there", test_list[1].GetString());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  list.emplace_back("there");
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(list), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+        ASSERT_FALSE(test_list.empty());
+        ASSERT_EQ(3, test_list.size());
+        ASSERT_TRUE(test_list[0].is_string());
+        ASSERT_EQ("hello", test_list[0].GetString());
+        ASSERT_TRUE(test_list[1].is_string());
+        ASSERT_EQ("there", test_list[1].GetString());
+        ASSERT_TRUE(test_list[2].is_int());
+        ASSERT_EQ(42, test_list[2].GetInt());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  list.emplace_back(42);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(list), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+}
+
+TEST_F(PersistentSettingTest, GetSetDictionary) {
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+  persistent_settings->ValidatePersistentSettings();
+
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_dict =
+            persistent_settings->GetPersistentSettingAsDictionary("key");
+        ASSERT_FALSE(test_dict.empty());
+        ASSERT_EQ(1, test_dict.size());
+        ASSERT_TRUE(test_dict["key_string"]->is_string());
+        ASSERT_EQ("hello", test_dict["key_string"]->GetString());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+  dict.try_emplace("key_string", std::make_unique<base::Value>("hello"));
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(dict), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_dict =
+            persistent_settings->GetPersistentSettingAsDictionary("key");
+        ASSERT_FALSE(test_dict.empty());
+        ASSERT_EQ(2, test_dict.size());
+        ASSERT_TRUE(test_dict["key_string"]->is_string());
+        ASSERT_EQ("hello", test_dict["key_string"]->GetString());
+        ASSERT_TRUE(test_dict["key_int"]->is_int());
+        ASSERT_EQ(42, test_dict["key_int"]->GetInt());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  dict.try_emplace("key_int", std::make_unique<base::Value>(42));
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(dict), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+}
+
+TEST_F(PersistentSettingTest, URLAsKey) {
+  // Tests that json_pref_store has the correct SetValue and
+  // RemoveValue changes for using a URL as a PersistentSettings
+  // Key.
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+  persistent_settings->ValidatePersistentSettings();
+
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
+            "http://127.0.0.1:45019/");
+        ASSERT_FALSE(test_dict.empty());
+        ASSERT_EQ(1, test_dict.size());
+        ASSERT_TRUE(test_dict["http://127.0.0.1:45019/"]->is_string());
+        ASSERT_EQ("Dictionary URL Key Works!",
+                  test_dict["http://127.0.0.1:45019/"]->GetString());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+
+  // Test that json_pref_store uses SetKey instead of Set, making URL
+  // keys viable.
+  base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+  dict.try_emplace("http://127.0.0.1:45019/",
+                   std::make_unique<base::Value>("Dictionary URL Key Works!"));
+  persistent_settings->SetPersistentSetting("http://127.0.0.1:45019/",
+                                            std::make_unique<base::Value>(dict),
+                                            std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
+            "http://127.0.0.1:45019/");
+        ASSERT_TRUE(test_dict.empty());
+        test_done->Signal();
+      },
+      persistent_settings.get(), &test_done_);
+  persistent_settings->RemovePersistentSetting("http://127.0.0.1:45019/",
+                                               std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+}
+
 TEST_F(PersistentSettingTest, RemoveSetting) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -311,7 +477,7 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(true), std::move(closure));
   test_done_.Wait();
@@ -326,16 +492,15 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->RemovePersistentSetting("key", std::move(closure));
   test_done_.Wait();
   test_done_.Reset();
-
-  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, DeleteSettings) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -350,7 +515,7 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(true), std::move(closure));
   test_done_.Wait();
@@ -365,16 +530,15 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->DeletePersistentSettings(std::move(closure));
   test_done_.Wait();
   test_done_.Reset();
-
-  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, InvalidSettings) {
-  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  auto persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -389,18 +553,18 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(true), std::move(closure));
   test_done_.Wait();
   test_done_.Reset();
 
-  delete persistent_settings;
   // Sleep for one second to allow for the previous persistent_setting's
   // JsonPrefStore instance time to write to disk before creating a new
   // persistent_settings and JsonPrefStore instance.
   base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
-  persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
@@ -413,20 +577,18 @@
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
-      persistent_settings, &test_done_);
+      persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
       "key", std::make_unique<base::Value>(false), std::move(closure));
   test_done_.Wait();
   test_done_.Reset();
 
-  delete persistent_settings;
-  persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+  persistent_settings =
+      std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
-
-  delete persistent_settings;
 }
 
 }  // namespace persistent_storage
diff --git a/cobalt/renderer/backend/graphics_context.h b/cobalt/renderer/backend/graphics_context.h
index aaf4569..358ccbc 100644
--- a/cobalt/renderer/backend/graphics_context.h
+++ b/cobalt/renderer/backend/graphics_context.h
@@ -18,9 +18,9 @@
 #include <memory>
 
 #include "base/memory/ref_counted.h"
-#include "cobalt/extension/graphics.h"
 #include "cobalt/math/size.h"
 #include "cobalt/renderer/backend/render_target.h"
+#include "starboard/extension/graphics.h"
 
 namespace cobalt {
 namespace renderer {
diff --git a/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl b/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl
new file mode 100644
index 0000000..936209f
--- /dev/null
+++ b/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl
@@ -0,0 +1,29 @@
+#version 100

+

+precision mediump float;

+precision mediump sampler2D;

+uniform mediump vec4 uColor_Stage0;

+uniform mediump vec4 ucolor_Stage1;

+uniform sampler2D uTextureSampler_0_Stage0;

+varying highp vec2 vTextureCoords_Stage0;

+varying highp float vTexIndex_Stage0;

+void main() {

+    mediump vec4 outputColor_Stage0;

+    {

+        outputColor_Stage0 = uColor_Stage0;

+        mediump vec4 texColor;

+        {

+            texColor = texture2D(uTextureSampler_0_Stage0, vTextureCoords_Stage0);

+        }

+        outputColor_Stage0 = outputColor_Stage0 * texColor;

+    }

+    mediump vec4 output_Stage1;

+    {

+        {

+            output_Stage1 = outputColor_Stage0 * ucolor_Stage1;

+        }

+    }

+    {

+        gl_FragColor = output_Stage1;

+    }

+}

diff --git a/cobalt/renderer/pipeline.cc b/cobalt/renderer/pipeline.cc
index 98a2eab..1b27cbf 100644
--- a/cobalt/renderer/pipeline.cc
+++ b/cobalt/renderer/pipeline.cc
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <memory>
-
 #include "cobalt/renderer/pipeline.h"
 
+#include <memory>
+
 #include "base/bind.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
@@ -24,13 +24,13 @@
 #include "cobalt/base/address_sanitizer.h"
 #include "cobalt/base/cobalt_paths.h"
 #include "cobalt/base/polymorphic_downcast.h"
-#include "cobalt/extension/graphics.h"
 #include "cobalt/math/rect_f.h"
 #include "cobalt/render_tree/clear_rect_node.h"
 #include "cobalt/render_tree/composition_node.h"
 #include "cobalt/render_tree/dump_render_tree_to_string.h"
 #include "cobalt/watchdog/watchdog.h"
 #include "nb/memory_scope.h"
+#include "starboard/extension/graphics.h"
 #include "starboard/system.h"
 
 namespace cobalt {
diff --git a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
index 40bfa2f..6beb0a0 100644
--- a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
+++ b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
@@ -20,11 +20,11 @@
 
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
-#include "cobalt/extension/graphics.h"
 #include "cobalt/math/size.h"
 #include "cobalt/renderer/backend/egl/utils.h"
 #include "cobalt/renderer/egl_and_gles.h"
 #include "starboard/configuration.h"
+#include "starboard/extension/graphics.h"
 #include "third_party/glm/glm/gtc/type_ptr.hpp"
 
 namespace cobalt {
@@ -124,7 +124,9 @@
     case TexturedMeshRenderer::Image::YUV_3PLANE_10BIT_BT2020: {
       return k10BitBT2020ColorMatrix;
     } break;
-    default: { NOTREACHED(); }
+    default: {
+      NOTREACHED();
+    }
   }
   return NULL;
 }
@@ -826,7 +828,9 @@
             color_matrix, texture_infos,
             CreateUYVYFragmentShader(texture_target, *texture_wrap_s));
       } break;
-      default: { NOTREACHED(); }
+      default: {
+        NOTREACHED();
+      }
     }
 
     // Save our shader into the cache.
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
index ee32ab1..b64037d 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.h"
+
 #include <memory>
 #include <utility>
 
-#include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.h"
-
 #include "SkData.h"
 #include "SkGraphics.h"
 #include "SkStream.h"
@@ -28,10 +28,10 @@
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/language.h"
 #include "cobalt/configuration/configuration.h"
-#include "cobalt/extension/font.h"
 #include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.h"
 #include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.h"
 #include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.h"
+#include "starboard/extension/font.h"
 #include "third_party/icu/source/common/unicode/locid.h"
 
 const char* ROBOTO_SCRIPT = "latn";
diff --git a/cobalt/script/array_buffer.h b/cobalt/script/array_buffer.h
index 1bbaf7a..2618084 100644
--- a/cobalt/script/array_buffer.h
+++ b/cobalt/script/array_buffer.h
@@ -16,7 +16,7 @@
 #define COBALT_SCRIPT_ARRAY_BUFFER_H_
 
 #include <algorithm>
-#include <memory>
+#include <utility>
 
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
@@ -49,9 +49,8 @@
 
   // Create a new |ArrayBuffer| from existing block of memory.  See
   // |PreallocatedArrayBufferData| for details.
-  static Handle<ArrayBuffer> New(
-      GlobalEnvironment* global_environment,
-      std::unique_ptr<PreallocatedArrayBufferData> data);
+  static Handle<ArrayBuffer> New(GlobalEnvironment* global_environment,
+                                 PreallocatedArrayBufferData&& data);
 
   virtual ~ArrayBuffer() {}
 
@@ -72,10 +71,10 @@
 //   PreallocatedArrayBufferData data(16);
 //
 //   // Manipulate the data however you want.
-//   static_cast<uint8_t*>(data.data())[0] = 0xFF;
+//   data.data()[0] = 0xFF;
 //
 //   // Create a new |ArrayBuffer| using |data|.
-//   auto array_buffer = ArrayBuffer::New(env, &data);
+//   auto array_buffer = ArrayBuffer::New(env, std::move(data));
 //
 //   // |PreallocatedData| now no longer holds anything, data should now be
 //   // accessed from the ArrayBuffer itself.
@@ -86,12 +85,14 @@
   explicit PreallocatedArrayBufferData(size_t byte_length);
   ~PreallocatedArrayBufferData();
 
-  PreallocatedArrayBufferData(PreallocatedArrayBufferData&& other) = default;
+  PreallocatedArrayBufferData(PreallocatedArrayBufferData&& other) {
+    other.Detach(&data_, &byte_length_);
+  }
   PreallocatedArrayBufferData& operator=(PreallocatedArrayBufferData&& other) =
-      default;
+      delete;
 
-  void* data() { return data_; }
-  const void* data() const { return data_; }
+  uint8_t* data() { return data_; }
+  const uint8_t* data() const { return data_; }
   size_t byte_length() const { return byte_length_; }
 
   void Swap(PreallocatedArrayBufferData* that) {
@@ -106,7 +107,7 @@
   PreallocatedArrayBufferData(const PreallocatedArrayBufferData&) = delete;
   void operator=(const PreallocatedArrayBufferData&) = delete;
 
-  void Detach(void** data, size_t* byte_length) {
+  void Detach(uint8_t** data, size_t* byte_length) {
     DCHECK(data);
     DCHECK(byte_length);
 
@@ -117,7 +118,7 @@
     byte_length_ = 0u;
   }
 
-  void* data_ = nullptr;
+  uint8_t* data_ = nullptr;
   size_t byte_length_ = 0u;
 
   friend ArrayBuffer;
diff --git a/cobalt/script/script_value.h b/cobalt/script/script_value.h
index 25567d7..4fcec5e 100644
--- a/cobalt/script/script_value.h
+++ b/cobalt/script/script_value.h
@@ -71,6 +71,7 @@
         : owner_(wrappable), referenced_value_(script_value.MakeCopy()) {
       DCHECK(!referenced_value_->IsNull());
       referenced_value_->RegisterOwner(owner_);
+      referenced_value_->PreventGarbageCollection();
     }
 
     Reference(Wrappable* wrappable, const Handle<T>& local)
@@ -78,6 +79,7 @@
           referenced_value_(local.GetScriptValue()->MakeCopy()) {
       DCHECK(!referenced_value_->IsNull());
       referenced_value_->RegisterOwner(owner_);
+      referenced_value_->PreventGarbageCollection();
     }
 
     const T& value() const { return *(referenced_value_->GetValue()); }
@@ -89,7 +91,10 @@
       return *(referenced_value_.get());
     }
 
-    ~Reference() { referenced_value_->DeregisterOwner(owner_); }
+    ~Reference() {
+      referenced_value_->AllowGarbageCollection();
+      referenced_value_->DeregisterOwner(owner_);
+    }
 
    private:
     Wrappable* const owner_;
diff --git a/cobalt/script/v8c/v8c_array_buffer.cc b/cobalt/script/v8c/v8c_array_buffer.cc
index 835c320..939ea56 100644
--- a/cobalt/script/v8c/v8c_array_buffer.cc
+++ b/cobalt/script/v8c/v8c_array_buffer.cc
@@ -23,7 +23,7 @@
 namespace script {
 
 PreallocatedArrayBufferData::PreallocatedArrayBufferData(size_t byte_length) {
-  data_ = SbMemoryAllocate(byte_length);
+  data_ = static_cast<uint8_t*>(SbMemoryAllocate(byte_length));
   byte_length_ = byte_length;
 }
 
@@ -36,7 +36,10 @@
 }
 
 void PreallocatedArrayBufferData::Resize(size_t new_byte_length) {
-  data_ = SbMemoryReallocate(data_, new_byte_length);
+  if (byte_length_ == new_byte_length) {
+    return;
+  }
+  data_ = static_cast<uint8_t*>(SbMemoryReallocate(data_, new_byte_length));
   byte_length_ = new_byte_length;
 }
 
@@ -56,19 +59,18 @@
 }
 
 // static
-Handle<ArrayBuffer> ArrayBuffer::New(
-    GlobalEnvironment* global_environment,
-    std::unique_ptr<PreallocatedArrayBufferData> data) {
+Handle<ArrayBuffer> ArrayBuffer::New(GlobalEnvironment* global_environment,
+                                     PreallocatedArrayBufferData&& data) {
   auto* v8c_global_environment =
       base::polymorphic_downcast<v8c::V8cGlobalEnvironment*>(
           global_environment);
   v8::Isolate* isolate = v8c_global_environment->isolate();
   v8c::EntryScope entry_scope(isolate);
 
-  void* buffer;
+  uint8_t* buffer;
   size_t byte_length;
 
-  data->Detach(&buffer, &byte_length);
+  data.Detach(&buffer, &byte_length);
 
   v8::Local<v8::ArrayBuffer> array_buffer = v8::ArrayBuffer::New(
       isolate, buffer, byte_length, v8::ArrayBufferCreationMode::kInternalized);
diff --git a/cobalt/site/docs/codelabs/cobalt_extensions/codelab.md b/cobalt/site/docs/codelabs/cobalt_extensions/codelab.md
deleted file mode 100644
index c358376..0000000
--- a/cobalt/site/docs/codelabs/cobalt_extensions/codelab.md
+++ /dev/null
@@ -1,793 +0,0 @@
----
-layout: doc
-title: "Cobalt Extensions codelab"
----
-
-The Cobalt Extension framework provides a way to add optional, platform-specific
-features to the Cobalt application. A Cobalt Extension is an optional interface
-that porters can implement for their platforms if, and as, they wish.
-
-This tutorial uses coding exercises to guide you through the process of creating
-a simple example of a Cobalt Extension. By the end you should have a firm
-understanding of what Cobalt Extensions are, when to use them instead of
-alternatives, how to write them, and how to work with the Cobalt team to
-contribute them to the repository.
-
-## Prerequisites
-
-Because it's helpful to build and run Cobalt during the exercises, you'll first
-want to set up your environment and make sure you can build Cobalt. You can
-follow <a href="/development/setup-linux.html">Set up your environment -
-Linux</a> to do this if you're a Linux user. Please note that the exercise
-solutions assume you're using Linux but should be comparable to implementations
-for other platforms.
-
-Also note that while this codelab doesn't require it, you'll need to
-<a href="/starboard/porting.html">Port Cobalt to your platform</a> before you
-can actually use a Cobalt Extension to customize it for your platform.
-
-Finally, the exercises assume the ability to program in C and C++.
-
-### Exercise 0: Run Cobalt and inspect logs
-
-Assuming you've already built Cobalt, please now run Cobalt and pay special
-attention to a message it logs when it starts up. This message will be the focus
-of subsequent exercises.
-
-```
-$ out/linux-x64x11_debug/cobalt 2>/dev/null | grep "Starting application"
-```
-
-## Background
-
-Situated below Cobalt is Starboard. Starboard, which is a porting layer and OS
-abstraction, contains a minimal set of APIs to encapsulate the platform-specific
-functionalities that Cobalt uses. Each Starboard module (memory, socket, thread,
-etc.) defines functions that must be implemented on a porter's platform,
-imposing an implementation and maintenance cost on all porters. With this cost
-in mind the Cobalt team tries to keep the Starboard APIs stable and only adds a
-new API **when some functionality is required by Cobalt but the implementation
-depends on the platform**.
-
-A Starboard API can be made optional, though, by the introduction of an
-accompanying API that asks platforms whether they support the underlying feature
-and enables Cobalt to check for the answer at runtime. For example,
-`SbWindowOnScreenKeyboardIsSupported` is used so that only platforms that
-support an on screen keyboard need implement the related functions in
-`starboard/window.h`. To spare porters uninterested in the functionality, the
-Cobalt team chooses to make a Starboard API optional **when some Cobalt
-functionality is optional and the implementation is platform-dependent**.
-
-Finally, a nonobvious point explains why even an optional Starboard API may
-sometimes be too cumbersome: other applications beyond Cobalt are able to be run
-on top of Starboard. If a feature is needed by Cobalt but not by all Starboard-
-based applications or by Starboard itself, adding a Starboard API for it may add
-unnecessary size and complexity to the porting layer. **And here we arrive at
-the sweet spot for Cobalt Extensions: when the desired functionality is
-Cobalt-specific, optional in Cobalt, and has platform-dependent
-implementation.** Also, because Cobalt Extensions are lightweight and, as you'll
-see below, added without any changes to the Starboard layer, they're the
-preferred way for porters to add new, custom features to Cobalt.
-
-To summarize:
-
-<table>
-  <tr>
-    <th colspan="1">Tool</th>
-    <th colspan="1">Use case</th>
-    <th colspan="1">Ecosystem cost</th>
-  </tr>
-  <tr>
-    <td>Starboard API</td>
-    <td>Feature is <strong>required</strong> but implementation is
-    platform-dependent</td>
-    <td>High</td>
-  </tr>
-  <tr>
-    <td>Optional Starboard API</td>
-    <td>Feature is <strong>optional</strong> and implementation is
-    platform-dependent</td>
-    <td>Medium</td>
-  </tr>
-  <tr>
-    <td>Cobalt Extension</td>
-    <td>Feature is <strong>optional and specific to Cobalt</strong> and
-    implementation is platform-dependent</td>
-    <td>Low</td>
-  </tr>
-</table>
-
-As a caveat, please note that for all three of these abstractions the interface
-is in Cobalt's open-source repository and therefore visible to other porters.
-The implementation, on the other hand, is written and built by porters and so
-may be kept private.
-
-Finally, in addition to the alternatives mentioned, porters have in some cases
-made local changes to Cobalt, above the Starboard layer, to achieve some
-customization or optimization. This has been discouraged by the Cobalt team
-because it makes rebasing to future versions of Cobalt more difficult but has
-been possible because porters have historically built **both** Cobalt and
-Starboard. However, Cobalt is moving toward Evergreen
-(<a href="https://cobalt.googlesource.com/cobalt/+/refs/heads/master/starboard/doc/evergreen/cobalt_evergreen_overview.md">overview</a>),
-an architecture that enables automatic Cobalt updates on devices by separating a
-Google-built, Cobalt core shared library from the partner-built Starboard layer
-and Cobalt loader app. Because Cobalt core code is built by Google, custom
-changes to it are no longer possible for partners using Evergreen.
-
-## Anatomy of a Cobalt Extension
-
-### Extension structures
-
-Cobalt uses a structure to describe the interface for an extension and organizes
-the structures in headers under `cobalt/extension/`. The header for a "foo"
-extension should be named `foo.h` and the first version of it should contain the
-following content, as well as any additional members that provide the "foo"
-functionality.
-
-```
-#ifndef COBALT_EXTENSION_FOO_H_
-#define COBALT_EXTENSION_FOO_H_
-
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionFooName "dev.cobalt.extension.Foo"
-
-typedef struct CobaltExtensionFooApi {
-  // Name should be the string |kCobaltExtensionFooName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-
-} CobaltExtensionFooApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-#endif  // COBALT_EXTENSION_FOO_H_
-```
-
-Please note a few important points about this structure:
-
-*   The first two members must be, in order:
-    *   A `const char* |name|`, storing the extension's name.
-    *   A `uint32_t |version|`, storing the version number of the extension.
-        Extension versioning is discussed later on in this codelab.
-*   The following members can be any C types (including custom structures) that
-    are useful. Often, these are function pointers.
-
-### Extension access in Cobalt
-
-The `SbSystemGetExtension` function from Starboard's `system` module allows
-Cobalt to query for an extension by name. It returns a pointer to the constant,
-global instance of the structure implementing the extension with the given name
-if it exists, otherwise `NULL`. This function is the only mechanism Cobalt has
-to get an extension; the Starboard interface intentionally doesn't have any
-functions related to the specific extensions.
-
-The caller in Cobalt must static cast the `const void*` returned by
-`SbSystemGetExtension` to a `const CobaltExtensionFooApi*`, or pointer of
-whatever type the extension structure type happens to be, before using it. Since
-the caller can't be sure whether a platform implements the extension or, if it
-does, implements it correctly, it's good defensive programming to check that the
-resulting pointer is not `NULL` and that the `name` member in the pointed-to
-structure has the same value as `kCobaltExtensionFooName`.
-
-### Extension implementation
-
-Because Cobalt Extensions are platform-dependent, the implementations of an
-extension belong in Starboard ports. A Starboard port implements an extension by
-defining a constant, global instance of the structure and implementing the
-`SbSystemGetExtension` function to return a pointer to it. For our "foo"
-extension, an implementation for `custom_platform`'s Starboard port could look
-as follows.
-
-`starboard/custom_platform/foo.h` declares a `GetFooApi` accessor for the
-structure instance.
-
-```
-#ifndef STARBOARD_CUSTOM_PLATFORM_FOO_H_
-#define STARBOARD_CUSTOM_PLATFORM_FOO_H_
-
-namespace starboard {
-namespace custom_platform {
-
-const void* GetFooApi();
-
-}  // namespace custom_platform
-}  // namespace starboard
-
-#endif  // STARBOARD_CUSTOM_PLATFORM_FOO_H_
-```
-
-`starboard/custom_platform/foo.cc`, then, defines `GetFooApi`.
-
-```
-#include "starboard/custom_platform/foo.h"
-
-#include "cobalt/extension/foo.h"
-
-namespace starboard {
-namespace custom_platform {
-
-namespace {
-
-// Definitions of any functions included as components in the extension
-// are added here.
-
-const CobaltExtensionFooApi kFooApi = {
-    kCobaltExtensionFooName,
-    1,  // API version that's implemented.
-    // Any additional members are initialized here.
-};
-
-}  // namespace
-
-const void* GetFooApi() {
-  return &kFooApi;
-}
-
-}  // namespace custom_platform
-}  // namespace starboard
-```
-
-Finally, `starboard/custom_platform/system_get_extension.cc` defines
-`SbSystemGetExtension` to wire up the extensions for the platform.
-
-```
-#include "starboard/system.h"
-
-#include "cobalt/extension/foo.h"
-#include "starboard/common/string.h"
-#include "starboard/custom_platform/foo.h"
-
-const void* SbSystemGetExtension(const char* name) {
-  if (strcmp(name, kCobaltExtensionFooName) == 0) {
-    return starboard::custom_platform::GetFooApi();
-  }
-  // Other conditions here should handle other implemented extensions.
-
-  return NULL;
-}
-```
-
-Please feel free to browse existing extension implementations in the repository.
-For example, the reference Raspberry Pi port implements the `Graphics` extension
-across the following files.
-
-*   `starboard/raspi/shared/graphics.h`
-*   `starboard/raspi/shared/graphics.cc`
-*   `starboard/raspi/shared/system_get_extensions.cc`
-
-### Exercise 1: Write and use your first extension
-
-Now that you've seen the anatomy of a Cobalt Extension it's your turn to write
-one of your own. In Exercise 0 we saw that Cobalt logs "Starting application"
-when it's started. Please write a `Pleasantry` Cobalt Extension that has a
-member of type `const char*` and name `greeting` and make any necessary changes
-in `cobalt/browser/main.cc` so that the extension can be used to log a custom
-greeting directly after the plain "Starting application." Implement the
-extension for Linux, or whichever other platform you'd like, and confirm that
-the greeting is logged.
-
-#### Solution to Exercise 1
-
-Click the items below to expand parts of a solution. The `git diff`s are between
-the solution and the `master` branch.
-
-<details>
-    <summary style="display:list-item">Contents of new
-    `cobalt/extension/pleasantry.h` file.</summary>
-
-```
-#ifndef COBALT_EXTENSION_PLEASANTRY_H_
-#define COBALT_EXTENSION_PLEASANTRY_H_
-
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionPleasantryName "dev.cobalt.extension.Pleasantry"
-
-typedef struct CobaltExtensionPleasantryApi {
-  // Name should be the string |kCobaltExtensionPleasantryName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-  const char* greeting;
-
-} CobaltExtensionPleasantryApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-#endif  // COBALT_EXTENSION_PLEASANTRY_H_
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">Contents of new
-    `starboard/linux/shared/pleasantry.h` file.</summary>
-
-```
-#ifndef STARBOARD_LINUX_SHARED_PLEASANTRY_H_
-#define STARBOARD_LINUX_SHARED_PLEASANTRY_H_
-
-namespace starboard {
-namespace shared {
-
-const void* GetPleasantryApi();
-
-}  // namespace shared
-}  // namespace starboard
-
-#endif  // STARBOARD_LINUX_SHARED_PLEASANTRY_H_
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">Contents of new
-    `starboard/linux/shared/pleasantry.cc` file.</summary>
-
-```
-#include "starboard/linux/shared/pleasantry.h"
-
-#include "cobalt/extension/pleasantry.h"
-
-namespace starboard {
-namespace shared {
-
-namespace {
-
-const char *kGreeting = "Happy debugging!";
-
-const CobaltExtensionPleasantryApi kPleasantryApi = {
-    kCobaltExtensionPleasantryName,
-    1,
-    kGreeting,
-};
-
-}  // namespace
-
-const void* GetPleasantryApi() {
-  return &kPleasantryApi;
-}
-
-}  // namespace shared
-}  // namespace starboard
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">`git diff
-    starboard/linux/shared/starboard_platform.gypi`</summary>
-
-```
-@@ -38,6 +38,8 @@
-       '<(DEPTH)/starboard/linux/shared/netlink.cc',
-       '<(DEPTH)/starboard/linux/shared/netlink.h',
-       '<(DEPTH)/starboard/linux/shared/player_components_factory.cc',
-+      '<(DEPTH)/starboard/linux/shared/pleasantry.cc',
-+      '<(DEPTH)/starboard/linux/shared/pleasantry.h',
-       '<(DEPTH)/starboard/linux/shared/routes.cc',
-       '<(DEPTH)/starboard/linux/shared/routes.h',
-       '<(DEPTH)/starboard/linux/shared/system_get_connection_type.cc',
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">`git diff
-    starboard/linux/shared/system_get_extensions.cc`</summary>
-
-```
-@@ -16,12 +16,14 @@
-
- #include "cobalt/extension/configuration.h"
- #include "cobalt/extension/crash_handler.h"
-+#include "cobalt/extension/pleasantry.h"
- #include "starboard/common/string.h"
- #include "starboard/shared/starboard/crash_handler.h"
- #if SB_IS(EVERGREEN_COMPATIBLE)
- #include "starboard/elf_loader/evergreen_config.h"
- #endif
- #include "starboard/linux/shared/configuration.h"
-+#include "starboard/linux/shared/pleasantry.h"
-
- const void* SbSystemGetExtension(const char* name) {
- #if SB_IS(EVERGREEN_COMPATIBLE)
-@@ -41,5 +43,8 @@ const void* SbSystemGetExtension(const char* name) {
-   if (strcmp(name, kCobaltExtensionCrashHandlerName) == 0) {
-     return starboard::common::GetCrashHandlerApi();
-   }
-+  if (strcmp(name, kCobaltExtensionPleasantryName) == 0) {
-+    return starboard::shared::GetPleasantryApi();
-+  }
-   return NULL;
- }
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">`git diff cobalt/browser/main.cc`
-    </summary>
-
-```
-@@ -18,7 +18,9 @@
- #include "cobalt/base/wrap_main.h"
- #include "cobalt/browser/application.h"
- #include "cobalt/browser/switches.h"
-+#include "cobalt/extension/pleasantry.h"
- #include "cobalt/version.h"
-+#include "starboard/system.h"
-
- namespace {
-
-@@ -77,6 +79,14 @@ void StartApplication(int argc, char** argv, const char* link,
-     return;
-   }
-   LOG(INFO) << "Starting application.";
-+  const CobaltExtensionPleasantryApi* pleasantry_extension =
-+      static_cast<const CobaltExtensionPleasantryApi*>(
-+          SbSystemGetExtension(kCobaltExtensionPleasantryName));
-+  if (pleasantry_extension &&
-+      strcmp(pleasantry_extension->name, kCobaltExtensionPleasantryName) == 0 &&
-+      pleasantry_extension->version >= 1) {
-+    LOG(INFO) << pleasantry_extension->greeting;
-+  }
- #if SB_API_VERSION >= 13
-   DCHECK(!g_application);
-   g_application = new cobalt::browser::Application(quit_closure,
-```
-
-</details>
-
-## Extension versioning
-
-Cobalt Extensions are themselves extensible, but care must be taken to ensure
-that the extension interface in Cobalt and implementation in a platform's port,
-which may be built separately, are consistent.
-
-The `version` member, which is always the second member in an extension
-structure, indicates which version of the interface the structure describes. In
-other words, a `version` of the extension structure corresponds to a specific,
-invariant list of members. By convention, the first version of a Cobalt
-Extension is version `1` (i.e., one-based indexing, not zero-based).
-
-A new version of the extension can be introduced in the structure declaration by
-adding additional members to the end of the declaration and adding a comment to
-delineate, e.g., "The fields below this point were added in version 2 or later."
-To maintain compatibility and enable Cobalt to correctly index into instances of
-the structure, it's important that members are always added at the end of the
-structure declaration and that members are never removed. If a member is
-deprecated in a later version, this fact should simply be noted with a comment
-in the structure declaration.
-
-To implement a new version of the extension, a platform's port should then set
-the `version` member to the appropriate value when creating the instance of the
-structure, and also initialize all members required for the version.
-
-Finally, any code in Cobalt that uses the extension should guard references to
-members with version checks.
-
-### Exercise 2: Version your extension
-
-Add a second version of the `Pleasantry` extension that enables porters to also
-log a polite farewell message when the Cobalt application is stopped. To allow
-platforms more flexibility, add the new `farewell` member as a pointer to a
-function that takes no parameters and returns a `const char*`. Update
-`cobalt/browser/main.cc` so that Cobalt, if the platform implements version 2 of
-this extension, replaces the "Stopping application." message with a polite
-farewell provided by the platform.
-
-To keep things interesting, have the platform's implementation pseudo-randomly
-return one of several messages. And, once you've made the changes, build Cobalt
-and run it a few times to confirm that the feature behaves as expected.
-
-#### Solution to Exercise 2
-
-Click the items below to expand parts of a solution. The `git diff` is between
-the solution and the `master` branch.
-
-<details>
-    <summary style="display:list-item">Updated contents of
-    `cobalt/extension/pleasantry.h`.</summary>
-
-```
-#ifndef COBALT_EXTENSION_PLEASANTRY_H_
-#define COBALT_EXTENSION_PLEASANTRY_H_
-
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#define kCobaltExtensionPleasantryName "dev.cobalt.extension.Pleasantry"
-
-typedef struct CobaltExtensionPleasantryApi {
-  // Name should be the string |kCobaltExtensionPleasantryName|.
-  // This helps to validate that the extension API is correct.
-  const char* name;
-
-  // This specifies the version of the API that is implemented.
-  uint32_t version;
-
-  // The fields below this point were added in version 1 or later.
-  const char* greeting;
-
-  // The fields below this point were added in version 2 or later.
-  const char* (*GetFarewell)();
-
-} CobaltExtensionPleasantryApi;
-
-#ifdef __cplusplus
-}  // extern "C"
-#endif
-
-#endif  // COBALT_EXTENSION_PLEASANTRY_H_
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">Updated contents of
-    `starboard/linux/shared/pleasantry.cc`.</summary>
-
-```
-#include "starboard/linux/shared/pleasantry.h"
-
-#include <stdlib.h>
-
-#include "cobalt/extension/pleasantry.h"
-#include "starboard/system.h"
-#include "starboard/time.h"
-
-namespace starboard {
-namespace shared {
-
-namespace {
-
-const char* kGreeting = "Happy debugging!";
-
-const char* kFarewells[] = {
-  "Farewell",
-  "Take care",
-  "Thanks for running Cobalt",
-};
-
-const char* GetFarewell() {
-  srand (SbTimeGetNow());
-  int pseudo_random_index = rand() % SB_ARRAY_SIZE_INT(kFarewells);
-  return kFarewells[pseudo_random_index];
-}
-
-const CobaltExtensionPleasantryApi kPleasantryApi = {
-  kCobaltExtensionPleasantryName,
-  2,
-  kGreeting,
-  &GetFarewell,
-};
-
-}  // namespace
-
-const void* GetPleasantryApi() {
-  return &kPleasantryApi;
-}
-
-}  // namespace shared
-}  // namespace starboard
-```
-
-</details>
-
-<details>
-    <summary style="display:list-item">`git diff cobalt/browser/main.cc`
-    </summary>
-
-```
-@@ -18,7 +18,9 @@
- #include "cobalt/base/wrap_main.h"
- #include "cobalt/browser/application.h"
- #include "cobalt/browser/switches.h"
-+#include "cobalt/extension/pleasantry.h"
- #include "cobalt/version.h"
-+#include "starboard/system.h"
-
- namespace {
-
-@@ -54,6 +56,14 @@ bool CheckForAndExecuteStartupSwitches() {
-   return g_is_startup_switch_set;
- }
-
-+// Get the Pleasantry extension if it's implemented.
-+const CobaltExtensionPleasantryApi* GetPleasantryApi() {
-+  static const CobaltExtensionPleasantryApi* pleasantry_extension =
-+      static_cast<const CobaltExtensionPleasantryApi*>(
-+          SbSystemGetExtension(kCobaltExtensionPleasantryName));
-+  return pleasantry_extension;
-+}
-+
- void PreloadApplication(int argc, char** argv, const char* link,
-                         const base::Closure& quit_closure,
-                         SbTimeMonotonic timestamp) {
-@@ -77,6 +87,12 @@ void StartApplication(int argc, char** argv, const char* link,
-     return;
-   }
-   LOG(INFO) << "Starting application.";
-+  const CobaltExtensionPleasantryApi* pleasantry_extension = GetPleasantryApi();
-+  if (pleasantry_extension &&
-+      strcmp(pleasantry_extension->name, kCobaltExtensionPleasantryName) == 0 &&
-+      pleasantry_extension->version >= 1) {
-+    LOG(INFO) << pleasantry_extension->greeting;
-+  }
- #if SB_API_VERSION >= 13
-   DCHECK(!g_application);
-   g_application = new cobalt::browser::Application(quit_closure,
-@@ -96,7 +112,14 @@ void StartApplication(int argc, char** argv, const char* link,
- }
-
- void StopApplication() {
--  LOG(INFO) << "Stopping application.";
-+  const CobaltExtensionPleasantryApi* pleasantry_extension = GetPleasantryApi();
-+  if (pleasantry_extension &&
-+      strcmp(pleasantry_extension->name, kCobaltExtensionPleasantryName) == 0 &&
-+      pleasantry_extension->version >= 2) {
-+    LOG(INFO) << pleasantry_extension->GetFarewell();
-+  } else {
-+    LOG(INFO) << "Stopping application.";
-+  }
-   delete g_application;
-   g_application = NULL;
- }
-```
-
-</details>
-
-`starboard/linux/shared/pleasantry.h`,
-`starboard/linux/shared/starboard_platform.gypi`, and
-`starboard/linux/shared/system_get_extensions.cc` should be unchanged from the
-Exercise 1 solution.
-
-## Extension testing
-
-Each Cobalt Extension has a test in `cobalt/extension/extension_test.cc` that
-tests whether the extension is wired up correctly for the platform Cobalt
-happens to be built for.
-
-Since some platforms may not implement a particular extension, these tests begin
-by checking whether `SbSystemGetExtension` simply returns `NULL` for the
-extension's name. For our `foo` extension, the first few lines may contain the
-following.
-
-```
-TEST(ExtensionTest, Foo) {
-  typedef CobaltExtensionFooApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionFooName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  // Verifications about the global structure instance, if implemented.
-}
-```
-
-If `SbSystemGetExtension` does not return `NULL`, meaning the platform does
-implement the extension, the tests generally verify a few details about the
-structure:
-
-*   It has the expected name.
-*   Its version is in the range of possible versions for the extension.
-*   For whichever version is implemented, any members required for that version
-    are present.
-*   It's a singleton.
-
-### Exercise 3: Test your extension
-
-You guessed it! Add a test for your new extension to
-`cobalt/extension/extension_test.cc`.
-
-Once you've written your test you can execute it to confirm that it passes.
-`cobalt/extension/extension.gyp` configures an `extension_test` target to be
-built from our `extension_test.cc` source file. We can build that target for our
-platform and then run the executable to run the tests.
-
-```
-$ cobalt/build/gn.py -p linux-x64x11
-```
-
-```
-$ ninja -C out/linux-x64x11_devel all
-```
-
-```
-$ out/linux-x64x11_devel/extension_test
-```
-
-Tip: because the `extension_test` has type `<(gtest_target_type)`, we can use
-`--gtest_filter` to filter the tests that are run. For example, you can run just
-your newly added test with `--gtest_filter=ExtensionTest.Pleasantry`.
-
-#### Solution to Exercise 3
-
-<details>
-    <summary style="display:list-item">Click here to see a solution for the new
-    test.</summary>
-
-```
-TEST(ExtensionTest, Pleasantry) {
-  typedef CobaltExtensionPleasantryApi ExtensionApi;
-  const char* kExtensionName = kCobaltExtensionPleasantryName;
-
-  const ExtensionApi* extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  if (!extension_api) {
-    return;
-  }
-
-  EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_GE(extension_api->version, 1u);
-  EXPECT_LE(extension_api->version, 2u);
-
-  EXPECT_NE(extension_api->greeting, nullptr);
-
-  if (extension_api->version >= 2) {
-    EXPECT_NE(extension_api->GetFarewell, nullptr);
-  }
-
-  const ExtensionApi* second_extension_api =
-      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
-  EXPECT_EQ(second_extension_api, extension_api)
-      << "Extension struct should be a singleton";
-}
-```
-
-</details>
-
-You'll also want to include the header for the extension, i.e., `#include
-"cobalt/extension/pleasantry.h"`.
-
-## Contributing a Cobalt Extension
-
-Thanks for taking the time to complete the codelab!
-
-**If you'd like to contribute an actual Cobalt Extension to Cobalt in order to
-add some useful functionality for your platform, we encourage you to start a
-discussion with the Cobalt team before you begin coding.** To do so, please
-[file a feature request](https://issuetracker.google.com/issues/new?component=181120)
-for the extension and include the following information:
-
-*   The name of the Cobalt Extension.
-*   A description of the extension.
-*   Why a Cobalt Extension is the right tool, instead of some alternative.
-*   The fact that you'd like to contribute the extension (i.e., write the code)
-    rather than rely on the Cobalt team to prioritize, plan, and implement it.
-
-Please file this feature request with the appropriate priority and the Cobalt
-team will review the proposal accordingly. If the Cobalt team approves of the
-use case and design then a member of the team will assign the feature request
-back to you for implementation. At this point, please follow the
-<a href="/contributors/index.html">Contributing to Cobalt</a> guide to ensure
-your code is compliant and can be reviewed and submitted.
diff --git a/cobalt/site/docs/codelabs/starboard_extensions/codelab.md b/cobalt/site/docs/codelabs/starboard_extensions/codelab.md
new file mode 100644
index 0000000..7cac353
--- /dev/null
+++ b/cobalt/site/docs/codelabs/starboard_extensions/codelab.md
@@ -0,0 +1,793 @@
+---
+layout: doc
+title: "Starboard Extensions codelab"
+---
+
+The Starboard Extension framework provides a way to add optional, platform-specific
+features to the Starboard application. A Starboard Extension is an optional interface
+that porters can implement for their platforms if, and as, they wish.
+
+This tutorial uses coding exercises to guide you through the process of creating
+a simple example of a Starboard Extension. By the end you should have a firm
+understanding of what Starboard Extensions are, when to use them instead of
+alternatives, how to write them, and how to work with the Cobalt team to
+contribute them to the repository.
+
+## Prerequisites
+
+Because it's helpful to build and run Cobalt during the exercises, you'll first
+want to set up your environment and make sure you can build Cobalt. You can
+follow <a href="/development/setup-linux.html">Set up your environment -
+Linux</a> to do this if you're a Linux user. Please note that the exercise
+solutions assume you're using Linux but should be comparable to implementations
+for other platforms.
+
+Also note that while this codelab doesn't require it, you'll need to
+<a href="/starboard/porting.html">Port Cobalt to your platform</a> before you
+can actually use a Starboard Extension to customize it for your platform.
+
+Finally, the exercises assume the ability to program in C and C++.
+
+### Exercise 0: Run Cobalt and inspect logs
+
+Assuming you've already built Cobalt, please now run Cobalt and pay special
+attention to a message it logs when it starts up. This message will be the focus
+of subsequent exercises.
+
+```
+$ out/linux-x64x11_debug/cobalt 2>/dev/null | grep "Starting application"
+```
+
+## Background
+
+Situated below Cobalt is Starboard. Starboard, which is a porting layer and OS
+abstraction, contains a minimal set of APIs to encapsulate the platform-specific
+functionalities that Cobalt uses. Each Starboard module (memory, socket, thread,
+etc.) defines functions that must be implemented on a porter's platform,
+imposing an implementation and maintenance cost on all porters. With this cost
+in mind the Cobalt team tries to keep the Starboard APIs stable and only adds a
+new API **when some functionality is required by Cobalt but the implementation
+depends on the platform**.
+
+A Starboard API can be made optional, though, by the introduction of an
+accompanying API that asks platforms whether they support the underlying feature
+and enables Cobalt to check for the answer at runtime. For example,
+`SbWindowOnScreenKeyboardIsSupported` is used so that only platforms that
+support an on screen keyboard need implement the related functions in
+`starboard/window.h`. To spare porters uninterested in the functionality, the
+Cobalt team chooses to make a Starboard API optional **when some Cobalt
+functionality is optional and the implementation is platform-dependent**.
+
+Finally, a nonobvious point explains why even an optional Starboard API may
+sometimes be too cumbersome: other applications beyond Cobalt are able to be run
+on top of Starboard. If a feature is needed by Cobalt but not by all Starboard-
+based applications or by Starboard itself, adding a Starboard API for it may add
+unnecessary size and complexity to the porting layer. **And here we arrive at
+the sweet spot for Starboard Extensions: when the desired functionality is
+Cobalt-specific, optional in Cobalt, and has platform-dependent
+implementation.** Also, because Starboard Extensions are lightweight and, as you'll
+see below, added without any changes to the Starboard layer, they're the
+preferred way for porters to add new, custom features to Cobalt.
+
+To summarize:
+
+<table>
+  <tr>
+    <th colspan="1">Tool</th>
+    <th colspan="1">Use case</th>
+    <th colspan="1">Ecosystem cost</th>
+  </tr>
+  <tr>
+    <td>Starboard API</td>
+    <td>Feature is <strong>required</strong> but implementation is
+    platform-dependent</td>
+    <td>High</td>
+  </tr>
+  <tr>
+    <td>Optional Starboard API</td>
+    <td>Feature is <strong>optional</strong> and implementation is
+    platform-dependent</td>
+    <td>Medium</td>
+  </tr>
+  <tr>
+    <td>Starboard Extension</td>
+    <td>Feature is <strong>optional and specific to Cobalt</strong> and
+    implementation is platform-dependent</td>
+    <td>Low</td>
+  </tr>
+</table>
+
+As a caveat, please note that for all three of these abstractions the interface
+is in Cobalt's open-source repository and therefore visible to other porters.
+The implementation, on the other hand, is written and built by porters and so
+may be kept private.
+
+Finally, in addition to the alternatives mentioned, porters have in some cases
+made local changes to Cobalt, above the Starboard layer, to achieve some
+customization or optimization. This has been discouraged by the Cobalt team
+because it makes rebasing to future versions of Cobalt more difficult but has
+been possible because porters have historically built **both** Cobalt and
+Starboard. However, Cobalt is moving toward Evergreen
+(<a href="https://cobalt.googlesource.com/cobalt/+/refs/heads/master/starboard/doc/evergreen/cobalt_evergreen_overview.md">overview</a>),
+an architecture that enables automatic Cobalt updates on devices by separating a
+Google-built, Cobalt core shared library from the partner-built Starboard layer
+and Cobalt loader app. Because Cobalt core code is built by Google, custom
+changes to it are no longer possible for partners using Evergreen.
+
+## Anatomy of a Starboard Extension
+
+### Extension structures
+
+Cobalt uses a structure to describe the interface for an extension and organizes
+the structures in headers under `starboard/extension/`. The header for a "foo"
+extension should be named `foo.h` and the first version of it should contain the
+following content, as well as any additional members that provide the "foo"
+functionality.
+
+```
+#ifndef STARBOARD_EXTENSION_FOO_H_
+#define STARBOARD_EXTENSION_FOO_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kStarboardExtensionFooName "dev.starboard.extension.Foo"
+
+typedef struct StarboardExtensionFooApi {
+  // Name should be the string |kStarboardExtensionFooName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+} StarboardExtensionFooApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_FOO_H_
+```
+
+Please note a few important points about this structure:
+
+*   The first two members must be, in order:
+    *   A `const char* |name|`, storing the extension's name.
+    *   A `uint32_t |version|`, storing the version number of the extension.
+        Extension versioning is discussed later on in this codelab.
+*   The following members can be any C types (including custom structures) that
+    are useful. Often, these are function pointers.
+
+### Extension access in Cobalt
+
+The `SbSystemGetExtension` function from Starboard's `system` module allows
+Cobalt to query for an extension by name. It returns a pointer to the constant,
+global instance of the structure implementing the extension with the given name
+if it exists, otherwise `NULL`. This function is the only mechanism Cobalt has
+to get an extension; the Starboard interface intentionally doesn't have any
+functions related to the specific extensions.
+
+The caller in Cobalt must static cast the `const void*` returned by
+`SbSystemGetExtension` to a `const StarboardExtensionFooApi*`, or pointer of
+whatever type the extension structure type happens to be, before using it. Since
+the caller can't be sure whether a platform implements the extension or, if it
+does, implements it correctly, it's good defensive programming to check that the
+resulting pointer is not `NULL` and that the `name` member in the pointed-to
+structure has the same value as `kStarboardExtensionFooName`.
+
+### Extension implementation
+
+Because Starboard Extensions are platform-dependent, the implementations of an
+extension belong in Starboard ports. A Starboard port implements an extension by
+defining a constant, global instance of the structure and implementing the
+`SbSystemGetExtension` function to return a pointer to it. For our "foo"
+extension, an implementation for `custom_platform`'s Starboard port could look
+as follows.
+
+`starboard/custom_platform/foo.h` declares a `GetFooApi` accessor for the
+structure instance.
+
+```
+#ifndef STARBOARD_CUSTOM_PLATFORM_FOO_H_
+#define STARBOARD_CUSTOM_PLATFORM_FOO_H_
+
+namespace starboard {
+namespace custom_platform {
+
+const void* GetFooApi();
+
+}  // namespace custom_platform
+}  // namespace starboard
+
+#endif  // STARBOARD_CUSTOM_PLATFORM_FOO_H_
+```
+
+`starboard/custom_platform/foo.cc`, then, defines `GetFooApi`.
+
+```
+#include "starboard/custom_platform/foo.h"
+
+#include "starboard/extension/foo.h"
+
+namespace starboard {
+namespace custom_platform {
+
+namespace {
+
+// Definitions of any functions included as components in the extension
+// are added here.
+
+const StarboardExtensionFooApi kFooApi = {
+    kStarboardExtensionFooName,
+    1,  // API version that's implemented.
+    // Any additional members are initialized here.
+};
+
+}  // namespace
+
+const void* GetFooApi() {
+  return &kFooApi;
+}
+
+}  // namespace custom_platform
+}  // namespace starboard
+```
+
+Finally, `starboard/custom_platform/system_get_extension.cc` defines
+`SbSystemGetExtension` to wire up the extensions for the platform.
+
+```
+#include "starboard/system.h"
+
+#include "starboard/extension/foo.h"
+#include "starboard/common/string.h"
+#include "starboard/custom_platform/foo.h"
+
+const void* SbSystemGetExtension(const char* name) {
+  if (strcmp(name, kStarboardExtensionFooName) == 0) {
+    return starboard::custom_platform::GetFooApi();
+  }
+  // Other conditions here should handle other implemented extensions.
+
+  return NULL;
+}
+```
+
+Please feel free to browse existing extension implementations in the repository.
+For example, the reference Raspberry Pi port implements the `Graphics` extension
+across the following files.
+
+*   `starboard/raspi/shared/graphics.h`
+*   `starboard/raspi/shared/graphics.cc`
+*   `starboard/raspi/shared/system_get_extensions.cc`
+
+### Exercise 1: Write and use your first extension
+
+Now that you've seen the anatomy of a Starboard Extension it's your turn to write
+one of your own. In Exercise 0 we saw that Cobalt logs "Starting application"
+when it's started. Please write a `Pleasantry` Starboard Extension that has a
+member of type `const char*` and name `greeting` and make any necessary changes
+in `cobalt/browser/main.cc` so that the extension can be used to log a custom
+greeting directly after the plain "Starting application." Implement the
+extension for Linux, or whichever other platform you'd like, and confirm that
+the greeting is logged.
+
+#### Solution to Exercise 1
+
+Click the items below to expand parts of a solution. The `git diff`s are between
+the solution and the `master` branch.
+
+<details>
+    <summary style="display:list-item">Contents of new
+    `starboard/extension/pleasantry.h` file.</summary>
+
+```
+#ifndef STARBOARD_EXTENSION_PLEASANTRY_H_
+#define STARBOARD_EXTENSION_PLEASANTRY_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kStarboardExtensionPleasantryName "dev.starboard.extension.Pleasantry"
+
+typedef struct StarboardExtensionPleasantryApi {
+  // Name should be the string |kStarboardExtensionPleasantryName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+  const char* greeting;
+
+} StarboardExtensionPleasantryApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_PLEASANTRY_H_
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">Contents of new
+    `starboard/linux/shared/pleasantry.h` file.</summary>
+
+```
+#ifndef STARBOARD_LINUX_SHARED_PLEASANTRY_H_
+#define STARBOARD_LINUX_SHARED_PLEASANTRY_H_
+
+namespace starboard {
+namespace shared {
+
+const void* GetPleasantryApi();
+
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_LINUX_SHARED_PLEASANTRY_H_
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">Contents of new
+    `starboard/linux/shared/pleasantry.cc` file.</summary>
+
+```
+#include "starboard/linux/shared/pleasantry.h"
+
+#include "starboard/extension/pleasantry.h"
+
+namespace starboard {
+namespace shared {
+
+namespace {
+
+const char *kGreeting = "Happy debugging!";
+
+const StarboardExtensionPleasantryApi kPleasantryApi = {
+    kStarboardExtensionPleasantryName,
+    1,
+    kGreeting,
+};
+
+}  // namespace
+
+const void* GetPleasantryApi() {
+  return &kPleasantryApi;
+}
+
+}  // namespace shared
+}  // namespace starboard
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">`git diff
+    starboard/linux/shared/starboard_platform.gypi`</summary>
+
+```
+@@ -38,6 +38,8 @@
+       '<(DEPTH)/starboard/linux/shared/netlink.cc',
+       '<(DEPTH)/starboard/linux/shared/netlink.h',
+       '<(DEPTH)/starboard/linux/shared/player_components_factory.cc',
++      '<(DEPTH)/starboard/linux/shared/pleasantry.cc',
++      '<(DEPTH)/starboard/linux/shared/pleasantry.h',
+       '<(DEPTH)/starboard/linux/shared/routes.cc',
+       '<(DEPTH)/starboard/linux/shared/routes.h',
+       '<(DEPTH)/starboard/linux/shared/system_get_connection_type.cc',
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">`git diff
+    starboard/linux/shared/system_get_extensions.cc`</summary>
+
+```
+@@ -16,12 +16,14 @@
+
+ #include "starboard/extension/configuration.h"
+ #include "starboard/extension/crash_handler.h"
++#include "starboard/extension/pleasantry.h"
+ #include "starboard/common/string.h"
+ #include "starboard/shared/starboard/crash_handler.h"
+ #if SB_IS(EVERGREEN_COMPATIBLE)
+ #include "starboard/elf_loader/evergreen_config.h"
+ #endif
+ #include "starboard/linux/shared/configuration.h"
++#include "starboard/linux/shared/pleasantry.h"
+
+ const void* SbSystemGetExtension(const char* name) {
+ #if SB_IS(EVERGREEN_COMPATIBLE)
+@@ -41,5 +43,8 @@ const void* SbSystemGetExtension(const char* name) {
+   if (strcmp(name, kStarboardExtensionCrashHandlerName) == 0) {
+     return starboard::common::GetCrashHandlerApi();
+   }
++  if (strcmp(name, kStarboardExtensionPleasantryName) == 0) {
++    return starboard::shared::GetPleasantryApi();
++  }
+   return NULL;
+ }
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">`git diff cobalt/browser/main.cc`
+    </summary>
+
+```
+@@ -18,7 +18,9 @@
+ #include "cobalt/base/wrap_main.h"
+ #include "cobalt/browser/application.h"
+ #include "cobalt/browser/switches.h"
++#include "starboard/extension/pleasantry.h"
+ #include "cobalt/version.h"
++#include "starboard/system.h"
+
+ namespace {
+
+@@ -77,6 +79,14 @@ void StartApplication(int argc, char** argv, const char* link,
+     return;
+   }
+   LOG(INFO) << "Starting application.";
++  const StarboardExtensionPleasantryApi* pleasantry_extension =
++      static_cast<const StarboardExtensionPleasantryApi*>(
++          SbSystemGetExtension(kStarboardExtensionPleasantryName));
++  if (pleasantry_extension &&
++      strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
++      pleasantry_extension->version >= 1) {
++    LOG(INFO) << pleasantry_extension->greeting;
++  }
+ #if SB_API_VERSION >= 13
+   DCHECK(!g_application);
+   g_application = new cobalt::browser::Application(quit_closure,
+```
+
+</details>
+
+## Extension versioning
+
+Starboard Extensions are themselves extensible, but care must be taken to ensure
+that the extension interface in Cobalt and implementation in a platform's port,
+which may be built separately, are consistent.
+
+The `version` member, which is always the second member in an extension
+structure, indicates which version of the interface the structure describes. In
+other words, a `version` of the extension structure corresponds to a specific,
+invariant list of members. By convention, the first version of a Cobalt
+Extension is version `1` (i.e., one-based indexing, not zero-based).
+
+A new version of the extension can be introduced in the structure declaration by
+adding additional members to the end of the declaration and adding a comment to
+delineate, e.g., "The fields below this point were added in version 2 or later."
+To maintain compatibility and enable Cobalt to correctly index into instances of
+the structure, it's important that members are always added at the end of the
+structure declaration and that members are never removed. If a member is
+deprecated in a later version, this fact should simply be noted with a comment
+in the structure declaration.
+
+To implement a new version of the extension, a platform's port should then set
+the `version` member to the appropriate value when creating the instance of the
+structure, and also initialize all members required for the version.
+
+Finally, any code in Cobalt that uses the extension should guard references to
+members with version checks.
+
+### Exercise 2: Version your extension
+
+Add a second version of the `Pleasantry` extension that enables porters to also
+log a polite farewell message when the Cobalt application is stopped. To allow
+platforms more flexibility, add the new `farewell` member as a pointer to a
+function that takes no parameters and returns a `const char*`. Update
+`cobalt/browser/main.cc` so that Cobalt, if the platform implements version 2 of
+this extension, replaces the "Stopping application." message with a polite
+farewell provided by the platform.
+
+To keep things interesting, have the platform's implementation pseudo-randomly
+return one of several messages. And, once you've made the changes, build Cobalt
+and run it a few times to confirm that the feature behaves as expected.
+
+#### Solution to Exercise 2
+
+Click the items below to expand parts of a solution. The `git diff` is between
+the solution and the `master` branch.
+
+<details>
+    <summary style="display:list-item">Updated contents of
+    `starboard/extension/pleasantry.h`.</summary>
+
+```
+#ifndef STARBOARD_EXTENSION_PLEASANTRY_H_
+#define STARBOARD_EXTENSION_PLEASANTRY_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kStarboardExtensionPleasantryName "dev.starboard.extension.Pleasantry"
+
+typedef struct StarboardExtensionPleasantryApi {
+  // Name should be the string |kStarboardExtensionPleasantryName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+  const char* greeting;
+
+  // The fields below this point were added in version 2 or later.
+  const char* (*GetFarewell)();
+
+} StarboardExtensionPleasantryApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_PLEASANTRY_H_
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">Updated contents of
+    `starboard/linux/shared/pleasantry.cc`.</summary>
+
+```
+#include "starboard/linux/shared/pleasantry.h"
+
+#include <stdlib.h>
+
+#include "starboard/extension/pleasantry.h"
+#include "starboard/system.h"
+#include "starboard/time.h"
+
+namespace starboard {
+namespace shared {
+
+namespace {
+
+const char* kGreeting = "Happy debugging!";
+
+const char* kFarewells[] = {
+  "Farewell",
+  "Take care",
+  "Thanks for running Cobalt",
+};
+
+const char* GetFarewell() {
+  srand (SbTimeGetNow());
+  int pseudo_random_index = rand() % SB_ARRAY_SIZE_INT(kFarewells);
+  return kFarewells[pseudo_random_index];
+}
+
+const StarboardExtensionPleasantryApi kPleasantryApi = {
+  kStarboardExtensionPleasantryName,
+  2,
+  kGreeting,
+  &GetFarewell,
+};
+
+}  // namespace
+
+const void* GetPleasantryApi() {
+  return &kPleasantryApi;
+}
+
+}  // namespace shared
+}  // namespace starboard
+```
+
+</details>
+
+<details>
+    <summary style="display:list-item">`git diff cobalt/browser/main.cc`
+    </summary>
+
+```
+@@ -18,7 +18,9 @@
+ #include "cobalt/base/wrap_main.h"
+ #include "cobalt/browser/application.h"
+ #include "cobalt/browser/switches.h"
++#include "starboard/extension/pleasantry.h"
+ #include "cobalt/version.h"
++#include "starboard/system.h"
+
+ namespace {
+
+@@ -54,6 +56,14 @@ bool CheckForAndExecuteStartupSwitches() {
+   return g_is_startup_switch_set;
+ }
+
++// Get the Pleasantry extension if it's implemented.
++const StarboardExtensionPleasantryApi* GetPleasantryApi() {
++  static const StarboardExtensionPleasantryApi* pleasantry_extension =
++      static_cast<const StarboardExtensionPleasantryApi*>(
++          SbSystemGetExtension(kStarboardExtensionPleasantryName));
++  return pleasantry_extension;
++}
++
+ void PreloadApplication(int argc, char** argv, const char* link,
+                         const base::Closure& quit_closure,
+                         SbTimeMonotonic timestamp) {
+@@ -77,6 +87,12 @@ void StartApplication(int argc, char** argv, const char* link,
+     return;
+   }
+   LOG(INFO) << "Starting application.";
++  const StarboardExtensionPleasantryApi* pleasantry_extension = GetPleasantryApi();
++  if (pleasantry_extension &&
++      strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
++      pleasantry_extension->version >= 1) {
++    LOG(INFO) << pleasantry_extension->greeting;
++  }
+ #if SB_API_VERSION >= 13
+   DCHECK(!g_application);
+   g_application = new cobalt::browser::Application(quit_closure,
+@@ -96,7 +112,14 @@ void StartApplication(int argc, char** argv, const char* link,
+ }
+
+ void StopApplication() {
+-  LOG(INFO) << "Stopping application.";
++  const StarboardExtensionPleasantryApi* pleasantry_extension = GetPleasantryApi();
++  if (pleasantry_extension &&
++      strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
++      pleasantry_extension->version >= 2) {
++    LOG(INFO) << pleasantry_extension->GetFarewell();
++  } else {
++    LOG(INFO) << "Stopping application.";
++  }
+   delete g_application;
+   g_application = NULL;
+ }
+```
+
+</details>
+
+`starboard/linux/shared/pleasantry.h`,
+`starboard/linux/shared/starboard_platform.gypi`, and
+`starboard/linux/shared/system_get_extensions.cc` should be unchanged from the
+Exercise 1 solution.
+
+## Extension testing
+
+Each Starboard Extension has a test in `starboard/extension/extension_test.cc` that
+tests whether the extension is wired up correctly for the platform Cobalt
+happens to be built for.
+
+Since some platforms may not implement a particular extension, these tests begin
+by checking whether `SbSystemGetExtension` simply returns `NULL` for the
+extension's name. For our `foo` extension, the first few lines may contain the
+following.
+
+```
+TEST(ExtensionTest, Foo) {
+  typedef StarboardExtensionFooApi ExtensionApi;
+  const char* kExtensionName = kStarboardExtensionFooName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  // Verifications about the global structure instance, if implemented.
+}
+```
+
+If `SbSystemGetExtension` does not return `NULL`, meaning the platform does
+implement the extension, the tests generally verify a few details about the
+structure:
+
+*   It has the expected name.
+*   Its version is in the range of possible versions for the extension.
+*   For whichever version is implemented, any members required for that version
+    are present.
+*   It's a singleton.
+
+### Exercise 3: Test your extension
+
+You guessed it! Add a test for your new extension to
+`starboard/extension/extension_test.cc`.
+
+Once you've written your test you can execute it to confirm that it passes.
+`starboard/extension/extension.gyp` configures an `extension_test` target to be
+built from our `extension_test.cc` source file. We can build that target for our
+platform and then run the executable to run the tests.
+
+```
+$ cobalt/build/gn.py -p linux-x64x11
+```
+
+```
+$ ninja -C out/linux-x64x11_devel all
+```
+
+```
+$ out/linux-x64x11_devel/extension_test
+```
+
+Tip: because the `extension_test` has type `<(gtest_target_type)`, we can use
+`--gtest_filter` to filter the tests that are run. For example, you can run just
+your newly added test with `--gtest_filter=ExtensionTest.Pleasantry`.
+
+#### Solution to Exercise 3
+
+<details>
+    <summary style="display:list-item">Click here to see a solution for the new
+    test.</summary>
+
+```
+TEST(ExtensionTest, Pleasantry) {
+  typedef StarboardExtensionPleasantryApi ExtensionApi;
+  const char* kExtensionName = kStarboardExtensionPleasantryName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 2u);
+
+  EXPECT_NE(extension_api->greeting, nullptr);
+
+  if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->GetFarewell, nullptr);
+  }
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+```
+
+</details>
+
+You'll also want to include the header for the extension, i.e., `#include
+"starboard/extension/pleasantry.h"`.
+
+## Contributing a Starboard Extension
+
+Thanks for taking the time to complete the codelab!
+
+**If you'd like to contribute an actual Starboard Extension to Cobalt in order to
+add some useful functionality for your platform, we encourage you to start a
+discussion with the Cobalt team before you begin coding.** To do so, please
+[file a feature request](https://issuetracker.google.com/issues/new?component=181120)
+for the extension and include the following information:
+
+*   The name of the Starboard Extension.
+*   A description of the extension.
+*   Why a Starboard Extension is the right tool, instead of some alternative.
+*   The fact that you'd like to contribute the extension (i.e., write the code)
+    rather than rely on the Cobalt team to prioritize, plan, and implement it.
+
+Please file this feature request with the appropriate priority and the Cobalt
+team will review the proposal accordingly. If the Cobalt team approves of the
+use case and design then a member of the team will assign the feature request
+back to you for implementation. At this point, please follow the
+<a href="/contributors/index.html">Contributing to Cobalt</a> guide to ensure
+your code is compliant and can be reviewed and submitted.
diff --git a/cobalt/site/docs/development/setup-android.md b/cobalt/site/docs/development/setup-android.md
index 0382b42..2a5f0b1 100644
--- a/cobalt/site/docs/development/setup-android.md
+++ b/cobalt/site/docs/development/setup-android.md
@@ -209,7 +209,7 @@
 
 The test target itself (e.g. nplb) just builds an .so file (e.g. libnplb.so). To
 run that on a device, it needs to be packaged into an APK, which is done by the
-associated "deploy" target (e.g. nplb_deploy). The Starboard test runner does
+associated "install" target (e.g. nplb_install). The Starboard test runner does
 all this for you, so just use that to build and run tests. For example, to
 build and run "devel" NPLB on an ARM64 device, from the top-level directory:
 
diff --git a/cobalt/site/docs/gen/cobalt/doc/lifecycle.md b/cobalt/site/docs/gen/cobalt/doc/lifecycle.md
index 6b22ee0..d945ff1 100644
--- a/cobalt/site/docs/gen/cobalt/doc/lifecycle.md
+++ b/cobalt/site/docs/gen/cobalt/doc/lifecycle.md
@@ -88,7 +88,7 @@
 
 ### Deprecated `SbSystemRequest` functions.
 
-The `SbSytemRequest` functions are declared in `src/starboard/system.h`
+The `SbSystemRequest` functions are declared in `src/starboard/system.h`
 
 * The `SbSystemRequestPause` event is renamed to `SbSystemRequestBlur`
 * The `SbSystemRequestUnpause` event is renamed to `SbSystemRequestFocus`
diff --git a/cobalt/site/docs/gen/cobalt/doc/platform_services.md b/cobalt/site/docs/gen/cobalt/doc/platform_services.md
index 309b578..2228aa7 100644
--- a/cobalt/site/docs/gen/cobalt/doc/platform_services.md
+++ b/cobalt/site/docs/gen/cobalt/doc/platform_services.md
@@ -69,7 +69,7 @@
 Implementing the Starboard layer of Platform Service extension support uses the
 following interface in parallel with the IDL interface:
 
-*   [src/cobalt/extension/platform\_service.h](../extension/platform_service.h)
+*   [starboard/extension/platform\_service.h](../extension/platform_service.h)
 
 `CobaltExtensionPlatformServiceApi` is the main interface for the Starboard
 layer.
diff --git a/cobalt/site/docs/gen/starboard/doc/crash_handlers.md b/cobalt/site/docs/gen/starboard/doc/crash_handlers.md
index b7bd6e2..36ba542 100644
--- a/cobalt/site/docs/gen/starboard/doc/crash_handlers.md
+++ b/cobalt/site/docs/gen/starboard/doc/crash_handlers.md
@@ -20,7 +20,7 @@
 ```
 #include "starboard/system.h"
 
-#include "cobalt/extension/crash_handler.h"
+#include "starboard/extension/crash_handler.h"
 #include "starboard/shared/starboard/crash_handler.h"
 
 ...
diff --git a/cobalt/site/docs/reference/starboard/gn-configuration.md b/cobalt/site/docs/reference/starboard/gn-configuration.md
index 4c72557..2fff0ac 100644
--- a/cobalt/site/docs/reference/starboard/gn-configuration.md
+++ b/cobalt/site/docs/reference/starboard/gn-configuration.md
@@ -13,8 +13,6 @@
 | **`default_renderer_options_dependency`**<br><br> Override this value to adjust the default rasterizer setting for your platform.<br><br>The default value is `"//cobalt/renderer:default_options"`. |
 | **`enable_account_manager`**<br><br> Set to true to enable H5vccAccountManager.<br><br>The default value is `false`. |
 | **`enable_in_app_dial`**<br><br> Enables or disables the DIAL server that runs inside Cobalt. Note: Only enable if there's no system-wide DIAL support.<br><br>The default value is `false`. |
-| **`enable_sso`**<br><br> Set to true to enable H5vccSSO (Single Sign On).<br><br>The default value is `false`. |
-| **`enable_xhr_header_filtering`**<br><br> Set to true to enable filtering of HTTP headers before sending.<br><br>The default value is `false`. |
 | **`executable_configs`**<br><br> Target-specific configurations for executable targets.<br><br>The default value is `[]`. |
 | **`final_executable_type`**<br><br> The target type for executable targets. Allows changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
 | **`gl_type`**<br><br> The source of EGL and GLES headers and libraries. Valid values (case and everything sensitive!):<ul><li><code>none</code> - No EGL + GLES implementation is available on this platform.<li><code>system_gles2</code> - Use the system implementation of EGL + GLES2. The headers and libraries must be on the system include and link paths.<li><code>glimp</code> - Cobalt's own EGL + GLES2 implementation. This requires a valid Glimp implementation for the platform.<li><code>angle</code> - A DirectX-to-OpenGL adaptation layer. This requires a valid ANGLE implementation for the platform.<br><br>The default value is `"system_gles2"`. |
@@ -31,6 +29,7 @@
 | **`sb_enable_lib`**<br><br> Enables embedding Cobalt as a shared library within another app. This requires a 'lib' starboard implementation for the corresponding platform.<br><br>The default value is `false`. |
 | **`sb_enable_opus_sse`**<br><br> Enables optimizations on SSE compatible platforms.<br><br>The default value is `true`. |
 | **`sb_evergreen_compatible_enable_lite`**<br><br> Whether to adopt Evergreen Lite on the Evergreen compatible platform.<br><br>The default value is `false`. |
+| **`sb_evergreen_compatible_package`**<br><br> Whether to generate the whole package containing both Loader app and Cobalt core on the Evergreen compatible platform.<br><br>The default value is `false`. |
 | **`sb_evergreen_compatible_use_libunwind`**<br><br> Whether to use the libunwind library on Evergreen compatible platform.<br><br>The default value is `false`. |
 | **`sb_filter_based_player`**<br><br> Used to indicate that the player is filter based.<br><br>The default value is `true`. |
 | **`sb_is_evergreen`**<br><br> Whether this is an Evergreen build.<br><br>The default value is `false`. |
diff --git a/cobalt/speech/sandbox/speech_sandbox.cc b/cobalt/speech/sandbox/speech_sandbox.cc
index 78e3353..56f46bd 100644
--- a/cobalt/speech/sandbox/speech_sandbox.cc
+++ b/cobalt/speech/sandbox/speech_sandbox.cc
@@ -77,7 +77,7 @@
     const dom::DOMSettings::Options& dom_settings_options) {
   std::unique_ptr<script::EnvironmentSettings> environment_settings(
       new dom::DOMSettings(null_debugger_hooks_, kDOMMaxElementDepth, NULL,
-                           NULL, NULL, NULL, NULL, dom_settings_options));
+                           NULL, NULL, NULL, dom_settings_options));
   DCHECK(environment_settings);
 
   speech_recognition_ = new SpeechRecognition(environment_settings.get());
diff --git a/cobalt/sso/BUILD.gn b/cobalt/sso/BUILD.gn
deleted file mode 100644
index b4efae6..0000000
--- a/cobalt/sso/BUILD.gn
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright 2021 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source_set("sso") {
-  has_pedantic_warnings = true
-
-  sources = [ "sso_interface.h" ]
-
-  deps = [ "//cobalt/base" ]
-}
diff --git a/cobalt/sso/sso_interface.h b/cobalt/sso/sso_interface.h
deleted file mode 100644
index 20f2b9c..0000000
--- a/cobalt/sso/sso_interface.h
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_SSO_SSO_INTERFACE_H_
-#define COBALT_SSO_SSO_INTERFACE_H_
-
-#include <memory>
-#include <string>
-
-
-namespace cobalt {
-namespace sso {
-
-// Porters should inherit from this class, and implement the pure
-// virtual functions.
-class SsoInterface {
- public:
-  virtual ~SsoInterface() {}
-  virtual std::string getApiKey() const = 0;
-  virtual std::string getOauthClientId() const = 0;
-  virtual std::string getOauthClientSecret() const = 0;
-};
-
-// Porters should implement this function in the |starboard|
-// directory, and link the definition inside cobalt.
-std::unique_ptr<SsoInterface> CreateSSO();
-
-}  // namespace sso
-}  // namespace cobalt
-
-#endif  // COBALT_SSO_SSO_INTERFACE_H_
diff --git a/cobalt/test/run_all_unittests.cc b/cobalt/test/run_all_unittests.cc
index 63cde2f..1395d0c 100644
--- a/cobalt/test/run_all_unittests.cc
+++ b/cobalt/test/run_all_unittests.cc
@@ -29,6 +29,11 @@
   base::PathService::RegisterProvider(&cobalt::PathProvider,
                                       cobalt::paths::PATH_COBALT_START,
                                       cobalt::paths::PATH_COBALT_END);
+
+  // Copy the Starboard thread name to the PlatformThread name.
+  char thread_name[128] = {'\0'};
+  SbThreadGetName(thread_name, 127);
+  base::PlatformThread::SetName(thread_name);
   return test_suite.Run();
 }
 }  // namespace
diff --git a/cobalt/tools/automated_testing/cobalt_runner.py b/cobalt/tools/automated_testing/cobalt_runner.py
index d35afbe..3366638 100644
--- a/cobalt/tools/automated_testing/cobalt_runner.py
+++ b/cobalt/tools/automated_testing/cobalt_runner.py
@@ -38,11 +38,10 @@
 RE_WEBDRIVER_FAILED = re.compile(r'Could not start WebDriver server')
 # Pattern to match Cobalt log line for when a WindowDriver has been created.
 RE_WINDOWDRIVER_CREATED = re.compile(
-    (r'^\[[\d:]+/[\d.]+:INFO:browser_module\.cc\(\d+\)\] Created WindowDriver: '
-     r'ID=\S+'))
+    (r':browser_module\.cc\(\d+\)\] Created WindowDriver: ID=\S+'))
 # Pattern to match Cobalt log line for when a WebModule is has been loaded.
 RE_WEBMODULE_LOADED = re.compile(
-    r'^\[[\d:]+/[\d.]+:INFO:browser_module\.cc\(\d+\)\] Loaded WebModule')
+    r':browser_module\.cc\(\d+\)\] Loaded WebModule')
 
 # selenium imports
 # pylint: disable=C0103
@@ -56,6 +55,7 @@
 WEBDRIVER_HTTP_TIMEOUT_SECONDS = 2 * 60
 COBALT_EXIT_TIMEOUT_SECONDS = 5
 PAGE_LOAD_WAIT_SECONDS = 30
+POLL_UNTIL_WAIT_SECONDS = 30
 WINDOWDRIVER_CREATED_TIMEOUT_SECONDS = 45
 WEBMODULE_LOADED_TIMEOUT_SECONDS = 45
 FIND_ELEMENT_RETRY_LIMIT = 20
@@ -112,6 +112,9 @@
         exit.
     """
 
+    # Tracks if test execution started successfully
+    self.start_condition = threading.Condition()
+    # Tracks if Webdriver found the right script and ran it
     self.test_script_started = threading.Event()
     self.launcher = None
     self.webdriver = None
@@ -128,7 +131,7 @@
     self.log_handler = log_handler
 
     if log_file:
-      self.log_file = open(log_file)  # pylint: disable=consider-using-with
+      self.log_file = open(log_file, encoding='utf-8')  # pylint: disable=consider-using-with
       logging.basicConfig(stream=self.log_file, level=logging.INFO)
     else:
       self.log_file = sys.stdout
@@ -274,12 +277,12 @@
     self.launcher_is_running = True
     try:
       self.WaitForStart()
-    except KeyboardInterrupt:
+    except KeyboardInterrupt as e:
       # potentially from _thread.interrupt_main(). We will treat as
       # a timeout regardless.
 
       self.Exit(should_fail=True)
-      raise TimeoutException
+      raise TimeoutException from e
 
   def __exit__(self, exc_type, exc_value, exc_traceback):
     # The unittest module terminates with a SystemExit
@@ -331,12 +334,26 @@
 
   def _StartWebdriver(self, port):
     host, webdriver_port = self.launcher.GetHostAndPortGivenPort(port)
-    url = 'http://{}:{}/'.format(host, webdriver_port)
+    self.webdriver_url = f'http://{host}:{webdriver_port}/'
     self.webdriver = self.selenium_webdriver_module.Remote(
-        url, COBALT_WEBDRIVER_CAPABILITIES)
+        self.webdriver_url, COBALT_WEBDRIVER_CAPABILITIES)
     self.webdriver.command_executor.set_timeout(WEBDRIVER_HTTP_TIMEOUT_SECONDS)
     logging.info('Selenium Connected')
     self.test_script_started.set()
+    with self.start_condition:
+      self.start_condition.notify()
+
+  def ReconnectWebDriver(self):
+    logging.warning('ReconnectWebDriver\n\n\n\n')
+    if self.webdriver:
+      self.webdriver.quit()
+    if self.webdriver_url:
+      self.webdriver = self.selenium_webdriver_module.Remote(
+          self.webdriver_url, COBALT_WEBDRIVER_CAPABILITIES)
+    if self.webdriver:
+      self.webdriver.command_executor.set_timeout(
+          WEBDRIVER_HTTP_TIMEOUT_SECONDS)
+      logging.info('Selenium Reconnected')
 
   def WaitForStart(self):
     """Waits for the webdriver client to attach to Cobalt."""
@@ -344,7 +361,12 @@
     if not startup_timeout_seconds:
       startup_timeout_seconds = DEFAULT_STARTUP_TIMEOUT_SECONDS
 
-    if not self.test_script_started.wait(startup_timeout_seconds):
+    with self.start_condition:
+      if not self.start_condition.wait(startup_timeout_seconds):
+        self.Exit(should_fail=True)
+        raise TimeoutException
+
+    if not self.test_script_started.is_set():
       self.Exit(should_fail=True)
       raise TimeoutException
     logging.info('Cobalt started')
@@ -352,16 +374,22 @@
   def _RunLauncher(self):
     """Thread run routine."""
     try:
+      # Force a newline because unittest with verbosity=2 doesn't start on a new
+      # line.
+      sys.stderr.write('\n')
       logging.info('Running launcher')
       self.launcher.Run()
       logging.info('Cobalt terminated.')
       if not self.failed and self.success_message:
-        print('{}\n'.format(self.success_message))
+        print(f'{self.success_message}\n')
         logging.info('%s\n', self.success_message)
     # pylint: disable=broad-except
     except Exception as ex:
       sys.stderr.write('Exception running Cobalt ' + str(ex))
     finally:
+      # unblock main thread if it's still waiting to start, but Cobalt quit
+      with self.start_condition:
+        self.start_condition.notify()
       self.launcher_write_pipe.close()
       if not self.should_exit.is_set():
         # If the main thread is not expecting us to exit,
@@ -377,8 +405,7 @@
     while True:
       if retry_count >= EXECUTE_JAVASCRIPT_RETRY_LIMIT:
         raise TimeoutException(
-            'Selenium element or window not found in {} tries'.format(
-                EXECUTE_JAVASCRIPT_RETRY_LIMIT))
+            f'Selenium element or window not found in {retry_count} tries')
       retry_count += 1
       try:
         result = self.webdriver.execute_script(js_code)
@@ -398,7 +425,7 @@
     Returns:
       Python object represented by the cval string
     """
-    javascript_code = 'return h5vcc.cVal.getValue(\'{}\')'.format(cval_name)
+    javascript_code = f'return h5vcc.cVal.getValue(\'{cval_name}\')'
     cval_string = self.ExecuteJavaScript(javascript_code)
     if cval_string:
       try:
@@ -423,7 +450,7 @@
       Python dictionary of values indexed by the cval names provided.
     """
     javascript_code_list = [
-        'h5vcc.cVal.getValue(\'{}\')'.format(name) for name in cval_name_list
+        f'h5vcc.cVal.getValue(\'{name}\')' for name in cval_name_list
     ]
     javascript_code = 'return [' + ','.join(javascript_code_list) + ']'
     json_results = self.ExecuteJavaScript(javascript_code)
@@ -442,15 +469,14 @@
 
     Args:
       css_selector: A CSS selector
-      expected_num: The expected number of the selector type to be found.
 
     Raises:
       Underlying WebDriver exceptions
     """
     start_time = time.time()
     while (not self.FindElements(css_selector) and
-           (time.time() - start_time < PAGE_LOAD_WAIT_SECONDS)):
-      time.sleep(1)
+           (time.time() - start_time < POLL_UNTIL_WAIT_SECONDS)):
+      time.sleep(0.5)
     if expected_num:
       self.FindElements(css_selector, expected_num)
 
@@ -480,7 +506,7 @@
     # probably does.
     if not self.UniqueFind(css_selector):
       raise CobaltRunner.AssertException(
-          'Did not find selector: {}'.format(css_selector))
+          f'Did not find selector: {css_selector}')
 
   def FindElements(self, css_selector, expected_num=None):
     """Finds elements based on a selector.
@@ -502,8 +528,7 @@
     while True:
       if retry_count >= FIND_ELEMENT_RETRY_LIMIT:
         raise TimeoutException(
-            'Selenium element or window not found in {} tries'.format(
-                retry_count))
+            f'Selenium element or window not found in {retry_count} tries')
       retry_count += 1
       try:
         elements = self.webdriver.find_elements_by_css_selector(css_selector)
@@ -514,8 +539,8 @@
       break
     if expected_num and len(elements) != expected_num:
       raise CobaltRunner.AssertException(
-          'Expected number of element {} is: {}, got {}'.format(
-              css_selector, expected_num, len(elements)))
+          f'Expected number of element {css_selector} '
+          f'is: {expected_num}, got {len(elements)}')
     return elements
 
   def WaitForActiveElement(self):
@@ -524,7 +549,7 @@
     while True:
       if retry_count >= FIND_ELEMENT_RETRY_LIMIT:
         raise TimeoutException(
-            'Selenium active element not found in {} tries'.format(retry_count))
+            f'Selenium active element not found in {retry_count} tries')
       retry_count += 1
       try:
         element = self.webdriver.switch_to.active_element
diff --git a/cobalt/ui_navigation/scroll_engine/BUILD.gn b/cobalt/ui_navigation/scroll_engine/BUILD.gn
new file mode 100644
index 0000000..485dbea
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/BUILD.gn
@@ -0,0 +1,28 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+static_library("scroll_engine") {
+  has_pedantic_warnings = true
+  sources = [
+    "scroll_engine.cc",
+    "scroll_engine.h",
+  ]
+  deps = [
+    "//cobalt/base",
+    "//cobalt/cssom",
+    "//cobalt/dom",
+    "//cobalt/math",
+    "//cobalt/ui_navigation",
+  ]
+}
diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.cc b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc
new file mode 100644
index 0000000..bd5495c
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc
@@ -0,0 +1,308 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
+
+#include <algorithm>
+
+#include "cobalt/dom/pointer_event.h"
+
+namespace cobalt {
+namespace ui_navigation {
+namespace scroll_engine {
+
+namespace {
+
+const base::TimeDelta kFreeScrollDuration =
+    base::TimeDelta::FromMilliseconds(700);
+
+void BoundValuesByNavItemBounds(scoped_refptr<ui_navigation::NavItem> nav_item,
+                                float* x, float* y) {
+  float scroll_top_lower_bound;
+  float scroll_left_lower_bound;
+  float scroll_top_upper_bound;
+  float scroll_left_upper_bound;
+  nav_item->GetBounds(&scroll_top_lower_bound, &scroll_left_lower_bound,
+                      &scroll_top_upper_bound, &scroll_left_upper_bound);
+
+  *x = std::min(scroll_left_upper_bound, *x);
+  *x = std::max(scroll_left_lower_bound, *x);
+  *y = std::min(scroll_top_upper_bound, *y);
+  *y = std::max(scroll_top_lower_bound, *y);
+}
+
+math::Vector2dF BoundValuesByNavItemBounds(
+    scoped_refptr<ui_navigation::NavItem> nav_item, math::Vector2dF vector) {
+  float x = vector.x();
+  float y = vector.y();
+  BoundValuesByNavItemBounds(nav_item, &x, &y);
+  vector.set_x(x);
+  vector.set_y(y);
+  return vector;
+}
+
+bool ShouldFreeScroll(scoped_refptr<ui_navigation::NavItem> scroll_container,
+                      math::Vector2dF drag_vector) {
+  float x = drag_vector.x();
+  float y = drag_vector.y();
+  bool scrolling_right = x > 0;
+  bool scrolling_down = y > 0;
+  bool scrolling_left = !scrolling_right;
+  bool scrolling_up = !scrolling_down;
+
+  float scroll_top_lower_bound, scroll_left_lower_bound, scroll_top_upper_bound,
+      scroll_left_upper_bound;
+  float offset_x, offset_y;
+  scroll_container->GetBounds(&scroll_top_lower_bound, &scroll_left_lower_bound,
+                              &scroll_top_upper_bound,
+                              &scroll_left_upper_bound);
+  scroll_container->GetContentOffset(&offset_x, &offset_y);
+
+  bool can_scroll_left = scroll_left_lower_bound < offset_x;
+  bool can_scroll_right = scroll_left_upper_bound > offset_x;
+  bool can_scroll_up = scroll_top_lower_bound < offset_y;
+  bool can_scroll_down = scroll_top_upper_bound > offset_y;
+  return (
+      ((can_scroll_left && scrolling_left) ||
+       (can_scroll_right && scrolling_right)) &&
+      ((can_scroll_down && scrolling_down) || (can_scroll_up && scrolling_up)));
+}
+
+void ScrollNavItemWithVector(scoped_refptr<NavItem> nav_item,
+                             math::Vector2dF vector) {
+  float offset_x;
+  float offset_y;
+  nav_item->GetContentOffset(&offset_x, &offset_y);
+  offset_x += vector.x();
+  offset_y += vector.y();
+  BoundValuesByNavItemBounds(nav_item, &offset_x, &offset_y);
+
+  nav_item->SetContentOffset(offset_x, offset_y);
+}
+
+}  // namespace
+
+ScrollEngine::ScrollEngine()
+    : timing_function_(cssom::TimingFunction::GetEaseInOut()) {}
+ScrollEngine::~ScrollEngine() { free_scroll_timer_.Stop(); }
+
+void ScrollEngine::MaybeFreeScrollActiveNavItem() {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  DCHECK(previous_events_.size() == 2);
+  if (previous_events_.size() != 2) {
+    return;
+  }
+
+  auto previous_event = previous_events_.back();
+  auto current_event = previous_events_.front();
+  math::Vector2dF distance_delta =
+      previous_event.position - current_event.position;
+  // TODO(andrewsavage): See if we need this
+  // if (distance_delta.Length() < kFreeScrollThreshold) {
+  //   return;
+  // }
+
+  // Get the average velocity for the entire run
+  math::Vector2dF average_velocity = distance_delta;
+  average_velocity.Scale(1.0f / static_cast<float>(current_event.time_stamp -
+                                                   previous_event.time_stamp));
+  average_velocity.Scale(0.5);
+
+  // Get the distance
+  average_velocity.Scale(kFreeScrollDuration.ToSbTime());
+
+  float initial_offset_x;
+  float initial_offset_y;
+  active_item_->GetContentOffset(&initial_offset_x, &initial_offset_y);
+  math::Vector2dF initial_offset(initial_offset_x, initial_offset_y);
+
+  math::Vector2dF target_offset = initial_offset + average_velocity;
+  target_offset = BoundValuesByNavItemBounds(active_item_, target_offset);
+
+  nav_items_with_decaying_scroll_.push_back(
+      FreeScrollingNavItem(active_item_, initial_offset, target_offset));
+  if (!free_scroll_timer_.IsRunning()) {
+    free_scroll_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(5),
+                             this,
+                             &ScrollEngine::ScrollNavItemsWithDecayingScroll);
+  }
+
+  while (!previous_events_.empty()) {
+    previous_events_.pop();
+  }
+}
+
+void ScrollEngine::HandlePointerEventForActiveItem(
+    scoped_refptr<dom::PointerEvent> pointer_event) {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  if (pointer_event->type() == base::Tokens::pointerup()) {
+    MaybeFreeScrollActiveNavItem();
+    active_item_ = nullptr;
+    active_velocity_ = math::Vector2dF(0.0f, 0.0f);
+    return;
+  }
+
+  if (pointer_event->type() != base::Tokens::pointermove()) {
+    return;
+  }
+
+  auto current_coordinates =
+      math::Vector2dF(pointer_event->x(), pointer_event->y());
+  if (previous_events_.size() != 2) {
+    // This is an error.
+    previous_events_.push(EventPositionWithTimeStamp(
+        current_coordinates, pointer_event->time_stamp()));
+    return;
+  }
+
+  auto drag_vector = previous_events_.front().position - current_coordinates;
+  if (active_scroll_type_ == ScrollType::Horizontal) {
+    drag_vector.set_y(0.0f);
+  } else if (active_scroll_type_ == ScrollType::Vertical) {
+    drag_vector.set_x(0.0f);
+  }
+
+  previous_events_.push(EventPositionWithTimeStamp(
+      current_coordinates, pointer_event->time_stamp()));
+  previous_events_.pop();
+
+  active_velocity_ = drag_vector;
+
+  ScrollNavItemWithVector(active_item_, drag_vector);
+}
+
+void ScrollEngine::HandlePointerEvent(base::Token type,
+                                      const dom::PointerEventInit& event) {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  scoped_refptr<dom::PointerEvent> pointer_event(
+      new dom::PointerEvent(type, nullptr, event));
+  uint32_t pointer_id = pointer_event->pointer_id();
+  if (pointer_event->type() == base::Tokens::pointerdown()) {
+    events_to_handle_[pointer_id] = pointer_event;
+    return;
+  }
+  if (active_item_) {
+    HandlePointerEventForActiveItem(pointer_event);
+    return;
+  }
+
+  auto last_event_to_handle = events_to_handle_.find(pointer_id);
+  if (last_event_to_handle == events_to_handle_.end()) {
+    // Pointer events have not come in the appropriate order.
+    return;
+  }
+
+  if (pointer_event->type() == base::Tokens::pointermove()) {
+    if (last_event_to_handle->second->type() == base::Tokens::pointermove() ||
+        (math::Vector2dF(last_event_to_handle->second->x(),
+                         last_event_to_handle->second->y()) -
+         math::Vector2dF(pointer_event->x(), pointer_event->y()))
+                .Length() > kDragDistanceThreshold) {
+      events_to_handle_[pointer_id] = pointer_event;
+    }
+  } else if (pointer_event->type() == base::Tokens::pointerup()) {
+    if (last_event_to_handle->second->type() == base::Tokens::pointermove()) {
+      events_to_handle_[pointer_id] = pointer_event;
+    } else {
+      events_to_handle_.erase(pointer_id);
+    }
+  }
+}
+
+void ScrollEngine::HandleScrollStart(
+    scoped_refptr<ui_navigation::NavItem> scroll_container,
+    ScrollType scroll_type, int32_t pointer_id,
+    math::Vector2dF initial_coordinates, uint64 initial_time_stamp,
+    math::Vector2dF current_coordinates, uint64 current_time_stamp) {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  auto drag_vector = initial_coordinates - current_coordinates;
+  if (ShouldFreeScroll(scroll_container, drag_vector)) {
+    scroll_type = ScrollType::Free;
+  }
+  active_item_ = scroll_container;
+  active_scroll_type_ = scroll_type;
+
+  if (active_scroll_type_ == ScrollType::Horizontal) {
+    drag_vector.set_y(0.0f);
+  } else if (active_scroll_type_ == ScrollType::Vertical) {
+    drag_vector.set_x(0.0f);
+  }
+
+  previous_events_.push(
+      EventPositionWithTimeStamp(initial_coordinates, initial_time_stamp));
+  previous_events_.push(
+      EventPositionWithTimeStamp(current_coordinates, current_time_stamp));
+
+  active_velocity_ = drag_vector;
+  ScrollNavItemWithVector(active_item_, drag_vector);
+
+  auto event_to_handle = events_to_handle_.find(pointer_id);
+  if (event_to_handle != events_to_handle_.end()) {
+    HandlePointerEventForActiveItem(event_to_handle->second);
+  }
+}
+
+void ScrollEngine::CancelActiveScrollsForNavItems(
+    std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel) {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  for (auto scroll_to_cancel : scrolls_to_cancel) {
+    for (std::vector<FreeScrollingNavItem>::iterator it =
+             nav_items_with_decaying_scroll_.begin();
+         it != nav_items_with_decaying_scroll_.end();) {
+      if (it->nav_item.get() == scroll_to_cancel.get()) {
+        it = nav_items_with_decaying_scroll_.erase(it);
+      } else {
+        it++;
+      }
+    }
+  }
+}
+
+void ScrollEngine::ScrollNavItemsWithDecayingScroll() {
+  DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+  if (nav_items_with_decaying_scroll_.size() == 0) {
+    free_scroll_timer_.Stop();
+    return;
+  }
+  for (std::vector<FreeScrollingNavItem>::iterator it =
+           nav_items_with_decaying_scroll_.begin();
+       it != nav_items_with_decaying_scroll_.end();) {
+    auto now = base::Time::Now();
+    auto update_delta = now - it->last_change;
+    float fraction_of_time =
+        std::max<float>(update_delta / kFreeScrollDuration, 1.0);
+    float progress = timing_function_->Evaluate(fraction_of_time);
+    math::Vector2dF current_offset = it->target_offset - it->initial_offset;
+    current_offset.Scale(progress);
+    current_offset += it->initial_offset;
+    it->nav_item->SetContentOffset(current_offset.x(), current_offset.y());
+    it->last_change = now;
+
+    if (fraction_of_time == 1.0) {
+      it = nav_items_with_decaying_scroll_.erase(it);
+    } else {
+      it++;
+    }
+  }
+}
+
+}  // namespace scroll_engine
+}  // namespace ui_navigation
+}  // namespace cobalt
diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.h b/cobalt/ui_navigation/scroll_engine/scroll_engine.h
new file mode 100644
index 0000000..2399457
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.h
@@ -0,0 +1,107 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
+#define COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
+
+#include <map>
+#include <queue>
+#include <vector>
+
+#include "base/threading/thread.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "cobalt/base/token.h"
+#include "cobalt/dom/pointer_event.h"
+#include "cobalt/dom/pointer_event_init.h"
+#include "cobalt/math/vector2d_f.h"
+#include "cobalt/ui_navigation/nav_item.h"
+
+namespace cobalt {
+namespace ui_navigation {
+namespace scroll_engine {
+
+// kDragDistanceThreshold is measured in viewport coordinate distance.
+const float kDragDistanceThreshold = 20.0f;
+
+typedef enum ScrollType {
+  Unknown,
+  Horizontal,
+  Vertical,
+  Free,
+} ScrollType;
+
+class ScrollEngine {
+ public:
+  ScrollEngine();
+  ~ScrollEngine();
+
+  void HandlePointerEvent(base::Token type, const dom::PointerEventInit& event);
+  void HandleScrollStart(scoped_refptr<ui_navigation::NavItem> scroll_container,
+                         ScrollType scroll_type, int32_t pointer_id,
+                         math::Vector2dF initial_coordinates,
+                         uint64 initial_time_stamp,
+                         math::Vector2dF current_coordinates,
+                         uint64 current_time_stamp);
+  void CancelActiveScrollsForNavItems(
+      std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel);
+
+  void HandlePointerEventForActiveItem(
+      scoped_refptr<dom::PointerEvent> pointer_event);
+  void ScrollNavItemsWithDecayingScroll();
+  void MaybeFreeScrollActiveNavItem();
+
+  base::Thread* thread() { return &scroll_engine_; }
+
+ private:
+  base::Thread scroll_engine_{"ScrollEngineThread"};
+  base::RepeatingTimer free_scroll_timer_;
+
+  struct EventPositionWithTimeStamp {
+    EventPositionWithTimeStamp(math::Vector2dF position, uint64 time_stamp)
+        : position(position), time_stamp(time_stamp) {}
+    math::Vector2dF position;
+    uint64 time_stamp;
+  };
+
+  struct FreeScrollingNavItem {
+    FreeScrollingNavItem(scoped_refptr<NavItem> nav_item,
+                         math::Vector2dF initial_offset,
+                         math::Vector2dF target_offset)
+        : nav_item(nav_item),
+          initial_offset(initial_offset),
+          target_offset(target_offset),
+          last_change(base::Time::Now()) {}
+    scoped_refptr<NavItem> nav_item;
+    math::Vector2dF initial_offset;
+    math::Vector2dF target_offset;
+    base::Time last_change;
+  };
+
+  std::queue<EventPositionWithTimeStamp> previous_events_;
+  const scoped_refptr<cssom::TimingFunction>& timing_function_;
+
+  scoped_refptr<NavItem> active_item_;
+  math::Vector2dF active_velocity_;
+  ScrollType active_scroll_type_ = ScrollType::Unknown;
+  std::map<uint32_t, scoped_refptr<dom::PointerEvent>> events_to_handle_;
+  std::vector<FreeScrollingNavItem> nav_items_with_decaying_scroll_;
+};
+
+
+}  // namespace scroll_engine
+}  // namespace ui_navigation
+}  // namespace cobalt
+
+#endif  // COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
diff --git a/cobalt/updater/configurator.cc b/cobalt/updater/configurator.cc
index 3383817..027a68a 100644
--- a/cobalt/updater/configurator.cc
+++ b/cobalt/updater/configurator.cc
@@ -23,7 +23,6 @@
 #include "components/update_client/protocol_handler.h"
 #include "components/update_client/unzipper.h"
 #include "starboard/system.h"
-
 #include "url/gurl.h"
 
 namespace {
@@ -259,6 +258,7 @@
 }
 
 void Configurator::SetChannel(const std::string& updater_channel) {
+  LOG(INFO) << "Configurator::SetChannel updater_channel=" << updater_channel;
   base::AutoLock auto_lock(updater_channel_lock_);
   updater_channel_ = updater_channel;
 }
diff --git a/cobalt/updater/prefs.cc b/cobalt/updater/prefs.cc
index 3e195bb..fa0d8cd 100644
--- a/cobalt/updater/prefs.cc
+++ b/cobalt/updater/prefs.cc
@@ -18,13 +18,13 @@
 
 #include "base/files/file_path.h"
 #include "base/memory/scoped_refptr.h"
-#include "cobalt/extension/installation_manager.h"
 #include "cobalt/updater/utils.h"
 #include "components/prefs/json_pref_store.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/pref_service_factory.h"
 #include "components/update_client/update_client.h"
+#include "starboard/extension/installation_manager.h"
 
 namespace cobalt {
 namespace updater {
diff --git a/cobalt/updater/updater_module.cc b/cobalt/updater/updater_module.cc
index 00460fd..2305b02 100644
--- a/cobalt/updater/updater_module.cc
+++ b/cobalt/updater/updater_module.cc
@@ -32,7 +32,6 @@
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/version.h"
 #include "cobalt/browser/switches.h"
-#include "cobalt/extension/installation_manager.h"
 #include "cobalt/updater/crash_client.h"
 #include "cobalt/updater/crash_reporter.h"
 #include "cobalt/updater/utils.h"
@@ -41,11 +40,12 @@
 #include "components/update_client/utils.h"
 #include "starboard/common/file.h"
 #include "starboard/configuration_constants.h"
+#include "starboard/extension/installation_manager.h"
 
 namespace {
 
-using update_client::ComponentState;
 using update_client::CobaltSlotManagement;
+using update_client::ComponentState;
 
 // The SHA256 hash of the "cobalt_evergreen_public" key.
 constexpr uint8_t kCobaltPublicKeyHash[] = {
@@ -95,7 +95,7 @@
 namespace updater {
 
 // The delay in seconds before the first update check.
-const uint64_t kDefaultUpdateCheckDelaySeconds = 15;
+const uint64_t kDefaultUpdateCheckDelaySeconds = 30;
 
 void Observer::OnEvent(Events event, const std::string& id) {
   LOG(INFO) << "Observer::OnEvent id=" << id;
diff --git a/cobalt/updater/updater_module.h b/cobalt/updater/updater_module.h
index 157b121..f88ed46 100644
--- a/cobalt/updater/updater_module.h
+++ b/cobalt/updater/updater_module.h
@@ -22,13 +22,13 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/message_loop/message_loop.h"
 #include "base/threading/thread.h"
-#include "cobalt/extension/updater_notification.h"
 #include "cobalt/network/network_module.h"
 #include "cobalt/updater/configurator.h"
 #include "components/prefs/pref_service.h"
 #include "components/update_client/crx_update_item.h"
 #include "components/update_client/update_client.h"
 #include "starboard/event.h"
+#include "starboard/extension/updater_notification.h"
 
 namespace cobalt {
 namespace updater {
diff --git a/cobalt/updater/utils.cc b/cobalt/updater/utils.cc
index 5be22e3..05f3db1 100644
--- a/cobalt/updater/utils.cc
+++ b/cobalt/updater/utils.cc
@@ -16,11 +16,11 @@
 #include "base/strings/string_number_conversions.h"
 #include "base/values.h"
 #include "build/build_config.h"
-#include "cobalt/extension/installation_manager.h"
 #include "components/update_client/utils.h"
 #include "crypto/secure_hash.h"
 #include "crypto/sha2.h"
 #include "starboard/configuration_constants.h"
+#include "starboard/extension/installation_manager.h"
 #include "starboard/file.h"
 #include "starboard/string.h"
 #include "starboard/system.h"
diff --git a/cobalt/updater/utils_test.cc b/cobalt/updater/utils_test.cc
index 4a43cde..416586b 100644
--- a/cobalt/updater/utils_test.cc
+++ b/cobalt/updater/utils_test.cc
@@ -19,10 +19,10 @@
 #include "base/files/file_path.h"
 #include "base/strings/strcat.h"
 #include "base/values.h"
-#include "cobalt/extension/installation_manager.h"
 #include "gmock/gmock.h"
 #include "starboard/common/file.h"
 #include "starboard/directory.h"
+#include "starboard/extension/installation_manager.h"
 #include "starboard/file.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
diff --git a/cobalt/version.h b/cobalt/version.h
index 18d66ee..12dce9f 100644
--- a/cobalt/version.h
+++ b/cobalt/version.h
@@ -35,6 +35,6 @@
 //                  release is cut.
 //.
 
-#define COBALT_VERSION "23.master.0"
+#define COBALT_VERSION "24.master.0"
 
 #endif  // COBALT_VERSION_H_
diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc
index 5105785..61cc134 100644
--- a/cobalt/watchdog/watchdog.cc
+++ b/cobalt/watchdog/watchdog.cc
@@ -37,7 +37,7 @@
 // The Watchdog violations json filename.
 const char kWatchdogViolationsJson[] = "watchdog.json";
 // The frequency in microseconds of monitor loops.
-const int64_t kWatchdogMonitorFrequency = 1000000;
+const int64_t kWatchdogMonitorFrequency = 500000;
 // The maximum number of Watchdog violations.
 const int kWatchdogMaxViolations = 200;
 // The minimum number of microseconds between writes.
diff --git a/cobalt/web/BUILD.gn b/cobalt/web/BUILD.gn
index cda07f8..122f57c 100644
--- a/cobalt/web/BUILD.gn
+++ b/cobalt/web/BUILD.gn
@@ -97,6 +97,7 @@
     "url_utils.cc",
     "url_utils.h",
     "user_agent_platform_info.h",
+    "web_settings.h",
     "window_or_worker_global_scope.cc",
     "window_or_worker_global_scope.h",
   ]
@@ -107,11 +108,13 @@
     "//cobalt/browser:generated_bindings",
     "//cobalt/cache",
     "//cobalt/csp",
+    "//cobalt/dom:media_settings",
     "//cobalt/network",
     "//cobalt/network_bridge",
     "//cobalt/script",
     "//cobalt/script:engine",
     "//cobalt/script/v8c:engine",
+    "//cobalt/xhr:xhr_settings",
     "//net",
     "//url",
   ]
@@ -170,6 +173,8 @@
 
   sources = [
     "blob_test.cc",
+    "cache_storage_test.cc",
+    "cache_utils_test.cc",
     "crypto_test.cc",
     "csp_delegate_test.cc",
     "custom_event_test.cc",
diff --git a/cobalt/web/agent.cc b/cobalt/web/agent.cc
index e66ef8e..d111610 100644
--- a/cobalt/web/agent.cc
+++ b/cobalt/web/agent.cc
@@ -84,6 +84,7 @@
     return script_runner_.get();
   }
   Blob::Registry* blob_registry() const final { return blob_registry_.get(); }
+  web::WebSettings* web_settings() const final { return web_settings_; }
   network::NetworkModule* network_module() const final {
     DCHECK(fetcher_factory_);
     return fetcher_factory_->network_module();
@@ -100,9 +101,13 @@
     }
     environment_settings_.reset(environment_settings);
     if (environment_settings_) environment_settings_->set_context(this);
+    if (service_worker_jobs_) {
+      service_worker_jobs_->SetActiveWorker(environment_settings);
+    }
   }
 
   EnvironmentSettings* environment_settings() const final {
+    DCHECK(environment_settings_);
     DCHECK_EQ(environment_settings_->context(), this);
     return environment_settings_.get();
   }
@@ -144,9 +149,6 @@
   void set_active_service_worker(
       const scoped_refptr<worker::ServiceWorkerObject>& worker) final {
     active_service_worker_ = worker;
-    // Also hold a reference to the registration that contains this worker.
-    containing_service_worker_registration_ =
-        worker ? worker->containing_service_worker_registration() : nullptr;
   }
   const scoped_refptr<worker::ServiceWorkerObject>& active_service_worker()
       const final {
@@ -171,6 +173,9 @@
 
   // Name of the web instance.
   std::string name_;
+
+  web::WebSettings* const web_settings_;
+
   // FetcherFactory that is used to create a fetcher according to URL.
   std::unique_ptr<loader::FetcherFactory> fetcher_factory_;
 
@@ -216,15 +221,13 @@
   // Note: When a service worker is unregistered from the last client, this will
   // hold the last reference until the current page is unloaded.
   scoped_refptr<worker::ServiceWorkerObject> active_service_worker_;
-  scoped_refptr<worker::ServiceWorkerRegistrationObject>
-      containing_service_worker_registration_;
 
   base::ObserverList<Context::EnvironmentSettingsChangeObserver>::Unchecked
       environment_settings_change_observers_;
 };
 
 Impl::Impl(const std::string& name, const Agent::Options& options)
-    : name_(name) {
+    : name_(name), web_settings_(options.web_settings) {
   TRACE_EVENT0("cobalt::web", "Agent::Impl::Impl()");
   service_worker_jobs_ = options.service_worker_jobs;
   platform_info_ = options.platform_info;
@@ -287,7 +290,13 @@
   blob_registry_.reset();
   script_runner_.reset();
   execution_state_.reset();
-  global_environment_ = NULL;
+
+  // Ensure that global_environment_ is null before it's destroyed.
+  scoped_refptr<script::GlobalEnvironment> global_environment(
+      std::move(global_environment_));
+  DCHECK(!global_environment_);
+  global_environment = nullptr;
+
   javascript_engine_.reset();
   fetcher_factory_.reset();
   script_loader_factory_.reset();
@@ -453,7 +462,7 @@
 
 WindowOrWorkerGlobalScope* Impl::GetWindowOrWorkerGlobalScope() {
   script::Wrappable* global_wrappable =
-      global_environment()->global_wrappable();
+      global_environment_ ? global_environment_->global_wrappable() : nullptr;
   if (!global_wrappable) {
     return nullptr;
   }
diff --git a/cobalt/web/agent.h b/cobalt/web/agent.h
index aa48bb1..d57d526 100644
--- a/cobalt/web/agent.h
+++ b/cobalt/web/agent.h
@@ -30,6 +30,7 @@
 #include "cobalt/web/context.h"
 #include "cobalt/web/environment_settings.h"
 #include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
 
 namespace cobalt {
 namespace worker {
@@ -58,6 +59,7 @@
 
     script::JavaScriptEngine::Options javascript_engine_options;
 
+    web::WebSettings* web_settings = nullptr;
     network::NetworkModule* network_module = nullptr;
 
     // Optional directory to add to the search path for web files (file://).
diff --git a/cobalt/web/cache.cc b/cobalt/web/cache.cc
index bbef89f..9aa3155 100644
--- a/cobalt/web/cache.cc
+++ b/cobalt/web/cache.cc
@@ -115,6 +115,18 @@
     OnDone(/*success=*/false);
     return;
   }
+  status_text_ = request->response_headers()->GetStatusText();
+  response_code_ = request->response_headers()->response_code();
+  size_t iter = 0;
+  std::string name;
+  std::string value;
+  while (
+      request->response_headers()->EnumerateHeaderLines(&iter, &name, &value)) {
+    base::ListValue header;
+    header.GetList().emplace_back(name);
+    header.GetList().emplace_back(value);
+    headers_.GetList().push_back(std::move(header));
+  }
   int initial_capacity = request->response_headers()->HasHeader(
                              net::HttpRequestHeaders::kContentLength)
                              ? request->response_headers()->GetContentLength()
@@ -158,22 +170,25 @@
             auto* isolate = global_environment->isolate();
             auto cached =
                 cache::Cache::GetInstance()->Retrieve(kResourceType, key);
-            if (!cached) {
+            auto metadata =
+                cache::Cache::GetInstance()->Metadata(kResourceType, key);
+            if (!cached || !metadata || !metadata->FindKey("options")) {
               promise_reference->value().Resolve(
-                  cache_utils::GetUndefined(environment_settings));
+                  cache_utils::FromV8Value(isolate, v8::Undefined(isolate)));
               return;
             }
             script::v8c::EntryScope entry_scope(isolate);
-            auto response = cache_utils::CreateResponse(environment_settings,
-                                                        std::move(cached));
+            auto response = cache_utils::CreateResponse(
+                isolate, *cached, *(metadata->FindKey("options")));
             if (!response) {
               promise_reference->value().Reject();
             } else {
-              promise_reference->value().Resolve(script::Any(response.value()));
+              promise_reference->value().Resolve(
+                  cache_utils::FromV8Value(isolate, response.value()));
             }
           },
           environment_settings,
-          cache_utils::GetKey(environment_settings, request),
+          cache_utils::GetKey(environment_settings->base_url(), request),
           std::move(promise_reference)));
   return promise;
 }
@@ -185,7 +200,7 @@
   auto* global_environment = get_global_environment(environment_settings);
   auto* isolate = global_environment->isolate();
   script::v8c::EntryScope entry_scope(isolate);
-  uint32_t key = cache_utils::GetKey(environment_settings,
+  uint32_t key = cache_utils::GetKey(environment_settings->base_url(),
                                      request_reference->referenced_value());
   if (fetchers_.find(key) != fetchers_.end()) {
     base::AutoLock auto_lock(*(fetchers_[key]->lock()));
@@ -201,7 +216,7 @@
   auto* context = get_context(environment_settings);
   fetchers_[key] = std::make_unique<Cache::Fetcher>(
       context->network_module(),
-      GURL(cache_utils::GetUrl(environment_settings,
+      GURL(cache_utils::GetUrl(environment_settings->base_url(),
                                request_reference->referenced_value())),
       base::BindOnce(&Cache::OnFetchCompleted, base::Unretained(this), key));
 }
@@ -239,9 +254,24 @@
   auto promise_reference =
       std::make_unique<script::ValuePromiseVoid::Reference>(this, promise);
 
-  auto context = get_context(environment_settings);
-  base::SequencedTaskRunnerHandle::Get()->PostTask(
-      FROM_HERE,
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  auto context = isolate->GetCurrentContext();
+  script::v8c::EntryScope entry_scope(isolate);
+  auto v8_response =
+      GetV8Value(response_reference->referenced_value()).As<v8::Object>();
+  auto body_used = cache_utils::Get(v8_response, "bodyUsed");
+  if (!body_used || body_used->As<v8::Boolean>()->Value()) {
+    promise_reference->value().Reject(script::kTypeError);
+    return promise;
+  }
+  auto array_buffer_promise = cache_utils::Call(v8_response, "arrayBuffer");
+  if (!array_buffer_promise) {
+    promise_reference->value().Reject();
+    return promise;
+  }
+  cache_utils::Then(
+      array_buffer_promise.value(),
       base::BindOnce(
           [](script::EnvironmentSettings* environment_settings,
              std::unique_ptr<script::ValueHandleHolder::Reference>
@@ -249,131 +279,30 @@
              std::unique_ptr<script::ValueHandleHolder::Reference>
                  response_reference,
              std::unique_ptr<script::ValuePromiseVoid::Reference>
-                 promise_reference) {
-
-            auto* global_environment =
-                get_global_environment(environment_settings);
-            auto* isolate = global_environment->isolate();
-            script::v8c::EntryScope entry_scope(isolate);
-            auto context = global_environment->context();
-            auto maybe_body_used = cache_utils::TryGet(
-                context, GetV8Value(response_reference->referenced_value()),
-                "bodyUsed");
-            if (maybe_body_used.IsEmpty() ||
-                maybe_body_used.ToLocalChecked().As<v8::Boolean>()->Value()) {
-              promise_reference->value().Reject(script::kTypeError);
-              return;
-            }
-            auto maybe_text_function = cache_utils::TryGet(
-                context, GetV8Value(response_reference->referenced_value()),
-                "text");
-            if (maybe_text_function.IsEmpty()) {
+                 promise_reference,
+             v8::Local<v8::Promise> array_buffer_promise)
+              -> base::Optional<v8::Local<v8::Promise>> {
+            uint32_t key =
+                cache_utils::GetKey(environment_settings->base_url(),
+                                    request_reference->referenced_value());
+            std::string url =
+                cache_utils::GetUrl(environment_settings->base_url(),
+                                    request_reference->referenced_value());
+            auto options = cache_utils::ExtractResponseOptions(
+                cache_utils::ToV8Value(response_reference->referenced_value()));
+            if (!options) {
               promise_reference->value().Reject();
-              return;
+              return base::nullopt;
             }
-            auto text_function = maybe_text_function.ToLocalChecked();
-            v8::Local<v8::Value> text_result;
-            auto response_context =
-                script::GetIsolate(response_reference->referenced_value())
-                    ->GetCurrentContext();
-            if (text_function.IsEmpty() || !text_function->IsFunction() ||
-                !(text_function.As<v8::Function>()
-                      ->Call(response_context,
-                             GetV8Value(response_reference->referenced_value()),
-                             /*argc=*/0,
-                             /*argv=*/nullptr)
-                      .ToLocal(&text_result))) {
-              promise_reference->value().Reject();
-              return;
-            }
-            std::string url = cache_utils::GetUrl(
-                environment_settings, request_reference->referenced_value());
-            auto data = v8::Object::New(isolate);
-            cache_utils::Set(context, data, "environment_settings",
-                             v8::External::New(isolate, environment_settings));
-            cache_utils::Set(
-                context, data, "promise_reference",
-                v8::External::New(isolate, promise_reference.release()));
-            cache_utils::Set(
-                context, data, "request_reference",
-                v8::External::New(isolate, request_reference.release()));
-            auto then_callback =
-                v8::Function::New(
-                    context,
-                    [](const v8::FunctionCallbackInfo<v8::Value>& info) {
-                      auto* isolate = info.GetIsolate();
-                      auto context = info.GetIsolate()->GetCurrentContext();
-                      auto* environment_settings =
-                          static_cast<script::EnvironmentSettings*>(
-                              cache_utils::Get(context, info.Data(),
-                                               "environment_settings")
-                                  .As<v8::External>()
-                                  ->Value());
-                      std::unique_ptr<script::ValueHandleHolder::Reference>
-                      request_reference(
-                          static_cast<script::ValueHandleHolder::Reference*>(
-                              cache_utils::Get(context, info.Data(),
-                                               "request_reference")
-                                  .As<v8::External>()
-                                  ->Value()));
-                      std::unique_ptr<script::ValuePromiseVoid::Reference>
-                      promise_reference(
-                          static_cast<script::ValuePromiseVoid::Reference*>(
-                              cache_utils::Get(context, info.Data(),
-                                               "promise_reference")
-                                  .As<v8::External>()
-                                  ->Value()));
-                      uint32_t key = cache_utils::GetKey(
-                          environment_settings,
-                          request_reference->referenced_value());
-                      std::string url = cache_utils::GetUrl(
-                          environment_settings,
-                          request_reference->referenced_value());
-                      std::string body;
-                      FromJSValue(info.GetIsolate(), info[0],
-                                  script::v8c::kNoConversionFlags, nullptr,
-                                  &body);
-                      auto* begin =
-                          reinterpret_cast<const uint8_t*>(body.data());
-                      auto data = std::make_unique<std::vector<uint8_t>>(
-                          begin, begin + body.size());
-                      cache::Cache::GetInstance()->Store(
-                          kResourceType, key, *data, base::Value(url));
-                      promise_reference->value().Resolve();
-                    },
-                    data)
-                    .ToLocalChecked();
-            if (text_result.As<v8::Promise>()
-                    ->Then(context, then_callback)
-                    .IsEmpty()) {
-              promise_reference->value().Reject();
-              return;
-            }
-            auto catch_callback =
-                v8::Function::New(
-                    context,
-                    [](const v8::FunctionCallbackInfo<v8::Value>& info) {
-                      auto* isolate = info.GetIsolate();
-                      auto context = info.GetIsolate()->GetCurrentContext();
-                      std::unique_ptr<script::ValuePromiseVoid::Reference>
-                      promise_reference(
-                          static_cast<script::ValuePromiseVoid::Reference*>(
-                              cache_utils::Get(context, info.Data(),
-                                               "promise_reference")
-                                  .As<v8::External>()
-                                  ->Value()));
-                      promise_reference->value().Reject();
-                    },
-                    data)
-                    .ToLocalChecked();
-            if (text_result.As<v8::Promise>()
-                    ->Catch(context, catch_callback)
-                    .IsEmpty()) {
-              promise_reference->value().Reject();
-              return;
-            }
-            // Run |response.text()| promise.
-            isolate->PerformMicrotaskCheckpoint();
+            base::DictionaryValue metadata;
+            metadata.SetKey("url", base::Value(url));
+            metadata.SetKey("options", std::move(options.value()));
+            cache::Cache::GetInstance()->Store(
+                kResourceType, key,
+                cache_utils::ToUint8Vector(array_buffer_promise->Result()),
+                std::move(metadata));
+            promise_reference->value().Resolve();
+            return base::nullopt;
           },
           environment_settings, std::move(request_reference),
           std::move(response_reference), std::move(promise_reference)));
@@ -406,7 +335,7 @@
             promise_reference->value().Resolve(
                 cache::Cache::GetInstance()->Delete(
                     kResourceType, cache_utils::GetKey(
-                                       environment_settings,
+                                       environment_settings->base_url(),
                                        request_reference->referenced_value())));
           },
           environment_settings, std::move(request_reference),
@@ -432,27 +361,29 @@
                 get_global_environment(environment_settings);
             auto* isolate = global_environment->isolate();
             script::v8c::EntryScope entry_scope(isolate);
+            std::vector<v8::Local<v8::Value>> requests;
             auto keys =
                 cache::Cache::GetInstance()->KeysWithMetadata(kResourceType);
-            std::vector<v8::Local<v8::Value>> requests;
-            for (uint8_t key :
-                 cache::Cache::GetInstance()->KeysWithMetadata(kResourceType)) {
-              std::unique_ptr<base::Value> url =
+            for (uint32_t key : keys) {
+              auto metadata =
                   cache::Cache::GetInstance()->Metadata(kResourceType, key);
-              if (url && url->is_string()) {
-                base::Optional<script::Any> request =
-                    cache_utils::CreateRequest(environment_settings,
-                                               url->GetString());
-                if (request) {
-                  requests.push_back(GetV8Value(*(request->GetScriptValue())));
-                }
+              if (!metadata) {
+                continue;
+              }
+              auto url = metadata->FindKey("url");
+              if (!url) {
+                continue;
+              }
+              base::Optional<v8::Local<v8::Value>> request =
+                  cache_utils::CreateRequest(isolate, url->GetString());
+              if (request) {
+                requests.push_back(std::move(request.value()));
               }
             }
             promise_reference->value().Resolve(
                 script::Any(new script::v8c::V8cValueHandleHolder(
                     isolate, v8::Array::New(isolate, requests.data(),
                                             requests.size()))));
-
           },
           environment_settings, std::move(promise_reference)));
   return promise;
@@ -482,9 +413,16 @@
     return;
   }
   {
-    cache::Cache::GetInstance()->Store(kResourceType, key,
-                                       fetcher->BufferToVector(),
-                                       base::Value(fetcher->url().spec()));
+    base::DictionaryValue metadata;
+    metadata.SetKey("url", base::Value(fetcher->url().spec()));
+    base::DictionaryValue options;
+    options.SetKey("status", base::Value(fetcher->response_code()));
+    options.SetKey("statusText", base::Value(fetcher->status_text()));
+    options.SetKey("headers", std::move(fetcher->headers()));
+    metadata.SetKey("options", std::move(options));
+
+    cache::Cache::GetInstance()->Store(
+        kResourceType, key, fetcher->BufferToVector(), std::move(metadata));
     if (fetcher->mime_type() == "text/javascript") {
       auto* environment_settings = fetch_contexts_[key].second;
       auto* global_environment = get_global_environment(environment_settings);
diff --git a/cobalt/web/cache.h b/cobalt/web/cache.h
index ee09fb7..02e5c88 100644
--- a/cobalt/web/cache.h
+++ b/cobalt/web/cache.h
@@ -72,6 +72,9 @@
 
     const std::string& mime_type() const { return mime_type_; }
     GURL url() const { return url_; }
+    int response_code() const { return response_code_; }
+    const std::string& status_text() const { return status_text_; }
+    base::ListValue headers() { return std::move(headers_); }
     base::Lock* lock() const { return &lock_; }
     std::vector<uint8_t> BufferToVector() const;
     std::string BufferToString() const;
@@ -89,6 +92,9 @@
     std::string mime_type_;
     scoped_refptr<net::GrowableIOBuffer> buffer_;
     int buffer_size_;
+    int response_code_;
+    base::ListValue headers_;
+    std::string status_text_;
     mutable base::Lock lock_;
   };
 
diff --git a/cobalt/web/cache_storage.cc b/cobalt/web/cache_storage.cc
index db96ded..222b617 100644
--- a/cobalt/web/cache_storage.cc
+++ b/cobalt/web/cache_storage.cc
@@ -22,6 +22,7 @@
 #include "cobalt/base/source_location.h"
 #include "cobalt/cache/cache.h"
 #include "cobalt/script/source_code.h"
+#include "cobalt/script/v8c/v8c_value_handle.h"
 #include "cobalt/web/context.h"
 #include "cobalt/web/environment_settings_helper.h"
 
@@ -83,6 +84,28 @@
   return promise;
 }
 
+script::HandlePromiseBool CacheStorage::Has(
+    script::EnvironmentSettings* environment_settings,
+    const std::string& cache_name) {
+  script::HandlePromiseBool promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<bool>();
+  promise->Resolve(true);
+  return promise;
+}
+
+script::Handle<script::Promise<script::Handle<script::ValueHandle>>>
+CacheStorage::Keys(script::EnvironmentSettings* environment_settings) {
+  script::HandlePromiseAny promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<script::Any>();
+  auto global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  promise->Resolve(script::Any(
+      new script::v8c::V8cValueHandleHolder(isolate, v8::Array::New(isolate))));
+  return promise;
+}
+
 scoped_refptr<Cache> CacheStorage::GetOrCreateCache() {
   if (!cache_) {
     cache_ = new Cache();
diff --git a/cobalt/web/cache_storage.h b/cobalt/web/cache_storage.h
index ec25b1b..e3a4cfb 100644
--- a/cobalt/web/cache_storage.h
+++ b/cobalt/web/cache_storage.h
@@ -49,6 +49,11 @@
   script::HandlePromiseBool Delete(
       script::EnvironmentSettings* environment_settings,
       const std::string& cache_name);
+  script::HandlePromiseBool Has(
+      script::EnvironmentSettings* environment_settings,
+      const std::string& cache_name);
+  script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Keys(
+      script::EnvironmentSettings* environment_settings);
 
   DEFINE_WRAPPABLE_TYPE(CacheStorage);
 
diff --git a/cobalt/web/cache_storage.idl b/cobalt/web/cache_storage.idl
index a9744a6..9d4d586 100644
--- a/cobalt/web/cache_storage.idl
+++ b/cobalt/web/cache_storage.idl
@@ -22,4 +22,6 @@
   [CallWith=EnvironmentSettings, NewObject] Promise<Cache> open(DOMString cacheName);
   // Ignores |cacheName| and deletes all Cache API data.
   [CallWith=EnvironmentSettings, NewObject] Promise<boolean> delete(DOMString cacheName);
+  [CallWith=EnvironmentSettings, NewObject] Promise<boolean> has(DOMString cacheName);
+  [CallWith=EnvironmentSettings, NewObject] Promise<any> keys();
 };
diff --git a/cobalt/web/cache_storage_test.cc b/cobalt/web/cache_storage_test.cc
new file mode 100644
index 0000000..be76340
--- /dev/null
+++ b/cobalt/web/cache_storage_test.cc
@@ -0,0 +1,138 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/web/cache_utils.h"
+#include "cobalt/web/testing/test_with_javascript.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace web {
+
+namespace {
+
+class GetGlobalScopeTypeIdWindow : public ::testing::Test {
+ public:
+  base::TypeId GetGlobalScopeTypeId() const {
+    return base::GetTypeId<dom::Window>();
+  }
+};
+
+class CacheStorageTest
+    : public testing::TestWithJavaScriptBase<GetGlobalScopeTypeIdWindow> {};
+
+v8::Local<v8::Value> Await(base::Optional<v8::Local<v8::Value>> promise_value) {
+  EXPECT_TRUE(promise_value.has_value());
+  EXPECT_TRUE(promise_value.value()->IsPromise());
+  auto promise = promise_value->As<v8::Promise>();
+  auto* isolate = promise->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto e = std::make_unique<base::WaitableEvent>();
+  auto result_promise = promise->Then(
+      context, v8::Function::New(
+                   context,
+                   [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+                     static_cast<base::WaitableEvent*>(
+                         info.Data().As<v8::External>()->Value())
+                         ->Signal();
+                   },
+                   v8::External::New(isolate, e.get()))
+                   .ToLocalChecked());
+  EXPECT_TRUE(!result_promise.IsEmpty());
+  base::RunLoop run_loop;
+  run_loop.RunUntilIdle();
+  // Ensure promise is scheduled to be resolved.
+  isolate->PerformMicrotaskCheckpoint();
+  e->Wait();
+  EXPECT_TRUE(promise->State() == v8::Promise::PromiseState::kFulfilled);
+  return promise->Result();
+}
+
+base::Value MakeHeader(const std::string& name, const std::string& value) {
+  base::ListValue header;
+  header.GetList().emplace_back(name);
+  header.GetList().emplace_back(value);
+  return std::move(header);
+}
+
+std::vector<uint8_t> ToVector(const std::string& data) {
+  auto* begin = reinterpret_cast<const uint8_t*>(data.data());
+  auto* end = begin + data.size();
+  return std::vector<uint8_t>(begin, end);
+}
+
+}  // namespace
+
+TEST_F(CacheStorageTest, Work) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+
+  auto delete_result =
+      Await(cache_utils::Evaluate(isolate, "caches.delete('test');"));
+  EXPECT_TRUE(delete_result->IsBoolean() &&
+              delete_result.As<v8::Boolean>()->Value());
+
+  auto v8_cache = Await(cache_utils::Evaluate(isolate, "caches.open('test');"));
+  EXPECT_FALSE(v8_cache.IsEmpty());
+  EXPECT_FALSE(v8_cache->IsNullOrUndefined());
+  EXPECT_TRUE(v8_cache->IsObject());
+
+  std::string url = "https://www.example.com/1";
+  auto request = cache_utils::CreateRequest(isolate, url).value();
+  base::DictionaryValue options;
+  options.SetKey("status", base::Value(200));
+  options.SetKey("statusText", base::Value("OK"));
+  base::ListValue headers;
+  headers.GetList().push_back(MakeHeader("a", "1"));
+  options.SetKey("headers", std::move(headers));
+
+  std::string body = "abcde";
+  auto response =
+      cache_utils::CreateResponse(isolate, ToVector(body), options).value();
+  auto v8_put_result =
+      Await(cache_utils::Call(v8_cache, "put", {request, response}));
+  EXPECT_TRUE(v8_put_result->IsUndefined());
+
+  auto v8_match_result = Await(cache_utils::Call(v8_cache, "match", {request}));
+  EXPECT_EQ(200, cache_utils::FromNumber(
+                     cache_utils::Get(v8_match_result, "status").value()));
+  EXPECT_EQ("OK", cache_utils::GetString(v8_match_result, "statusText"));
+  EXPECT_EQ(
+      "1", cache_utils::FromV8String(
+               isolate, cache_utils::Call(v8_match_result, "headers.get",
+                                          {cache_utils::V8String(isolate, "a")})
+                            .value()));
+  auto match_result_options =
+      cache_utils::ExtractResponseOptions(v8_match_result).value();
+  EXPECT_EQ(200, cache_utils::Get(match_result_options, "status")->GetInt());
+  EXPECT_EQ("OK",
+            cache_utils::Get(match_result_options, "statusText")->GetString());
+  EXPECT_EQ(1, match_result_options.FindKey("headers")->GetList().size());
+  EXPECT_EQ("a",
+            cache_utils::Get(match_result_options, "headers.0.0")->GetString());
+  EXPECT_EQ("1",
+            cache_utils::Get(match_result_options, "headers.0.1")->GetString());
+  EXPECT_EQ(body,
+            cache_utils::FromV8String(
+                isolate, Await(cache_utils::Call(v8_match_result, "text"))));
+
+  auto v8_keys_result = Await(cache_utils::Call(v8_cache, "keys"));
+  EXPECT_TRUE(v8_keys_result->IsArray());
+  auto keys = v8_keys_result.As<v8::Array>();
+  EXPECT_EQ(1, keys->Length());
+  EXPECT_EQ(url, cache_utils::GetString(keys, "0.url"));
+}
+
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/cache_utils.cc b/cobalt/web/cache_utils.cc
index 92fc5a2..06bd976 100644
--- a/cobalt/web/cache_utils.cc
+++ b/cobalt/web/cache_utils.cc
@@ -14,156 +14,432 @@
 
 #include "cobalt/web/cache_utils.h"
 
-#include "cobalt/cache/cache.h"
+#include <algorithm>
+#include <utility>
+
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/strings/string_split.h"
 #include "cobalt/script/v8c/conversion_helpers.h"
 #include "cobalt/web/environment_settings_helper.h"
+#include "starboard/common/murmurhash2.h"
 
 namespace cobalt {
 namespace web {
 namespace cache_utils {
 
 v8::Local<v8::String> V8String(v8::Isolate* isolate, const std::string& s) {
-  return v8::String::NewFromUtf8(isolate, s.c_str()).ToLocalChecked();
+  return v8::String::NewFromUtf8(isolate, s.c_str())
+      .FromMaybe(v8::String::Empty(isolate));
 }
 
-v8::MaybeLocal<v8::Value> TryGet(v8::Local<v8::Context> context,
-                                 v8::Local<v8::Value> object,
-                                 const std::string& key) {
-  if (!object->IsObject()) {
-    return v8::MaybeLocal<v8::Value>();
+std::string FromV8String(v8::Isolate* isolate, v8::Local<v8::Value> value) {
+  if (!value->IsString()) {
+    return "";
   }
-  auto* isolate = context->GetIsolate();
-  return object.As<v8::Object>()->Get(context, V8String(isolate, key));
+  auto v8_string = value.As<v8::String>();
+  std::string result;
+  FromJSValue(isolate, v8_string, script::v8c::kNoConversionFlags, nullptr,
+              &result);
+  return result;
 }
 
-v8::Local<v8::Value> Get(v8::Local<v8::Context> context,
-                         v8::Local<v8::Value> object, const std::string& key) {
-  return TryGet(context, object, key).ToLocalChecked();
+base::Optional<v8::Local<v8::Value>> Parse(v8::Isolate* isolate,
+                                           const std::string& json) {
+  return Evaluate(isolate, "{ const obj = " + json + "; obj; }");
 }
 
-bool Set(v8::Local<v8::Context> context, v8::Local<v8::Value> object,
-         const std::string& key, v8::Local<v8::Value> value) {
-  if (!object->IsObject()) {
-    return false;
-  }
-  auto* isolate = context->GetIsolate();
-  auto result =
-      object.As<v8::Object>()->Set(context, V8String(isolate, key), value);
-  return !result.IsNothing();
+base::Optional<v8::Local<v8::Value>> BaseToV8(v8::Isolate* isolate,
+                                              const base::Value& value) {
+  auto json = std::make_unique<std::string>();
+  base::JSONWriter::WriteWithOptions(
+      value, base::JSONWriter::OPTIONS_OMIT_BINARY_VALUES, json.get());
+  return Parse(isolate, *json);
 }
 
-v8::MaybeLocal<v8::Value> TryCall(v8::Local<v8::Context> context,
-                                  v8::Local<v8::Value> object,
-                                  const std::string& key, int argc,
-                                  v8::Local<v8::Value> argv[]) {
-  v8::Local<v8::Value> function;
-  if (!cache_utils::TryGet(context, object, key).ToLocal(&function) ||
-      function.IsEmpty() || !function->IsFunction()) {
-    return v8::MaybeLocal<v8::Value>();
-  }
-  auto object_context =
-      object.As<v8::Object>()->GetIsolate()->GetCurrentContext();
-  return function.As<v8::Function>()->Call(object_context, object, argc, argv);
-}
-
-script::Any GetUndefined(script::EnvironmentSettings* environment_settings) {
-  auto* global_environment = get_global_environment(environment_settings);
-  auto* isolate = global_environment->isolate();
-  return script::Any(
-      new script::v8c::V8cValueHandleHolder(isolate, v8::Undefined(isolate)));
-}
-
-script::Any EvaluateString(script::EnvironmentSettings* environment_settings,
-                           const std::string& js_code) {
-  auto* global_environment = get_global_environment(environment_settings);
-  auto* wrappable = get_global_wrappable(environment_settings);
-  base::Optional<script::ValueHandleHolder::Reference> reference;
-  scoped_refptr<script::SourceCode> source_code =
-      script::SourceCode::CreateSourceCodeWithoutCaching(
-          js_code, base::SourceLocation(__FILE__, __LINE__, 1));
-  bool eval_enabled = global_environment->IsEvalEnabled();
-  if (!eval_enabled) {
-    global_environment->EnableEval();
-  }
-  bool success =
-      global_environment->EvaluateScript(source_code, wrappable, &reference);
-  if (!eval_enabled) {
-    global_environment->DisableEval("");
-  }
-  if (success && reference) {
-    return script::Any(reference.value());
-  } else {
-    return GetUndefined(environment_settings);
-  }
-}
-
-base::Optional<script::Any> CreateInstance(
-    script::EnvironmentSettings* environment_settings,
-    const std::string& class_name, int argc, v8::Local<v8::Value> argv[]) {
-  auto* global_environment = get_global_environment(environment_settings);
-  auto* isolate = global_environment->isolate();
-  auto reponse_function =
-      cache_utils::EvaluateString(environment_settings, class_name);
-  auto v8_function =
-      script::GetV8Value(*reponse_function.GetScriptValue()).As<v8::Function>();
-  auto context = isolate->GetCurrentContext();
-  auto maybe_instance = v8_function->NewInstance(context, argc, argv);
-  if (maybe_instance.IsEmpty()) {
+base::Optional<v8::Local<v8::Promise>> OptionalPromise(
+    base::Optional<v8::Local<v8::Value>> value) {
+  if (!value || !(*value)->IsPromise()) {
     return base::nullopt;
   }
-  return script::Any(new script::v8c::V8cValueHandleHolder(
-      isolate, maybe_instance.ToLocalChecked()));
+  return value->As<v8::Promise>();
 }
 
-base::Optional<script::Any> CreateRequest(
-    script::EnvironmentSettings* environment_settings, const std::string& url) {
-  auto* global_environment = get_global_environment(environment_settings);
-  auto* isolate = global_environment->isolate();
-  v8::Local<v8::Value> argv[] = {V8String(isolate, url)};
-  return CreateInstance(environment_settings, "Request", /*argc=*/1, argv);
+std::string Stringify(v8::Isolate* isolate, v8::Local<v8::Value> value) {
+  auto global = isolate->GetCurrentContext()->Global();
+  Set(global, "___tempObject", value);
+  auto result = Evaluate(isolate, "JSON.stringify(___tempObject);");
+  Delete(global, "___tempObject");
+  if (!result) {
+    return "";
+  }
+  return FromV8String(isolate, result.value());
 }
 
-base::Optional<script::Any> CreateResponse(
-    script::EnvironmentSettings* environment_settings,
-    std::unique_ptr<std::vector<uint8_t>> data) {
-  auto* global_environment = get_global_environment(environment_settings);
-  auto* isolate = global_environment->isolate();
-  auto array_buffer = v8::ArrayBuffer::New(isolate, data->size());
-  memcpy(array_buffer->GetBackingStore()->Data(), data->data(), data->size());
-  v8::Local<v8::Value> argv[] = {array_buffer};
-  return CreateInstance(environment_settings, "Response", /*argc=*/1, argv);
+base::Optional<base::Value> Deserialize(const std::string& json) {
+  if (json.empty()) {
+    return base::nullopt;
+  }
+  return base::Value::FromUniquePtrValue(base::JSONReader::Read(json));
 }
 
-uint32_t GetKey(const std::string& url) { return cache::Cache::CreateKey(url); }
+base::Optional<base::Value> V8ToBase(v8::Isolate* isolate,
+                                     v8::Local<v8::Value> value) {
+  return Deserialize(Stringify(isolate, value));
+}
 
-uint32_t GetKey(script::EnvironmentSettings* environment_settings,
+template <typename T>
+base::Optional<v8::Local<T>> ToOptional(v8::MaybeLocal<T> value) {
+  if (value.IsEmpty()) {
+    return base::nullopt;
+  }
+  return value.ToLocalChecked();
+}
+
+v8::Isolate* GetIsolate(v8::Local<v8::Value> object) {
+  if (!object->IsObject()) {
+    return nullptr;
+  }
+  return object.As<v8::Object>()->GetIsolate();
+}
+
+base::Optional<v8::Local<v8::Value>> GetInternal(v8::Local<v8::Value> object,
+                                                 const std::string& path,
+                                                 bool parent) {
+  auto* isolate = GetIsolate(object);
+  if (!isolate) {
+    return base::nullopt;
+  }
+
+  base::Optional<v8::Local<v8::Value>> curr = object;
+  auto context = isolate->GetCurrentContext();
+  auto parts = base::SplitString(path, ".", base::TRIM_WHITESPACE,
+                                 base::SPLIT_WANT_NONEMPTY);
+  int offset = parent ? -1 : 0;
+  for (int i = 0; i < parts.size() + offset; i++) {
+    if (!curr || !curr.value()->IsObject()) {
+      return base::nullopt;
+    }
+    std::string part = parts[i];
+    if (base::ContainsOnlyChars(part, "0123456789")) {
+      uint32_t index;
+      if (!base::StringToUint32(part, &index)) {
+        return base::nullopt;
+      }
+      curr = ToOptional(curr->As<v8::Object>()->Get(context, index));
+    } else {
+      curr = ToOptional(
+          curr->As<v8::Object>()->Get(context, V8String(isolate, part)));
+    }
+  }
+  return curr;
+}
+
+base::Optional<v8::Local<v8::Value>> Get(v8::Local<v8::Value> object,
+                                         const std::string& path) {
+  return GetInternal(object, path, /*parent=*/false);
+}
+
+const base::Value* Get(const base::Value& value, const std::string& path,
+                       bool parent) {
+  if (!value.is_dict() && !value.is_list()) {
+    return nullptr;
+  }
+  const base::Value* curr = &value;
+  auto parts = base::SplitString(path, ".", base::TRIM_WHITESPACE,
+                                 base::SPLIT_WANT_NONEMPTY);
+  int offset = parent ? -1 : 0;
+  for (int i = 0; i < parts.size() + offset; i++) {
+    std::string part = parts[i];
+    if (curr->is_list()) {
+      uint32_t index;
+      if (!base::StringToUint32(part, &index)) {
+        return nullptr;
+      }
+      if (index > curr->GetList().size() - 1) {
+        return nullptr;
+      }
+      curr = &curr->GetList()[index];
+    } else if (curr->is_dict()) {
+      curr = curr->FindKey(part);
+    } else {
+      return nullptr;
+    }
+  }
+  return curr;
+}
+
+template <typename T>
+using V8Transform =
+    std::function<base::Optional<T>(v8::Isolate*, v8::Local<v8::Value>)>;
+
+struct V8Transforms {
+  static base::Optional<double> ToDouble(v8::Isolate* isolate,
+                                         v8::Local<v8::Value> value) {
+    if (!value->IsNumber()) {
+      return base::nullopt;
+    }
+    return value.As<v8::Number>()->Value();
+  }
+
+  static base::Optional<std::string> ToString(v8::Isolate* isolate,
+                                              v8::Local<v8::Value> value) {
+    if (!value->IsString()) {
+      return base::nullopt;
+    }
+    auto v8_string = value.As<v8::String>();
+    std::string result;
+    FromJSValue(isolate, v8_string, script::v8c::kNoConversionFlags, nullptr,
+                &result);
+    return std::move(result);
+  }
+
+};  // V8Transforms
+
+template <typename T>
+base::Optional<T> Get(v8::Local<v8::Value> object, const std::string& path,
+                      V8Transform<T> transform) {
+  auto value = GetInternal(object, path, /*parent=*/false);
+  if (!value) {
+    return base::nullopt;
+  }
+  return transform(GetIsolate(object), value.value());
+}
+
+base::Optional<std::string> GetString(v8::Local<v8::Value> object,
+                                      const std::string& path) {
+  return Get<std::string>(object, path, V8Transforms::ToString);
+}
+
+base::Optional<double> GetNumber(v8::Local<v8::Value> object,
+                                 const std::string& path) {
+  return Get<double>(object, path, V8Transforms::ToDouble);
+}
+
+bool Set(v8::Local<v8::Object> object, const std::string& key,
+         v8::Local<v8::Value> value) {
+  auto* isolate = object->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto result = object->Set(context, V8String(isolate, key), value);
+  return result.FromMaybe(false);
+}
+
+bool Delete(v8::Local<v8::Object> object, const std::string& key) {
+  auto* isolate = object->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto result = object->Delete(context, V8String(isolate, key));
+  return result.FromMaybe(false);
+}
+
+double FromNumber(v8::Local<v8::Value> value) {
+  return value.As<v8::Number>()->Value();
+}
+
+v8::Local<v8::Number> ToNumber(v8::Isolate* isolate, double d) {
+  return v8::Number::New(isolate, d);
+}
+
+std::vector<uint8_t> ToUint8Vector(v8::Local<v8::Value> buffer) {
+  if (!buffer->IsArrayBuffer()) {
+    return std::vector<uint8_t>();
+  }
+  auto array_buffer = buffer.As<v8::ArrayBuffer>();
+  auto byte_length = array_buffer->ByteLength();
+  auto uint8_array =
+      v8::Uint8Array::New(array_buffer, /*byte_offset=*/0, byte_length);
+  auto vector = std::vector<uint8_t>(byte_length);
+  uint8_array->CopyContents(vector.data(), byte_length);
+  return std::move(vector);
+}
+
+base::Optional<v8::Local<v8::Value>> Call(
+    v8::Local<v8::Value> object, const std::string& path,
+    std::initializer_list<v8::Local<v8::Value>> args) {
+  if (!object->IsObject()) {
+    return base::nullopt;
+  }
+  v8::Local<v8::Value> result;
+  auto optional_function = cache_utils::Get(object, path);
+  if (!optional_function || !optional_function.value()->IsFunction()) {
+    return base::nullopt;
+  }
+  auto context_object =
+      cache_utils::GetInternal(object, path, /*parent=*/true).value();
+  auto context =
+      context_object.As<v8::Object>()->GetIsolate()->GetCurrentContext();
+  const size_t argc = args.size();
+  std::vector<v8::Local<v8::Value>> argv = args;
+  return ToOptional(optional_function->As<v8::Function>()->Call(
+      context, context_object, argv.size(), argv.data()));
+}
+
+base::Optional<v8::Local<v8::Value>> Then(v8::Local<v8::Value> value,
+                                          OnFullfilled on_fullfilled) {
+  if (!value->IsPromise()) {
+    on_fullfilled.Reset();
+    return base::nullopt;
+  }
+  auto promise = value.As<v8::Promise>();
+  auto* isolate = promise->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto data = v8::Object::New(isolate);
+  Set(data, "promise", promise);
+  auto* on_fullfilled_ptr = new OnFullfilled(std::move(on_fullfilled));
+  Set(data, "onFullfilled", v8::External::New(isolate, on_fullfilled_ptr));
+  auto resulting_promise = promise->Then(
+      context,
+      v8::Function::New(
+          context,
+          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+            auto promise = Get(info.Data(), "promise")->As<v8::Promise>();
+            auto on_fullfilled = std::unique_ptr<OnFullfilled>(
+                static_cast<OnFullfilled*>(Get(info.Data(), "onFullfilled")
+                                               ->As<v8::External>()
+                                               ->Value()));
+            auto optional_resulting_promise =
+                std::move(*on_fullfilled).Run(promise);
+            if (!optional_resulting_promise) {
+              return;
+            }
+            info.GetReturnValue().Set(optional_resulting_promise.value());
+          },
+          data)
+          .ToLocalChecked(),
+      v8::Function::New(
+          context,
+          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+            auto on_fullfilled = std::unique_ptr<OnFullfilled>(
+                static_cast<OnFullfilled*>(Get(info.Data(), "onFullfilled")
+                                               ->As<v8::External>()
+                                               ->Value()));
+            on_fullfilled->Reset();
+            info.GetIsolate()->ThrowException(info[0]);
+          },
+          data)
+          .ToLocalChecked());
+  if (resulting_promise.IsEmpty()) {
+    delete on_fullfilled_ptr;
+    return base::nullopt;
+  }
+  return resulting_promise.ToLocalChecked();
+}
+
+script::Any FromV8Value(v8::Isolate* isolate, v8::Local<v8::Value> value) {
+  return script::Any(new script::v8c::V8cValueHandleHolder(isolate, value));
+}
+
+v8::Local<v8::Value> ToV8Value(const script::Any& any) {
+  return script::GetV8Value(*any.GetScriptValue());
+}
+
+base::Optional<v8::Local<v8::Value>> Evaluate(v8::Isolate* isolate,
+                                              const std::string& js_code) {
+  auto context = isolate->GetCurrentContext();
+  auto script = v8::Script::Compile(context, V8String(isolate, js_code));
+  if (script.IsEmpty()) {
+    return base::nullopt;
+  }
+  return ToOptional(script.ToLocalChecked()->Run(context));
+}
+
+base::Optional<v8::Local<v8::Value>> CreateInstance(
+    v8::Isolate* isolate, const std::string& class_name,
+    std::initializer_list<v8::Local<v8::Value>> args) {
+  auto constructor = Evaluate(isolate, class_name);
+  if (!constructor) {
+    return base::nullopt;
+  }
+  auto context = isolate->GetCurrentContext();
+  std::vector<v8::Local<v8::Value>> argv = args;
+  return ToOptional(constructor.value().As<v8::Function>()->NewInstance(
+      context, argv.size(), argv.data()));
+}
+
+base::Optional<v8::Local<v8::Value>> CreateRequest(v8::Isolate* isolate,
+                                                   const std::string& url) {
+  return CreateInstance(isolate, "Request", {V8String(isolate, url)});
+}
+
+base::Optional<v8::Local<v8::Value>> CreateResponse(
+    v8::Isolate* isolate, const std::vector<uint8_t>& body,
+    const base::Value& options) {
+  auto status = options.FindKey("status");
+  auto status_text = options.FindKey("statusText");
+  auto headers = options.FindKey("headers");
+  if (body.size() == 0 || !status || !status_text || !headers) {
+    return base::nullopt;
+  }
+  auto v8_body = v8::ArrayBuffer::New(isolate, body.size());
+  memcpy(v8_body->GetBackingStore()->Data(), body.data(), body.size());
+  auto v8_options = v8::Object::New(isolate);
+  Set(v8_options, "status", ToNumber(isolate, status->GetDouble()));
+  Set(v8_options, "statusText", V8String(isolate, status_text->GetString()));
+  auto v8_headers = v8::Object::New(isolate);
+  for (const auto& header : headers->GetList()) {
+    const auto& pair = header.GetList();
+    DCHECK(pair.size() == 2);
+    auto name = pair[0].GetString();
+    auto value = pair[1].GetString();
+    Set(v8_headers, name, V8String(isolate, value));
+  }
+  Set(v8_options, "headers", v8_headers);
+  return CreateInstance(isolate, "Response", {v8_body, v8_options});
+}
+
+base::Optional<base::Value> ExtractResponseOptions(
+    v8::Local<v8::Value> response) {
+  if (!response->IsObject()) {
+    return base::nullopt;
+  }
+  auto response_object = response.As<v8::Object>();
+  auto* isolate = response_object->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto global = context->Global();
+  Set(global, "___tempResponseObject", response_object);
+  auto result = Evaluate(isolate,
+                         "(() =>"
+                         "___tempResponseObject instanceof Response && {"
+                         "status: ___tempResponseObject.status,"
+                         "statusText: ___tempResponseObject.statusText,"
+                         "headers: Array.from(___tempResponseObject.headers),"
+                         "}"
+                         ")()");
+  Delete(global, "___tempResponseObject");
+  if (!result) {
+    return base::nullopt;
+  }
+  return V8ToBase(isolate, result.value());
+}
+
+uint32_t GetKey(const std::string& s) {
+  return starboard::MurmurHash2_32(s.c_str(), s.size());
+}
+
+uint32_t GetKey(const GURL& base_url,
                 const script::ValueHandleHolder& request_info) {
-  return GetKey(GetUrl(environment_settings, request_info));
+  return GetKey(GetUrl(base_url, request_info));
 }
 
-std::string GetUrl(script::EnvironmentSettings* environment_settings,
+std::string GetUrl(const GURL& base_url,
                    const script::ValueHandleHolder& request_info) {
   auto v8_value = GetV8Value(request_info);
   auto* isolate = GetIsolate(request_info);
-  v8::Local<v8::String> v8_string;
-  if (v8_value->IsString()) {
-    v8_string = v8_value.As<v8::String>();
-  } else {
-    auto context = isolate->GetCurrentContext();
-    // Treat like |Request| and get "url" property.
-    v8_string = Get(context, v8_value, "url").As<v8::String>();
-  }
   std::string url;
-  FromJSValue(isolate, v8_string, script::v8c::kNoConversionFlags, nullptr,
-              &url);
+  if (v8_value->IsString()) {
+    url = FromV8String(isolate, v8_value);
+  } else {
+    // Treat like |Request| and get "url" property.
+    auto v8_url = Get(v8_value, "url");
+    if (!v8_url) {
+      return "";
+    }
+    url = FromV8String(isolate, v8_url.value());
+  }
   GURL::Replacements replacements;
   replacements.ClearUsername();
   replacements.ClearPassword();
   replacements.ClearRef();
-  return environment_settings->base_url()
-      .Resolve(url)
-      .ReplaceComponents(replacements)
-      .spec();
+  return base_url.Resolve(url).ReplaceComponents(replacements).spec();
 }
 
 }  // namespace cache_utils
diff --git a/cobalt/web/cache_utils.h b/cobalt/web/cache_utils.h
index 0ed4c1b..863a079 100644
--- a/cobalt/web/cache_utils.h
+++ b/cobalt/web/cache_utils.h
@@ -15,87 +15,117 @@
 #ifndef COBALT_WEB_CACHE_UTILS_H_
 #define COBALT_WEB_CACHE_UTILS_H_
 
+#include <initializer_list>
 #include <memory>
 #include <string>
 #include <vector>
 
+#include "base/bind.h"
+#include "base/optional.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/values.h"
 #include "cobalt/script/environment_settings.h"
 #include "cobalt/script/script_value_factory.h"
 #include "cobalt/script/value_handle.h"
+#include "url/gurl.h"
 #include "v8/include/v8.h"
 
 namespace cobalt {
 namespace web {
 namespace cache_utils {
 
+using OnFullfilled = base::OnceCallback<base::Optional<v8::Local<v8::Promise>>(
+    v8::Local<v8::Promise>)>;
+
 v8::Local<v8::String> V8String(v8::Isolate* isolate, const std::string& s);
 
-v8::MaybeLocal<v8::Value> TryGet(v8::Local<v8::Context> context,
-                                 v8::Local<v8::Value> object,
-                                 const std::string& key);
+std::string FromV8String(v8::Isolate* isolate, v8::Local<v8::Value> value);
 
-v8::Local<v8::Value> Get(v8::Local<v8::Context> context,
-                         v8::Local<v8::Value> object, const std::string& key);
+base::Optional<v8::Local<v8::Promise>> OptionalPromise(
+    base::Optional<v8::Local<v8::Value>> value);
+
+const base::Value* Get(const base::Value& value, const std::string& path,
+                       bool parent = false);
+
+base::Optional<v8::Local<v8::Value>> Get(v8::Local<v8::Value> object,
+                                         const std::string& path);
 
 template <typename T>
-inline T* GetExternal(v8::Local<v8::Context> context,
-                      v8::Local<v8::Value> object, const std::string& key) {
-  return static_cast<T*>(
-      web::cache_utils::Get(context, object, key).As<v8::External>()->Value());
+inline T* GetExternal(v8::Local<v8::Value> object, const std::string& path) {
+  base::Optional<v8::Local<v8::Value>> value = Get(object, path);
+  if (!value) {
+    return nullptr;
+  }
+  return static_cast<T*>(value->As<v8::External>()->Value());
 }
 
 template <typename T>
-inline std::unique_ptr<T> GetOwnedExternal(v8::Local<v8::Context> context,
-                                           v8::Local<v8::Value> object,
-                                           const std::string& key) {
-  return std::unique_ptr<T>(
-      web::cache_utils::GetExternal<T>(context, object, key));
+inline std::unique_ptr<T> GetOwnedExternal(v8::Local<v8::Object> object,
+                                           const std::string& path) {
+  return std::unique_ptr<T>(web::cache_utils::GetExternal<T>(object, path));
 }
 
-bool Set(v8::Local<v8::Context> context, v8::Local<v8::Value> object,
-         const std::string& key, v8::Local<v8::Value> value);
+base::Optional<double> GetNumber(v8::Local<v8::Value> object,
+                                 const std::string& path);
+base::Optional<std::string> GetString(v8::Local<v8::Value> object,
+                                      const std::string& path);
 
+bool Set(v8::Local<v8::Object> object, const std::string& key,
+         v8::Local<v8::Value> value);
 
 template <typename T>
-inline bool SetExternal(v8::Local<v8::Context> context,
-                        v8::Local<v8::Value> object, const std::string& key,
+inline bool SetExternal(v8::Local<v8::Object> object, const std::string& key,
                         T* value) {
-  auto* isolate = context->GetIsolate();
-  return Set(context, object, key, v8::External::New(isolate, value));
+  auto* isolate = object->GetIsolate();
+  return Set(object, key, v8::External::New(isolate, value));
 }
 
 template <typename T>
-inline bool SetOwnedExternal(v8::Local<v8::Context> context,
-                             v8::Local<v8::Value> object,
+inline bool SetOwnedExternal(v8::Local<v8::Object> object,
                              const std::string& key, std::unique_ptr<T> value) {
-  return SetExternal<T>(context, object, key, value.release());
+  return SetExternal<T>(object, key, value.release());
 }
 
-v8::MaybeLocal<v8::Value> TryCall(v8::Local<v8::Context> context,
-                                  v8::Local<v8::Value> object,
-                                  const std::string& key, int argc = 0,
-                                  v8::Local<v8::Value> argv[] = nullptr);
+bool Delete(v8::Local<v8::Object> object, const std::string& key);
 
-script::Any GetUndefined(script::EnvironmentSettings* environment_settings);
+base::Optional<v8::Local<v8::Value>> Call(
+    v8::Local<v8::Value> object, const std::string& key,
+    std::initializer_list<v8::Local<v8::Value>> args = {});
+base::Optional<v8::Local<v8::Value>> Then(v8::Local<v8::Value> value,
+                                          OnFullfilled on_fullfilled);
 
-script::Any EvaluateString(script::EnvironmentSettings* environment_settings,
-                           const std::string& js_code);
+std::string Stringify(v8::Isolate* isolate, v8::Local<v8::Value> value);
+base::Optional<v8::Local<v8::Value>> BaseToV8(v8::Isolate* isolate,
+                                              const base::Value& value);
+base::Optional<base::Value> Deserialize(const std::string& json);
+base::Optional<base::Value> V8ToBase(v8::Isolate* isolate,
+                                     v8::Local<v8::Value> value);
 
-base::Optional<script::Any> CreateInstance(
-    script::EnvironmentSettings* environment_settings,
-    const std::string& class_name, int argc, v8::Local<v8::Value> argv[]);
-base::Optional<script::Any> CreateRequest(
-    script::EnvironmentSettings* environment_settings, const std::string& url);
-base::Optional<script::Any> CreateResponse(
-    script::EnvironmentSettings* environment_settings,
-    std::unique_ptr<std::vector<uint8_t>> data);
+double FromNumber(v8::Local<v8::Value> value);
+v8::Local<v8::Number> ToNumber(v8::Isolate* isolate, double d);
 
-uint32_t GetKey(const std::string& url);
+std::vector<uint8_t> ToUint8Vector(v8::Local<v8::Value> buffer);
 
-uint32_t GetKey(script::EnvironmentSettings* environment_settings,
+script::Any FromV8Value(v8::Isolate* isolate, v8::Local<v8::Value> value);
+v8::Local<v8::Value> ToV8Value(const script::Any& any);
+
+base::Optional<v8::Local<v8::Value>> Evaluate(v8::Isolate* isolate,
+                                              const std::string& js_code);
+
+base::Optional<v8::Local<v8::Value>> CreateRequest(v8::Isolate* isolate,
+                                                   const std::string& url);
+base::Optional<v8::Local<v8::Value>> CreateResponse(
+    v8::Isolate* isolate, const std::vector<uint8_t>& body,
+    const base::Value& options);
+base::Optional<base::Value> ExtractResponseOptions(
+    v8::Local<v8::Value> response);
+
+uint32_t GetKey(const std::string& s);
+
+uint32_t GetKey(const GURL& base_url,
                 const script::ValueHandleHolder& request_info);
 
-std::string GetUrl(script::EnvironmentSettings* environment_settings,
+std::string GetUrl(const GURL& base_url,
                    const script::ValueHandleHolder& request_info);
 
 }  // namespace cache_utils
diff --git a/cobalt/web/cache_utils_test.cc b/cobalt/web/cache_utils_test.cc
new file mode 100644
index 0000000..e076af4
--- /dev/null
+++ b/cobalt/web/cache_utils_test.cc
@@ -0,0 +1,230 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/web/cache_utils.h"
+
+#include <utility>
+
+#include "base/message_loop/message_loop.h"
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/web/agent.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/testing/test_with_javascript.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace web {
+
+namespace {
+
+v8::Local<v8::Value> ExpectState(
+    base::Optional<v8::Local<v8::Value>> promise_value,
+    v8::Promise::PromiseState expected) {
+  EXPECT_TRUE(promise_value.has_value());
+  EXPECT_TRUE(promise_value.value()->IsPromise());
+  auto promise = promise_value->As<v8::Promise>();
+  auto* isolate = promise->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto e = std::make_unique<base::WaitableEvent>();
+  auto fulfilled_promise = promise->Then(
+      context, v8::Function::New(
+                   context,
+                   [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+                     static_cast<base::WaitableEvent*>(
+                         info.Data().As<v8::External>()->Value())
+                         ->Signal();
+                   },
+                   v8::External::New(isolate, e.get()))
+                   .ToLocalChecked());
+  auto rejected_promise = promise->Catch(
+      context, v8::Function::New(
+                   context,
+                   [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+                     static_cast<base::WaitableEvent*>(
+                         info.Data().As<v8::External>()->Value())
+                         ->Signal();
+                   },
+                   v8::External::New(isolate, e.get()))
+                   .ToLocalChecked());
+  EXPECT_TRUE(!fulfilled_promise.IsEmpty());
+  EXPECT_TRUE(!rejected_promise.IsEmpty());
+  base::RunLoop run_loop;
+  run_loop.RunUntilIdle();
+  // Ensure promise is scheduled to be resolved.
+  isolate->PerformMicrotaskCheckpoint();
+  e->Wait();
+  EXPECT_TRUE(promise->State() == expected);
+  return promise->Result();
+}
+
+class GetGlobalScopeTypeIdWindow : public ::testing::Test {
+ public:
+  base::TypeId GetGlobalScopeTypeId() const {
+    return base::GetTypeId<dom::Window>();
+  }
+};
+
+class CacheUtilsTest
+    : public testing::TestWithJavaScriptBase<GetGlobalScopeTypeIdWindow> {};
+
+}  // namespace
+
+TEST_F(CacheUtilsTest, Eval) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  auto v8_this = v8::Object::New(isolate);
+  cache_utils::Set(v8_this, "a", v8::Number::New(isolate, 1.0));
+  EXPECT_TRUE(v8_this->IsObject());
+  EXPECT_DOUBLE_EQ(1.0,
+                   cache_utils::Get(v8_this, "a")->As<v8::Number>()->Value());
+  auto context = isolate->GetCurrentContext();
+  auto global = context->Global();
+  cache_utils::Set(global, "obj", v8_this);
+  auto result =
+      cache_utils::Evaluate(isolate, "obj.a++; obj;")->As<v8::Object>();
+
+  EXPECT_DOUBLE_EQ(2.0,
+                   cache_utils::Get(result, "a")->As<v8::Number>()->Value());
+  EXPECT_DOUBLE_EQ(2.0,
+                   cache_utils::Get(v8_this, "a")->As<v8::Number>()->Value());
+}
+
+TEST_F(CacheUtilsTest, ResponseHeaders) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  std::string body_string = "body";
+  auto* body_begin = reinterpret_cast<const uint8_t*>(body_string.data());
+  auto* body_end = body_begin + body_string.size();
+  std::vector<uint8_t> body(body_begin, body_end);
+  base::DictionaryValue initial_options;
+  initial_options.SetKey("status", base::Value(200));
+  initial_options.SetKey("statusText", base::Value("OK"));
+  base::ListValue initial_headers;
+  base::ListValue header_1;
+  header_1.GetList().emplace_back("a");
+  header_1.GetList().emplace_back("1");
+  initial_headers.GetList().push_back(std::move(header_1));
+  base::ListValue header_2;
+  header_2.GetList().emplace_back("b");
+  header_2.GetList().emplace_back("2");
+  initial_headers.GetList().push_back(std::move(header_2));
+  initial_options.SetKey("headers", std::move(initial_headers));
+  auto response =
+      cache_utils::CreateResponse(isolate, body, initial_options).value();
+
+  EXPECT_DOUBLE_EQ(200, cache_utils::FromNumber(
+                            cache_utils::Get(response, "status").value()));
+  EXPECT_EQ("OK",
+            cache_utils::FromV8String(
+                isolate, cache_utils::Get(response, "statusText").value()));
+  base::Value options = cache_utils::ExtractResponseOptions(response).value();
+
+  EXPECT_DOUBLE_EQ(200, cache_utils::Get(options, "status")->GetDouble());
+  EXPECT_EQ("OK", cache_utils::Get(options, "statusText")->GetString());
+  EXPECT_EQ(2, cache_utils::Get(options, "headers")->GetList().size());
+  EXPECT_EQ("a", cache_utils::Get(options, "headers.0.0")->GetString());
+  EXPECT_EQ("1", cache_utils::Get(options, "headers.0.1")->GetString());
+  EXPECT_EQ("b", cache_utils::Get(options, "headers.1.0")->GetString());
+  EXPECT_EQ("2", cache_utils::Get(options, "headers.1.1")->GetString());
+}
+
+TEST_F(CacheUtilsTest, BaseToV8) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  std::string json = R"~({
+  "options": {
+    "headers": [["a", "1"], ["b", "2"]],
+    "status": 200,
+    "statusText": "OK"
+  },
+  "url": "https://www.example.com/1"
+})~";
+  base::Value base_value = cache_utils::Deserialize(json).value();
+  auto v8_value = cache_utils::BaseToV8(isolate, base_value).value();
+  EXPECT_TRUE(v8_value->IsObject());
+  EXPECT_EQ("https://www.example.com/1",
+            cache_utils::GetString(v8_value, "url"));
+  EXPECT_DOUBLE_EQ(200,
+                   cache_utils::GetNumber(v8_value, "options.status").value());
+  EXPECT_EQ("OK", cache_utils::GetString(v8_value, "options.statusText"));
+  EXPECT_EQ("a", cache_utils::GetString(v8_value, "options.headers.0.0"));
+  EXPECT_EQ("1", cache_utils::GetString(v8_value, "options.headers.0.1"));
+  EXPECT_EQ("b", cache_utils::GetString(v8_value, "options.headers.1.0"));
+  EXPECT_EQ("2", cache_utils::GetString(v8_value, "options.headers.1.1"));
+
+  EXPECT_EQ(base_value, cache_utils::V8ToBase(isolate, v8_value));
+}
+
+TEST_F(CacheUtilsTest, ThenFulfilled) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  auto promise =
+      cache_utils::Evaluate(isolate, "Promise.resolve('success')").value();
+  auto still_fulfilled_promise = cache_utils::Then(
+      promise, base::BindOnce([&](v8::Local<v8::Promise> p)
+                                  -> base::Optional<v8::Local<v8::Promise>> {
+        auto* isolate = p->GetIsolate();
+        std::string new_message =
+            "still " + cache_utils::FromV8String(isolate, p->Result());
+        return cache_utils::Evaluate(isolate,
+                                     base::StringPrintf("Promise.resolve('%s')",
+                                                        new_message.c_str()))
+            ->As<v8::Promise>();
+      }));
+  auto promise_result = ExpectState(still_fulfilled_promise,
+                                    v8::Promise::PromiseState::kFulfilled);
+  EXPECT_EQ("still success",
+            cache_utils::FromV8String(isolate, promise_result));
+}
+
+TEST_F(CacheUtilsTest, ThenFulfilledThenRejected) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  auto promise =
+      cache_utils::Evaluate(isolate, "Promise.resolve('success')").value();
+  auto rejected_promise = cache_utils::Then(
+      promise, base::BindOnce([&](v8::Local<v8::Promise> p)
+                                  -> base::Optional<v8::Local<v8::Promise>> {
+        auto* isolate = p->GetIsolate();
+        std::string new_message =
+            "no longer " + cache_utils::FromV8String(isolate, p->Result());
+        return cache_utils::Evaluate(isolate,
+                                     base::StringPrintf("Promise.reject('%s')",
+                                                        new_message.c_str()))
+            ->As<v8::Promise>();
+      }));
+  auto promise_result =
+      ExpectState(rejected_promise, v8::Promise::PromiseState::kRejected);
+  EXPECT_EQ("no longer success",
+            cache_utils::FromV8String(isolate, promise_result));
+}
+
+TEST_F(CacheUtilsTest, ThenRejected) {
+  auto* isolate = web_context()->global_environment()->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  auto promise =
+      cache_utils::Evaluate(isolate, "Promise.reject('fail')").value();
+  auto still_rejected_promise = cache_utils::Then(
+      promise,
+      base::BindOnce(
+          [](v8::Local<v8::Promise>) -> base::Optional<v8::Local<v8::Promise>> {
+            return base::nullopt;
+          }));
+  auto promise_result =
+      ExpectState(still_rejected_promise, v8::Promise::PromiseState::kRejected);
+  EXPECT_EQ("fail", cache_utils::FromV8String(isolate, promise_result));
+}
+
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/context.h b/cobalt/web/context.h
index 19f6caf..66825db 100644
--- a/cobalt/web/context.h
+++ b/cobalt/web/context.h
@@ -29,6 +29,7 @@
 #include "cobalt/web/blob.h"
 #include "cobalt/web/environment_settings.h"
 #include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
 
 namespace cobalt {
 namespace worker {
@@ -62,6 +63,7 @@
   virtual script::ExecutionState* execution_state() const = 0;
   virtual script::ScriptRunner* script_runner() const = 0;
   virtual Blob::Registry* blob_registry() const = 0;
+  virtual web::WebSettings* web_settings() const = 0;
   virtual network::NetworkModule* network_module() const = 0;
   virtual worker::ServiceWorkerJobs* service_worker_jobs() const = 0;
 
diff --git a/cobalt/web/csp_delegate.cc b/cobalt/web/csp_delegate.cc
index 8b1cbb9..2a54b9c 100644
--- a/cobalt/web/csp_delegate.cc
+++ b/cobalt/web/csp_delegate.cc
@@ -45,11 +45,29 @@
 
 CspDelegateSecure::~CspDelegateSecure() {}
 
+void CspDelegateSecure::ClonePolicyContainer(
+    const csp::ContentSecurityPolicy& other) {
+  // https://html.spec.whatwg.org/commit-snapshots/814668ef2d1919a2a9387a0b29ebc6df7748fa80/#clone-a-policy-container
+  // To clone a policy container given a policy container policyContainer:
+  // 1. Let clone be a new policy container.
+  //   We already have csp_ initialized in the constructor.
+  // 2. For each policy in policyContainer's CSP list, append a copy of policy
+  // into clone's CSP list.
+  for (const auto& directive_list : other.policies()) {
+    DCHECK(directive_list);
+    csp_->append_policy(*directive_list);
+  }
+  // 3. Set clone's embedder policy to a copy of policyContainer's embedder
+  // policy.
+  //   Cobalt doesn't currently store embedder policy.
+  // 4. Set clone's referrer policy to policyContainer's referrer policy.
+  csp_->set_referrer_policy(csp_->referrer_policy());
+}
+
 bool CspDelegateSecure::CanLoad(ResourceType type, const GURL& url,
                                 bool did_redirect) const {
-  const csp::ContentSecurityPolicy::RedirectStatus redirect_status =
-      did_redirect ? csp::ContentSecurityPolicy::kDidRedirect
-                   : csp::ContentSecurityPolicy::kDidNotRedirect;
+  const csp::RedirectStatus redirect_status =
+      did_redirect ? csp::kDidRedirect : csp::kDidNotRedirect;
 
   // Special case for "offline" mode- in the absence of any server policy,
   // we check our default navigation policy, to permit navigation to
@@ -87,6 +105,9 @@
     case kStyle:
       can_load = csp_->AllowStyleFromSource(url, redirect_status);
       break;
+    case kWorker:
+      can_load = csp_->AllowWorkerFromSource(url, redirect_status);
+      break;
     case kXhr:
       can_load = csp_->AllowConnectToSource(url, redirect_status);
       break;
@@ -100,12 +121,18 @@
 bool CspDelegateSecure::IsValidNonce(ResourceType type,
                                      const std::string& nonce) const {
   bool is_valid = false;
-  if (type == kScript) {
-    is_valid = csp_->AllowScriptWithNonce(nonce);
-  } else if (type == kStyle) {
-    is_valid = csp_->AllowStyleWithNonce(nonce);
-  } else {
-    NOTREACHED() << "Invalid resource type " << type;
+  switch (type) {
+    case kScript:
+      is_valid = csp_->AllowScriptWithNonce(nonce);
+      break;
+    case kStyle:
+      is_valid = csp_->AllowStyleWithNonce(nonce);
+      break;
+    case kWorker:
+      is_valid = csp_->AllowWorkerWithNonce(nonce);
+      break;
+    default:
+      NOTREACHED() << "Invalid resource type " << type;
   }
   return is_valid;
 }
@@ -118,14 +145,21 @@
     return true;
   }
   bool can_load = false;
-  if (type == kScript) {
-    can_load = csp_->AllowInlineScript(location.file_path, location.line_number,
-                                       content);
-  } else if (type == kStyle) {
-    can_load = csp_->AllowInlineStyle(location.file_path, location.line_number,
-                                      content);
-  } else {
-    NOTREACHED() << "Invalid resource type" << type;
+  switch (type) {
+    case kScript:
+      can_load = csp_->AllowInlineScript(location.file_path,
+                                         location.line_number, content);
+      break;
+    case kStyle:
+      can_load = csp_->AllowInlineStyle(location.file_path,
+                                        location.line_number, content);
+      break;
+    case kWorker:
+      can_load = csp_->AllowInlineWorker(location.file_path,
+                                         location.line_number, content);
+      break;
+    default:
+      NOTREACHED() << "Invalid resource type" << type;
   }
   return can_load;
 }
@@ -133,8 +167,7 @@
 bool CspDelegateSecure::AllowEval(std::string* eval_disabled_message) const {
   bool allow_eval =
       // If CSP is not provided, allow eval() function.
-      !was_header_received_ ||
-      csp_->AllowEval(csp::ContentSecurityPolicy::kSuppressReport);
+      !was_header_received_ || csp_->AllowEval(csp::kSuppressReport);
   if (!allow_eval && eval_disabled_message) {
     *eval_disabled_message = csp_->disable_eval_error_message();
   }
@@ -142,7 +175,7 @@
 }
 
 void CspDelegateSecure::ReportEval() const {
-  csp_->AllowEval(csp::ContentSecurityPolicy::kSendReport);
+  csp_->AllowEval(csp::kSendReport);
 }
 
 bool CspDelegateSecure::OnReceiveHeaders(const csp::ResponseHeaders& headers) {
diff --git a/cobalt/web/csp_delegate.h b/cobalt/web/csp_delegate.h
index 0a74804..4fa1cfb 100644
--- a/cobalt/web/csp_delegate.h
+++ b/cobalt/web/csp_delegate.h
@@ -38,6 +38,7 @@
     kMedia,
     kScript,
     kStyle,
+    kWorker,
     kXhr,
     kWebSocket,
   };
@@ -45,6 +46,10 @@
   CspDelegate();
   virtual ~CspDelegate();
 
+  virtual const csp::ContentSecurityPolicy* GetPolicyContainer() = 0;
+  virtual void ClonePolicyContainer(
+      const csp::ContentSecurityPolicy& other) = 0;
+
   // Return |true| if the given resource type can be loaded from |url|.
   // Set |did_redirect| if url was the result of a redirect.
   virtual bool CanLoad(ResourceType type, const GURL& url,
@@ -83,6 +88,13 @@
 class CspDelegateInsecure : public CspDelegate {
  public:
   CspDelegateInsecure() {}
+  const csp::ContentSecurityPolicy* GetPolicyContainer() override {
+    return nullptr;
+  }
+  void ClonePolicyContainer(const csp::ContentSecurityPolicy& other) override {
+    // No policy to clone.
+    return;
+  }
   bool CanLoad(ResourceType, const GURL&, bool) const override { return true; }
   bool IsValidNonce(ResourceType, const std::string&) const override {
     return true;
@@ -109,6 +121,11 @@
                     const base::Closure& policy_changed_callback);
   ~CspDelegateSecure();
 
+  const csp::ContentSecurityPolicy* GetPolicyContainer() override {
+    return csp_.get();
+  }
+  void ClonePolicyContainer(const csp::ContentSecurityPolicy& other) override;
+
   // Return |true| if the given resource type can be loaded from |url|.
   // Set |did_redirect| if url was the result of a redirect.
   bool CanLoad(ResourceType type, const GURL& url,
diff --git a/cobalt/web/csp_delegate_test.cc b/cobalt/web/csp_delegate_test.cc
index b10dbaf..96e5ccc 100644
--- a/cobalt/web/csp_delegate_test.cc
+++ b/cobalt/web/csp_delegate_test.cc
@@ -34,8 +34,12 @@
 namespace {
 
 struct ResourcePair {
+  // Resource type queried for the test.
   CspDelegate::ResourceType type;
+  // Directive to allow 'self' for.
   const char* directive;
+  // Effective directive reported in the violation.
+  const char* effective_directive;
 };
 
 std::ostream& operator<<(std::ostream& out, const ResourcePair& obj) {
@@ -43,21 +47,35 @@
 }
 
 const ResourcePair s_params[] = {
-    {CspDelegate::kFont, "font-src"},
-    {CspDelegate::kImage, "img-src"},
-    {CspDelegate::kLocation, "h5vcc-location-src"},
-    {CspDelegate::kMedia, "media-src"},
-    {CspDelegate::kScript, "script-src"},
-    {CspDelegate::kStyle, "style-src"},
-    {CspDelegate::kXhr, "connect-src"},
-    {CspDelegate::kWebSocket, "connect-src"},
+    {CspDelegate::kFont, "font-src", "font-src"},
+    {CspDelegate::kFont, "default-src", "font-src"},
+    {CspDelegate::kImage, "img-src", "img-src"},
+    {CspDelegate::kImage, "default-src", "img-src"},
+    {CspDelegate::kLocation, "h5vcc-location-src", "h5vcc-location-src"},
+    {CspDelegate::kMedia, "media-src", "media-src"},
+    {CspDelegate::kMedia, "default-src", "media-src"},
+    {CspDelegate::kScript, "script-src", "script-src"},
+    {CspDelegate::kScript, "default-src", "script-src"},
+    {CspDelegate::kStyle, "style-src", "style-src"},
+    {CspDelegate::kStyle, "default-src", "style-src"},
+    {CspDelegate::kWorker, "worker-src", "worker-src"},
+    {CspDelegate::kWorker, "script-src", "worker-src"},
+    {CspDelegate::kWorker, "default-src", "worker-src"},
+    {CspDelegate::kXhr, "connect-src", "connect-src"},
+    {CspDelegate::kXhr, "default-src", "connect-src"},
+    {CspDelegate::kWebSocket, "connect-src", "connect-src"},
+    {CspDelegate::kWebSocket, "default-src", "connect-src"},
 };
 
 std::string ResourcePairName(::testing::TestParamInfo<ResourcePair> info) {
-  std::string name(info.param.directive);
-  std::replace(name.begin(), name.end(), '-', '_');
-  base::StringAppendF(&name, "_type_%d", info.param.type);
-  return name;
+  std::string directive(info.param.directive);
+  std::replace(directive.begin(), directive.end(), '-', '_');
+  std::string effective_directive(info.param.effective_directive);
+  std::replace(effective_directive.begin(), effective_directive.end(), '-',
+               '_');
+  return base::StringPrintf("type_%d_directive_%s_effective_%s",
+                            info.param.type, directive.c_str(),
+                            effective_directive.c_str());
 }
 
 class MockViolationReporter : public CspViolationReporter {
@@ -115,8 +133,13 @@
 
   csp_delegate_.reset(new CspDelegateSecure(
       std::move(reporter), origin, csp::kCSPRequired, base::Closure()));
-  std::string policy =
-      base::StringPrintf("default-src none; %s 'self'", GetParam().directive);
+  std::string policy;
+  if (!strcmp(GetParam().directive, "default-src")) {
+    policy = base::StringPrintf("%s 'self'", GetParam().directive);
+  } else {
+    policy =
+        base::StringPrintf("default-src none; %s 'self'", GetParam().directive);
+  }
   csp_delegate_->OnReceiveHeader(policy, csp::kHeaderTypeEnforce,
                                  csp::kHeaderSourceMeta);
 }
@@ -129,7 +152,7 @@
 
 TEST_P(CspDelegateTest, LoadNotOk) {
   CspDelegate::ResourceType param = GetParam().type;
-  std::string effective_directive = GetParam().directive;
+  std::string effective_directive = GetParam().effective_directive;
   GURL test_url("http://www.evil.com");
 
   csp::ViolationInfo info;
diff --git a/cobalt/web/environment_settings_helper.cc b/cobalt/web/environment_settings_helper.cc
index 4a87c2e..3c50954 100644
--- a/cobalt/web/environment_settings_helper.cc
+++ b/cobalt/web/environment_settings_helper.cc
@@ -32,6 +32,10 @@
   return get_context(environment_settings)->global_environment();
 }
 
+v8::Isolate* get_isolate(script::EnvironmentSettings* environment_settings) {
+  return get_global_environment(environment_settings)->isolate();
+}
+
 script::Wrappable* get_global_wrappable(
     script::EnvironmentSettings* environment_settings) {
   return get_global_environment(environment_settings)->global_wrappable();
@@ -42,5 +46,10 @@
   return get_global_environment(environment_settings)->script_value_factory();
 }
 
+base::MessageLoop* get_message_loop(
+    script::EnvironmentSettings* environment_settings) {
+  return get_context(environment_settings)->message_loop();
+}
+
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/environment_settings_helper.h b/cobalt/web/environment_settings_helper.h
index ca1b83e..5511937 100644
--- a/cobalt/web/environment_settings_helper.h
+++ b/cobalt/web/environment_settings_helper.h
@@ -19,17 +19,21 @@
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/script_value_factory.h"
 #include "cobalt/web/context.h"
+#include "v8/include/v8.h"
 
 namespace cobalt {
 namespace web {
 
 Context* get_context(script::EnvironmentSettings* environment_settings);
+v8::Isolate* get_isolate(script::EnvironmentSettings* environment_settings);
 script::GlobalEnvironment* get_global_environment(
     script::EnvironmentSettings* environment_settings);
 script::Wrappable* get_global_wrappable(
     script::EnvironmentSettings* environment_settings);
 script::ScriptValueFactory* get_script_value_factory(
     script::EnvironmentSettings* environment_settings);
+base::MessageLoop* get_message_loop(
+    script::EnvironmentSettings* environment_settings);
 
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/event_target.h b/cobalt/web/event_target.h
index 7484d98..134fe79 100644
--- a/cobalt/web/event_target.h
+++ b/cobalt/web/event_target.h
@@ -303,6 +303,13 @@
     SetAttributeEventListener(base::Tokens::scroll(), event_listener);
   }
 
+  const EventListenerScriptValue* onpointercancel() {
+    return GetAttributeEventListener(base::Tokens::pointercancel());
+  }
+  void set_onpointercancel(const EventListenerScriptValue& event_listener) {
+    SetAttributeEventListener(base::Tokens::pointercancel(), event_listener);
+  }
+
   const EventListenerScriptValue* ongotpointercapture() {
     return GetAttributeEventListener(base::Tokens::gotpointercapture());
   }
diff --git a/cobalt/web/on_error_event_listener.h b/cobalt/web/on_error_event_listener.h
index 18754ca..d7455c3 100644
--- a/cobalt/web/on_error_event_listener.h
+++ b/cobalt/web/on_error_event_listener.h
@@ -45,7 +45,7 @@
  protected:
   virtual base::Optional<bool> HandleEvent(
       const scoped_refptr<script::Wrappable>& callback_this,
-      EventOrMessage message, const std::string& filename, uint32 lineno,
+      const EventOrMessage& message, const std::string& filename, uint32 lineno,
       uint32 colno, const script::ValueHandleHolder* error,
       bool* had_exception) const = 0;
 };
diff --git a/cobalt/web/testing/BUILD.gn b/cobalt/web/testing/BUILD.gn
index 17f0326..8c67e01 100644
--- a/cobalt/web/testing/BUILD.gn
+++ b/cobalt/web/testing/BUILD.gn
@@ -28,6 +28,7 @@
     "//base",
     "//base/test:test_support",
     "//cobalt/base",
+    "//cobalt/browser",
     "//cobalt/loader",
     "//cobalt/network",
     "//cobalt/script",
diff --git a/cobalt/web/testing/stub_web_context.h b/cobalt/web/testing/stub_web_context.h
index dd97ea3..311c255 100644
--- a/cobalt/web/testing/stub_web_context.h
+++ b/cobalt/web/testing/stub_web_context.h
@@ -32,6 +32,7 @@
 #include "cobalt/web/testing/stub_environment_settings.h"
 #include "cobalt/web/url.h"
 #include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
 #include "cobalt/worker/service_worker.h"
 #include "cobalt/worker/service_worker_object.h"
 #include "cobalt/worker/service_worker_registration.h"
@@ -51,6 +52,7 @@
     javascript_engine_ = script::JavaScriptEngine::CreateEngine();
     global_environment_ = javascript_engine_->CreateGlobalEnvironment();
     blob_registry_.reset(new Blob::Registry);
+    web_settings_.reset(new WebSettingsImpl());
     network_module_.reset(new network::NetworkModule());
     fetcher_factory_.reset(new loader::FetcherFactory(
         network_module_.get(),
@@ -99,6 +101,10 @@
     DCHECK(blob_registry_);
     return blob_registry_.get();
   }
+  web::WebSettings* web_settings() const final {
+    DCHECK(web_settings_);
+    return web_settings_.get();
+  }
   network::NetworkModule* network_module() const final {
     DCHECK(network_module_);
     return network_module_.get();
@@ -207,6 +213,7 @@
   std::unique_ptr<script::JavaScriptEngine> javascript_engine_;
   scoped_refptr<script::GlobalEnvironment> global_environment_;
 
+  std::unique_ptr<WebSettingsImpl> web_settings_;
   std::unique_ptr<network::NetworkModule> network_module_;
   // Environment Settings object
   std::unique_ptr<EnvironmentSettings> environment_settings_;
diff --git a/cobalt/web/web_settings.h b/cobalt/web/web_settings.h
new file mode 100644
index 0000000..3079c31
--- /dev/null
+++ b/cobalt/web/web_settings.h
@@ -0,0 +1,91 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_WEB_SETTINGS_H_
+#define COBALT_WEB_WEB_SETTINGS_H_
+
+#include <string>
+
+#include "cobalt/dom/media_settings.h"
+#include "cobalt/xhr/xhr_settings.h"
+#include "starboard/types.h"
+
+namespace cobalt {
+namespace web {
+
+// The `WebSettings` interface and its concrete implementation
+// `WebSettingsImpl` provide a convenient way to share browser wide settings
+// that can be read by various modules of Cobalt across the main web module and
+// all Workers.
+//
+// These settings can be updated by the web app via `h5vcc.settings` or other
+// web interfaces.
+//
+// The following should be introduced when a group of new settings are needed;
+// 1. A new interface close to where the code using these settings to allow C++
+//    code read the settings, like the `XhrSettings` used below.
+//    A pure virtual function should be added to `WebSettings` to return a
+//    pointer to this interface.
+// 2. A concrete class inherited from the new interface to allow the settings to
+//    be updated.
+//    A member variable as an instance of this class should be added to
+//    `WebSettingsImpl` and hooked up in `WebSettingsImpl::Set()`.
+
+// A centralized interface to hold all setting interfaces to allow them to be
+// accessed.  Any modifications to the settings should be done through the
+// concrete class below.
+class WebSettings {
+ public:
+  virtual const dom::MediaSettings& media_settings() const = 0;
+  virtual const xhr::XhrSettings& xhr_settings() const = 0;
+
+ protected:
+  ~WebSettings() {}
+};
+
+// A centralized class to hold all concrete setting classes to allow
+// modifications.  It exposes the Set() function to allow updating settings via
+// a name.
+class WebSettingsImpl final : public web::WebSettings {
+ public:
+  const dom::MediaSettings& media_settings() const override {
+    return media_settings_impl_;
+  }
+
+  const xhr::XhrSettingsImpl& xhr_settings() const override {
+    return xhr_settings_impl_;
+  }
+
+  bool Set(const std::string& name, int32_t value) {
+    if (media_settings_impl_.Set(name, value)) {
+      return true;
+    }
+
+    if (xhr_settings_impl_.Set(name, value)) {
+      return true;
+    }
+
+    LOG(WARNING) << "Failed to set " << name << " to " << value;
+    return false;
+  }
+
+ private:
+  dom::MediaSettingsImpl media_settings_impl_;
+  xhr::XhrSettingsImpl xhr_settings_impl_;
+};
+
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_WEB_SETTINGS_H_
diff --git a/cobalt/web/window_or_worker_global_scope.h b/cobalt/web/window_or_worker_global_scope.h
index 73d8c5c..d2580ac 100644
--- a/cobalt/web/window_or_worker_global_scope.h
+++ b/cobalt/web/window_or_worker_global_scope.h
@@ -99,6 +99,8 @@
     return nullptr;
   }
 
+  // The CspDelegate gives access to the CSP list of the policy container
+  //   https://html.spec.whatwg.org/commit-snapshots/ae3c91103aada3d6d346a6dd3c5356773195fc79/#policy-container
   web::CspDelegate* csp_delegate() const {
     DCHECK(csp_delegate_);
     return csp_delegate_.get();
diff --git a/cobalt/worker/BUILD.gn b/cobalt/worker/BUILD.gn
index 5e6df51..f56409c 100644
--- a/cobalt/worker/BUILD.gn
+++ b/cobalt/worker/BUILD.gn
@@ -44,6 +44,8 @@
     "service_worker_jobs.h",
     "service_worker_object.cc",
     "service_worker_object.h",
+    "service_worker_persistent_settings.cc",
+    "service_worker_persistent_settings.h",
     "service_worker_registration.cc",
     "service_worker_registration.h",
     "service_worker_registration_map.cc",
diff --git a/cobalt/worker/dedicated_worker.cc b/cobalt/worker/dedicated_worker.cc
index 1daf030..39251a7 100644
--- a/cobalt/worker/dedicated_worker.cc
+++ b/cobalt/worker/dedicated_worker.cc
@@ -33,41 +33,48 @@
 }  // namespace
 
 DedicatedWorker::DedicatedWorker(script::EnvironmentSettings* settings,
-                                 const std::string& scriptURL)
+                                 const std::string& scriptURL,
+                                 script::ExceptionState* exception_state)
     : web::EventTarget(settings), script_url_(scriptURL) {
-  Initialize();
+  Initialize(exception_state);
 }
 
 DedicatedWorker::DedicatedWorker(script::EnvironmentSettings* settings,
                                  const std::string& scriptURL,
-                                 const WorkerOptions& worker_options)
+                                 const WorkerOptions& worker_options,
+                                 script::ExceptionState* exception_state)
     : web::EventTarget(settings),
       script_url_(scriptURL),
       worker_options_(worker_options) {
-  Initialize();
+  Initialize(exception_state);
 }
 
-void DedicatedWorker::Initialize() {
+void DedicatedWorker::Initialize(script::ExceptionState* exception_state) {
   // Algorithm for the Worker constructor.
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#dom-worker
 
   // 1. The user agent may throw a "SecurityError" web::DOMException if the
-  // request
-  //    violates a policy decision (e.g. if the user agent is configured to
-  //    not
-  //    allow the page to start dedicated workers).
+  //    request violates a policy decision (e.g. if the user agent is configured
+  //    to not allow the page to start dedicated workers).
   // 2. Let outside settings be the current settings object.
   // 3. Parse the scriptURL argument relative to outside settings.
   Worker::Options options;
   const GURL& base_url = environment_settings()->base_url();
   options.url = base_url.Resolve(script_url_);
 
-  LOG_IF(WARNING, !options.url.is_valid())
-      << script_url_ << " cannot be resolved based on " << base_url << ".";
-
   // 4. If this fails, throw a "SyntaxError" web::DOMException.
+  if (!options.url.is_valid()) {
+    web::DOMException::Raise(
+        web::DOMException::kSyntaxErr,
+        script_url_ + " cannot be resolved based on " + base_url.spec() + ".",
+        exception_state);
+    return;
+  }
+
   // 5. Let worker URL be the resulting URL record.
   options.web_options.stack_size = cobalt::browser::kWorkerStackSize;
+  options.web_options.web_settings =
+      environment_settings()->context()->web_settings();
   options.web_options.network_module =
       environment_settings()->context()->network_module();
   // 6. Let worker be a new Worker object.
@@ -77,12 +84,23 @@
   // 9. Run this step in parallel:
   //    1. Run a worker given worker, worker URL, outside settings, outside
   //    port, and options.
-  options.outside_settings = environment_settings();
+  options.outside_context = environment_settings()->context();
   options.outside_port = outside_port_.get();
   options.options = worker_options_;
   options.web_options.service_worker_jobs =
-      options.outside_settings->context()->service_worker_jobs();
-
+      options.outside_context->service_worker_jobs();
+  // Store the current source location as the construction location, to be used
+  // in the error event if worker loading of initialization fails.
+  auto stack_trace =
+      options.outside_context->global_environment()->GetStackTrace(0);
+  if (stack_trace.size() > 0) {
+    options.construction_location = base::SourceLocation(
+        stack_trace[0].source_url, stack_trace[0].line_number,
+        stack_trace[0].column_number);
+  } else {
+    options.construction_location.file_path =
+        environment_settings()->creation_url().spec();
+  }
   worker_.reset(new Worker(kDedicatedWorkerName, options));
   // 10. Return worker.
 }
diff --git a/cobalt/worker/dedicated_worker.h b/cobalt/worker/dedicated_worker.h
index b1264d8..9a88582 100644
--- a/cobalt/worker/dedicated_worker.h
+++ b/cobalt/worker/dedicated_worker.h
@@ -35,9 +35,11 @@
 class DedicatedWorker : public AbstractWorker, public web::EventTarget {
  public:
   DedicatedWorker(script::EnvironmentSettings* settings,
-                  const std::string& scriptURL);
+                  const std::string& scriptURL,
+                  script::ExceptionState* exception_state);
   DedicatedWorker(script::EnvironmentSettings* settings,
-                  const std::string& scriptURL, const WorkerOptions& options);
+                  const std::string& scriptURL, const WorkerOptions& options,
+                  script::ExceptionState* exception_state);
   DedicatedWorker(const DedicatedWorker&) = delete;
   DedicatedWorker& operator=(const DedicatedWorker&) = delete;
 
@@ -78,7 +80,7 @@
 
  private:
   ~DedicatedWorker() override;
-  void Initialize();
+  void Initialize(script::ExceptionState* exception_state);
 
   const std::string script_url_;
   const WorkerOptions worker_options_;
diff --git a/cobalt/worker/dedicated_worker_global_scope.cc b/cobalt/worker/dedicated_worker_global_scope.cc
index a03e8a0..7043148 100644
--- a/cobalt/worker/dedicated_worker_global_scope.cc
+++ b/cobalt/worker/dedicated_worker_global_scope.cc
@@ -30,9 +30,9 @@
     : WorkerGlobalScope(settings), cross_origin_isolated_capability_(false) {
   // Algorithm for 'run a worker'
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
-  //     14.9. If is shared is false and owner's cross-origin isolated
-  //     capability is false, then set worker global scope's cross-origin
-  //     isolated capability to false.
+  // 14.9. If is shared is false and owner's cross-origin isolated
+  //       capability is false, then set worker global scope's cross-origin
+  //       isolated capability to false.
   if (!parent_cross_origin_isolated_capability) {
     cross_origin_isolated_capability_ = false;
   }
@@ -41,32 +41,40 @@
 void DedicatedWorkerGlobalScope::Initialize() {
   // Algorithm for 'run a worker'
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
-  //     14.4. Initialize worker global scope's policy container given worker
-  //        global scope, response, and inside settings.
-  //     14.5. If the Run CSP initialization for a global object algorithm
-  //     returns
-  //        "Blocked" when executed upon worker global scope, set response to a
-  //        network error. [CSP]
+  // 14.4. Initialize worker global scope's policy container given worker
+  //       global scope, response, and inside settings.
+  InitializePolicyContainer();
+  // 14.5. If the Run CSP initialization for a global object algorithm
+  //       returns "Blocked" when executed upon worker global scope, set
+  //       response to a network error. [CSP]
 
-  //     14.6. If worker global scope's embedder policy's value is compatible
-  //     with
-  //        cross-origin isolation and is shared is true, then set agent's agent
-  //        cluster's cross-origin isolation mode to "logical" or "concrete".
-  //        The one chosen is implementation-defined.
-  //     No need for dedicated worker.
+  // Note: This is a no-op here:
+  // The "Run CSP initialization for a global object algorithm"
+  //   https://www.w3.org/TR/2023/WD-CSP3-20230110/#run-global-object-csp-initialization
+  // Only returns "Blocked" if a "directive’s initialization algorithm on
+  // global" returns "Blocked".
+  // And "directive’s initialization algorithm on global" is this:
+  //   https://www.w3.org/TR/2023/WD-CSP3-20230110/#directive-initialization
+  // Unless otherwise specified, it has no effect and it returns "Allowed".
 
-  //     14.7. If the result of checking a global object's embedder policy with
-  //        worker global scope, outside settings, and response is false, then
-  //        set response to a network error.
-  //     14.8. Set worker global scope's cross-origin isolated capability to
-  //     true
-  //        if agent's agent cluster's cross-origin isolation mode is
-  //        "concrete".
+  // 14.6. If worker global scope's embedder policy's value is compatible with
+  //       cross-origin isolation and is shared is true, then set agent's agent
+  //       cluster's cross-origin isolation mode to "logical" or "concrete".
+  //       The one chosen is implementation-defined.
+  // No need for dedicated worker.
 
-  //     14.10. If is shared is false and response's url's scheme is "data",
-  //     then
-  //         set worker global scope's cross-origin isolated capability to
-  //         false.
+  // 14.7. If the result of checking a global object's embedder policy with
+  //       worker global scope, outside settings, and response is false, then
+  //       set response to a network error.
+  // Cobalt does not implement embedder policy.
+  // 14.8. Set worker global scope's cross-origin isolated capability to true
+  //       if agent's agent cluster's cross-origin isolation mode is
+  //       "concrete".
+  // Cobalt does not implement "concrete" cross-origin isolation mode.
+
+  // 14.10. If is shared is false and response's url's scheme is "data", then
+  //        set worker global scope's cross-origin isolated capability to
+  //        false.
   if (!Url().SchemeIs("data")) {
     cross_origin_isolated_capability_ = false;
   }
@@ -79,6 +87,27 @@
       ->PostMessage(message);
 }
 
+void DedicatedWorkerGlobalScope::InitializePolicyContainer() {
+  // Algorithm for Initialize a worker global scope's policy container
+  //    https://html.spec.whatwg.org/commit-snapshots/814668ef2d1919a2a9387a0b29ebc6df7748fa80/#initialize-worker-policy-container
+  // 1. If workerGlobalScope's url is local but its scheme is not "blob":
+  //    URL: https://fetch.spec.whatwg.org/#local-scheme: A local scheme is
+  //    "about", "blob", or "data".
+  if (Url().SchemeIs(url::kAboutScheme) || Url().SchemeIs(url::kDataScheme)) {
+    // 1.1. Assert: workerGlobalScope's owner set's size is 1.
+    DCHECK(owner_set()->size() == 1);
+    // 1.2. Set workerGlobalScope's policy container to a clone of
+    //      workerGlobalScope's owner set[0]'s relevant settings object's
+    //      policy container.
+    auto* owner = *owner_set()->begin();
+    DCHECK(owner->csp_delegate()->GetPolicyContainer());
+    csp_delegate()->ClonePolicyContainer(
+        *owner->csp_delegate()->GetPolicyContainer());
+  }
+  // 2. Otherwise, set workerGlobalScope's policy container to the result
+  //    of creating a policy container from a fetch response given response
+  //    and environment.
+}
 
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/dedicated_worker_global_scope.h b/cobalt/worker/dedicated_worker_global_scope.h
index 3793608..4252b4f 100644
--- a/cobalt/worker/dedicated_worker_global_scope.h
+++ b/cobalt/worker/dedicated_worker_global_scope.h
@@ -79,6 +79,8 @@
   ~DedicatedWorkerGlobalScope() override {}
 
  private:
+  void InitializePolicyContainer();
+
   bool cross_origin_isolated_capability_;
 
   std::string name_;
diff --git a/cobalt/worker/fetch_event.cc b/cobalt/worker/fetch_event.cc
index 4480643..82e0927 100644
--- a/cobalt/worker/fetch_event.cc
+++ b/cobalt/worker/fetch_event.cc
@@ -28,22 +28,19 @@
 FetchEvent::FetchEvent(script::EnvironmentSettings* environment_settings,
                        const std::string& type,
                        const FetchEventInit& event_init_dict)
-    : FetchEvent(
-          environment_settings, base::Token(type), event_init_dict,
-          /*respond_with_callback=*/std::make_unique<RespondWithCallback>(),
-          /*report_load_timing_info=*/
-          std::make_unique<ReportLoadTimingInfo>()) {}
+    : FetchEvent(environment_settings, base::Token(type), event_init_dict,
+                 RespondWithCallback(), ReportLoadTimingInfo()) {}
 
-FetchEvent::FetchEvent(
-    script::EnvironmentSettings* environment_settings, base::Token type,
-    const FetchEventInit& event_init_dict,
-    std::unique_ptr<RespondWithCallback> respond_with_callback,
-    std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info)
+FetchEvent::FetchEvent(script::EnvironmentSettings* environment_settings,
+                       base::Token type, const FetchEventInit& event_init_dict,
+                       RespondWithCallback respond_with_callback,
+                       ReportLoadTimingInfo report_load_timing_info)
     : ExtendableEvent(type, event_init_dict),
+      environment_settings_(environment_settings),
       respond_with_callback_(std::move(respond_with_callback)),
       report_load_timing_info_(std::move(report_load_timing_info)) {
   auto script_value_factory =
-      web::get_script_value_factory(environment_settings);
+      web::get_script_value_factory(environment_settings_);
   handled_property_ = std::make_unique<script::ValuePromiseVoid::Reference>(
       this, script_value_factory->CreateBasicPromise<void>());
   request_ = std::make_unique<script::ValueHandleHolder::Reference>(
@@ -56,112 +53,73 @@
   load_timing_info_.service_worker_start_time = base::TimeTicks::Now();
 }
 
-void FetchEvent::RespondWith(
-    script::EnvironmentSettings* environment_settings,
-    std::unique_ptr<script::Promise<script::ValueHandle*>>& response) {
-  respond_with_called_ = true;
-
-  // TODO: call |WaitUntil()|.
-  v8::Local<v8::Promise> v8_response = response->promise();
-  auto* global_environment = web::get_global_environment(environment_settings);
-  auto* isolate = global_environment->isolate();
-  auto context = isolate->GetCurrentContext();
-  auto data = v8::Object::New(isolate);
-  web::cache_utils::SetExternal(context, data, "environment_settings",
-                                environment_settings);
-  web::cache_utils::SetOwnedExternal(context, data, "respond_with_callback",
-                                     std::move(respond_with_callback_));
-  web::cache_utils::SetOwnedExternal(context, data, "report_load_timing_info",
-                                     std::move(report_load_timing_info_));
-  web::cache_utils::SetExternal(context, data, "load_timing_info",
-                                &load_timing_info_);
-  web::cache_utils::SetExternal(context, data, "handled",
-                                handled_property_.get());
-  auto result = v8_response->Then(
-      context,
-      v8::Function::New(
-          context,
-          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
-            auto* isolate = info.GetIsolate();
-            auto context = info.GetIsolate()->GetCurrentContext();
-            v8::Local<v8::Value> text_result;
-            if (!web::cache_utils::TryCall(context, /*object=*/info[0], "text")
-                     .ToLocal(&text_result)) {
-              auto respond_with_callback =
-                  web::cache_utils::GetOwnedExternal<RespondWithCallback>(
-                      context, info.Data(), "respond_with_callback");
-              std::move(*respond_with_callback)
-                  .Run(std::make_unique<std::string>());
-              return;
-            }
-            auto* load_timing_info =
-                web::cache_utils::GetExternal<net::LoadTimingInfo>(
-                    context, info.Data(), "load_timing_info");
-            load_timing_info->receive_headers_end = base::TimeTicks::Now();
-            auto result = text_result.As<v8::Promise>()->Then(
-                context,
-                v8::Function::New(
-                    context,
-                    [](const v8::FunctionCallbackInfo<v8::Value>& info) {
-                      auto* isolate = info.GetIsolate();
-                      auto context = info.GetIsolate()->GetCurrentContext();
-                      auto* handled = web::cache_utils::GetExternal<
-                          script::ValuePromiseVoid::Reference>(
-                          context, info.Data(), "handled");
-                      handled->value().Resolve();
-                      auto respond_with_callback =
-                          web::cache_utils::GetOwnedExternal<
-                              RespondWithCallback>(context, info.Data(),
-                                                   "respond_with_callback");
-                      auto body = std::make_unique<std::string>();
-                      FromJSValue(isolate, info[0],
-                                  script::v8c::kNoConversionFlags, nullptr,
-                                  body.get());
-                      auto* load_timing_info =
-                          web::cache_utils::GetExternal<net::LoadTimingInfo>(
-                              context, info.Data(), "load_timing_info");
-                      auto report_load_timing_info =
-                          web::cache_utils::GetOwnedExternal<
-                              ReportLoadTimingInfo>(context, info.Data(),
-                                                    "report_load_timing_info");
-                      std::move(*report_load_timing_info)
-                          .Run(*load_timing_info);
-                      auto* environment_settings =
-                          web::cache_utils::GetExternal<
-                              script::EnvironmentSettings>(
-                              context, info.Data(), "environment_settings");
-                      web::get_context(environment_settings)
-                          ->network_module()
-                          ->task_runner()
-                          ->PostTask(
-                              FROM_HERE,
-                              base::BindOnce(
-                                  [](std::unique_ptr<base::OnceCallback<void(
-                                         std::unique_ptr<std::string>)>>
-                                         respond_with_callback,
-                                     std::unique_ptr<std::string> body) {
-                                    std::move(*respond_with_callback)
-                                        .Run(std::move(body));
-                                  },
-                                  std::move(respond_with_callback),
-                                  std::move(body)));
-                    },
-                    info.Data())
-                    .ToLocalChecked());
-            if (result.IsEmpty()) {
-              LOG(WARNING) << "Failure during FetchEvent respondWith handling. "
-                              "Retrieving Response text failed.";
-            }
-          },
-          data)
-          .ToLocalChecked());
-  if (result.IsEmpty()) {
-    LOG(WARNING) << "Failure during FetchEvent respondWith handling.";
-  }
+base::Optional<v8::Local<v8::Promise>> FetchEvent::GetText(
+    v8::Local<v8::Promise> response_promise) {
+  std::move(report_load_timing_info_).Run(load_timing_info_);
+  handled_property_->value().Resolve();
+  return web::cache_utils::OptionalPromise(
+      web::cache_utils::Call(response_promise->Result(), "text"));
 }
 
-script::HandlePromiseVoid FetchEvent::handled(
-    script::EnvironmentSettings* environment_settings) {
+base::Optional<v8::Local<v8::Promise>> FetchEvent::DoRespondWith(
+    v8::Local<v8::Promise> text_promise) {
+  auto* isolate = text_promise->GetIsolate();
+  auto context = isolate->GetCurrentContext();
+  auto resolver = v8::Promise::Resolver::New(context);
+  if (resolver.IsEmpty()) {
+    return base::nullopt;
+  }
+  auto body = web::cache_utils::FromV8String(text_promise->GetIsolate(),
+                                             text_promise->Result());
+  auto callback = base::BindOnce(
+      [](v8::Local<v8::Context> context,
+         v8::Local<v8::Promise::Resolver> resolver) {
+        DCHECK(resolver->Resolve(context, v8::Undefined(resolver->GetIsolate()))
+                   .FromMaybe(false));
+      },
+      context, resolver.ToLocalChecked());
+  web::get_context(environment_settings_)
+      ->network_module()
+      ->task_runner()
+      ->PostTask(
+          FROM_HERE,
+          base::BindOnce(
+              [](RespondWithCallback respond_with_callback, std::string body,
+                 base::MessageLoop* loop, base::OnceClosure callback) {
+                std::move(respond_with_callback)
+                    .Run(std::make_unique<std::string>(std::move(body)));
+                loop->task_runner()->PostTask(FROM_HERE, std::move(callback));
+              },
+              std::move(respond_with_callback_), std::move(body),
+              base::MessageLoop::current(), std::move(callback)));
+  return resolver.ToLocalChecked()->GetPromise();
+}
+
+void FetchEvent::RespondWith(
+    std::unique_ptr<script::Promise<script::ValueHandle*>>& response,
+    script::ExceptionState* exception_state) {
+  respond_with_called_ = true;
+
+  auto text_promise = web::cache_utils::Then(
+      response->promise(),
+      base::BindOnce(&FetchEvent::GetText, base::Unretained(this)));
+  if (!text_promise) {
+    return;
+  }
+  auto done_promise = web::cache_utils::Then(
+      text_promise.value(),
+      base::BindOnce(&FetchEvent::DoRespondWith, base::Unretained(this)));
+  if (!done_promise) {
+    return;
+  }
+  auto* isolate = response->promise()->GetIsolate();
+  std::unique_ptr<script::Promise<script::ValueHandle*>> wait_promise;
+  script::v8c::FromJSValue(isolate, done_promise.value(), 0, exception_state,
+                           &wait_promise);
+  WaitUntil(environment_settings_, wait_promise, exception_state);
+}
+
+script::HandlePromiseVoid FetchEvent::handled() {
   return script::HandlePromiseVoid(handled_property_->referenced_value());
 }
 
diff --git a/cobalt/worker/fetch_event.h b/cobalt/worker/fetch_event.h
index ac3ff69..3b9aa46 100644
--- a/cobalt/worker/fetch_event.h
+++ b/cobalt/worker/fetch_event.h
@@ -41,14 +41,14 @@
              const FetchEventInit& event_init_dict);
   FetchEvent(script::EnvironmentSettings*, base::Token type,
              const FetchEventInit& event_init_dict,
-             std::unique_ptr<RespondWithCallback> respond_with_callback,
-             std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info);
+             RespondWithCallback respond_with_callback,
+             ReportLoadTimingInfo report_load_timing_info);
   ~FetchEvent() override = default;
 
   void RespondWith(
-      script::EnvironmentSettings*,
-      std::unique_ptr<script::Promise<script::ValueHandle*>>& response);
-  script::HandlePromiseVoid handled(script::EnvironmentSettings*);
+      std::unique_ptr<script::Promise<script::ValueHandle*>>& response,
+      script::ExceptionState* exception_state);
+  script::HandlePromiseVoid handled();
 
   const script::ValueHandleHolder* request() {
     return &(request_->referenced_value());
@@ -59,8 +59,14 @@
   DEFINE_WRAPPABLE_TYPE(FetchEvent);
 
  private:
-  std::unique_ptr<RespondWithCallback> respond_with_callback_;
-  std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info_;
+  base::Optional<v8::Local<v8::Promise>> GetText(
+      v8::Local<v8::Promise> response_promise);
+  base::Optional<v8::Local<v8::Promise>> DoRespondWith(
+      v8::Local<v8::Promise> text_promise);
+
+  script::EnvironmentSettings* environment_settings_;
+  RespondWithCallback respond_with_callback_;
+  ReportLoadTimingInfo report_load_timing_info_;
   std::unique_ptr<script::ValueHandleHolder::Reference> request_;
   std::unique_ptr<script::ValuePromiseVoid::Reference> handled_property_;
   bool respond_with_called_ = false;
diff --git a/cobalt/worker/fetch_event.idl b/cobalt/worker/fetch_event.idl
index 04253c3..072a625 100644
--- a/cobalt/worker/fetch_event.idl
+++ b/cobalt/worker/fetch_event.idl
@@ -24,6 +24,6 @@
 
 
   // Takes a |Promise<any>| because |Response| is polyfilled.
-  [CallWith=EnvironmentSettings] void respondWith(Promise<any> response);
-  [CallWith=EnvironmentSettings] readonly attribute Promise<void> handled;
+  [RaisesException] void respondWith(Promise<any> response);
+  readonly attribute Promise<void> handled;
 };
diff --git a/cobalt/worker/service_worker.cc b/cobalt/worker/service_worker.cc
index 6eb5597..bc67613 100644
--- a/cobalt/worker/service_worker.cc
+++ b/cobalt/worker/service_worker.cc
@@ -33,9 +33,7 @@
 
 ServiceWorker::ServiceWorker(script::EnvironmentSettings* settings,
                              worker::ServiceWorkerObject* worker)
-    : web::EventTarget(settings),
-      worker_(worker),
-      state_(kServiceWorkerStateParsed) {}
+    : web::EventTarget(settings), worker_(worker) {}
 
 void ServiceWorker::PostMessage(const script::ValueHandleHolder& message) {
   // Algorithm for ServiceWorker.postMessage():
@@ -55,8 +53,7 @@
     return;
   }
   // 5. If the result of running the Should Skip Event algorithm with
-  // "message"
-  //    and serviceWorker is true, then return.
+  // "message" and serviceWorker is true, then return.
   if (service_worker->ShouldSkipEvent(base::Tokens::message())) return;
   // 6. Run these substeps in parallel:
   ServiceWorkerJobs* jobs =
@@ -66,7 +63,7 @@
       FROM_HERE,
       base::BindOnce(&ServiceWorkerJobs::ServiceWorkerPostMessageSubSteps,
                      base::Unretained(jobs), base::Unretained(service_worker),
-                     base::Unretained(incumbent_settings),
+                     base::Unretained(incumbent_settings->context()),
                      std::move(serialize_result)));
 }
 
diff --git a/cobalt/worker/service_worker.h b/cobalt/worker/service_worker.h
index 07d4df5..898a6e5 100644
--- a/cobalt/worker/service_worker.h
+++ b/cobalt/worker/service_worker.h
@@ -51,8 +51,12 @@
   std::string script_url() const { return worker_->script_url().spec(); }
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworker-state
-  void set_state(ServiceWorkerState state) { state_ = state; }
-  ServiceWorkerState state() const { return state_; }
+  void set_state(ServiceWorkerState state) {
+    if (worker_) worker_->set_state(state);
+  }
+  ServiceWorkerState state() const {
+    return worker_ ? worker_->state() : kServiceWorkerStateParsed;
+  }
 
   const EventListenerScriptValue* onstatechange() const {
     return GetAttributeEventListener(base::Tokens::statechange());
@@ -80,7 +84,6 @@
   ~ServiceWorker() override { worker_ = nullptr; }
 
   scoped_refptr<ServiceWorkerObject> worker_;
-  ServiceWorkerState state_;
 };
 
 }  // namespace worker
diff --git a/cobalt/worker/service_worker_container.cc b/cobalt/worker/service_worker_container.cc
index 97677b2..05b953e 100644
--- a/cobalt/worker/service_worker_container.cc
+++ b/cobalt/worker/service_worker_container.cc
@@ -77,8 +77,8 @@
   // 3. If readyPromise is pending, run the following substeps in parallel:
   if (ready_promise->State() == script::PromiseState::kPending) {
     //    3.1. Let client by this's service worker client.
-    web::EnvironmentSettings* client = environment_settings();
-    worker::ServiceWorkerJobs* jobs = client->context()->service_worker_jobs();
+    web::Context* client = environment_settings()->context();
+    worker::ServiceWorkerJobs* jobs = client->service_worker_jobs();
     jobs->message_loop()->task_runner()->PostTask(
         FROM_HERE,
         base::BindOnce(&ServiceWorkerJobs::MaybeResolveReadyPromiseSubSteps,
@@ -135,7 +135,7 @@
       new script::ValuePromiseWrappable::Reference(this, promise));
 
   // 2. Let client be this's service worker client.
-  web::EnvironmentSettings* client = environment_settings();
+  web::Context* client = environment_settings()->context();
   // 3. Let scriptURL be the result of parsing scriptURL with this's
   // relevant settings object’s API base URL.
   const GURL& base_url = environment_settings()->base_url();
@@ -152,9 +152,9 @@
   base::MessageLoop::current()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&ServiceWorkerJobs::StartRegister,
-                     base::Unretained(client->context()->service_worker_jobs()),
-                     scope_url, script_url, std::move(promise_reference),
-                     client, options.type(), options.update_via_cache()));
+                     base::Unretained(client->service_worker_jobs()), scope_url,
+                     script_url, std::move(promise_reference), client,
+                     options.type(), options.update_via_cache()));
   // 7. Return p.
   return promise;
 }
@@ -197,11 +197,11 @@
   // Algorithm for 'ServiceWorkerContainer.getRegistration()':
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
   // 1. Let client be this's service worker client.
-  web::EnvironmentSettings* client = environment_settings();
+  web::Context* client = environment_settings()->context();
 
   // 2. Let storage key be the result of running obtain a storage key given
   //    client.
-  url::Origin storage_key = client->ObtainStorageKey();
+  url::Origin storage_key = client->environment_settings()->ObtainStorageKey();
 
   // 3. Let clientURL be the result of parsing clientURL with this's relevant
   //    settings object’s API base URL.
@@ -232,7 +232,7 @@
 
   // 7. Let promise be a new promise.
   // 8. Run the following substeps in parallel:
-  worker::ServiceWorkerJobs* jobs = client->context()->service_worker_jobs();
+  worker::ServiceWorkerJobs* jobs = client->service_worker_jobs();
   DCHECK(jobs);
   jobs->message_loop()->task_runner()->PostTask(
       FROM_HERE, base::BindOnce(&ServiceWorkerJobs::GetRegistrationSubSteps,
@@ -260,7 +260,7 @@
 void ServiceWorkerContainer::GetRegistrationsTask(
     std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
         promise_reference) {
-  auto* client = environment_settings();
+  auto* client = environment_settings()->context();
   // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistrations
   worker::ServiceWorkerJobs* jobs =
       environment_settings()->context()->service_worker_jobs();
diff --git a/cobalt/worker/service_worker_container.h b/cobalt/worker/service_worker_container.h
index aca45b6..94679d6 100644
--- a/cobalt/worker/service_worker_container.h
+++ b/cobalt/worker/service_worker_container.h
@@ -76,8 +76,6 @@
       std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
           promise_reference);
 
-  scoped_refptr<ServiceWorker> ready_;
-
   script::HandlePromiseWrappable ready_promise_;
   std::unique_ptr<script::ValuePromiseWrappable::Reference> promise_reference_;
 };
diff --git a/cobalt/worker/service_worker_global_scope.cc b/cobalt/worker/service_worker_global_scope.cc
index a1fd89c..c357b15 100644
--- a/cobalt/worker/service_worker_global_scope.cc
+++ b/cobalt/worker/service_worker_global_scope.cc
@@ -70,26 +70,29 @@
             DCHECK(service_worker);
             // 3. Let url be request’s url.
             DCHECK(url.is_valid());
-            std::string* resource = service_worker->LookupScriptResource(url);
+            const ScriptResource* script_resource =
+                service_worker->LookupScriptResource(url);
+            std::string* resource_content =
+                script_resource ? script_resource->content.get() : nullptr;
             // 4. If serviceWorker’s state is not "parsed" or "installing":
             if (service_worker->state() != kServiceWorkerStateParsed &&
                 service_worker->state() != kServiceWorkerStateInstalling) {
               // 4.1. Return map[url] if it exists and a network error
               // otherwise.
-              if (!resource) {
+              if (!resource_content) {
                 web::DOMException::Raise(web::DOMException::kNetworkErr,
                                          exception_state);
               }
-              return resource;
+              return resource_content;
             }
 
             // 5. If map[url] exists:
-            if (resource) {
+            if (resource_content) {
               // 5.1. Append url to serviceWorker’s set of used scripts.
               service_worker->AppendToSetOfUsedScripts(url);
 
               // 5.2. Return map[url].
-              return resource;
+              return resource_content;
             }
 
             // 6. Let registration be serviceWorker’s containing service worker
@@ -106,7 +109,7 @@
             // a separate callback that gets passed to the ScriptLoader for the
             // FetcherFactory.
 
-            return resource;
+            return resource_content;
           },
           service_worker_object_),
       base::Bind(
@@ -188,11 +191,10 @@
 
 void ServiceWorkerGlobalScope::StartFetch(
     const GURL& url,
-    std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
-        callback,
-    std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+    base::OnceCallback<void(std::unique_ptr<std::string>)> callback,
+    base::OnceCallback<void(const net::LoadTimingInfo&)>
         report_load_timing_info,
-    std::unique_ptr<base::OnceClosure> fallback) {
+    base::OnceClosure fallback) {
   if (base::MessageLoop::current() !=
       environment_settings()->context()->message_loop()) {
     environment_settings()->context()->message_loop()->task_runner()->PostTask(
@@ -204,11 +206,11 @@
     return;
   }
   if (!service_worker()) {
-    std::move(*fallback).Run();
+    std::move(fallback).Run();
     return;
   }
   // TODO: handle the following steps in
-  //       https://w3c.github.io/ServiceWorker/#handle-fetch.
+  //       https://www.w3.org/TR/2022/CRD-service-workers-20220712/#handle-fetch.
   // 22. If activeWorker’s state is "activating", wait for activeWorker’s state
   //     to become "activated".
   // 23. If the result of running the Run Service Worker algorithm with
@@ -217,14 +219,15 @@
   auto* global_environment = get_global_environment(environment_settings());
   auto* isolate = global_environment->isolate();
   script::v8c::EntryScope entry_scope(isolate);
-  auto request =
-      web::cache_utils::CreateRequest(environment_settings(), url.spec());
+  auto request = web::cache_utils::CreateRequest(isolate, url.spec());
   if (!request) {
-    std::move(*fallback).Run();
+    std::move(fallback).Run();
     return;
   }
+
   FetchEventInit event_init;
-  event_init.set_request(request.value().GetScriptValue());
+  event_init.set_request(
+      web::cache_utils::FromV8Value(isolate, request.value()).GetScriptValue());
   scoped_refptr<FetchEvent> fetch_event =
       new FetchEvent(environment_settings(), base::Tokens::fetch(), event_init,
                      std::move(callback), std::move(report_load_timing_info));
@@ -232,7 +235,7 @@
   DispatchEvent(fetch_event);
   // TODO: implement steps 25 and 26.
   if (!fetch_event->respond_with_called()) {
-    std::move(*fallback).Run();
+    std::move(fallback).Run();
   }
 }
 
diff --git a/cobalt/worker/service_worker_global_scope.h b/cobalt/worker/service_worker_global_scope.h
index f89606c..3e06e53 100644
--- a/cobalt/worker/service_worker_global_scope.h
+++ b/cobalt/worker/service_worker_global_scope.h
@@ -73,11 +73,10 @@
 
   void StartFetch(
       const GURL& url,
-      std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
-          callback,
-      std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+      base::OnceCallback<void(std::unique_ptr<std::string>)> callback,
+      base::OnceCallback<void(const net::LoadTimingInfo&)>
           report_load_timing_info,
-      std::unique_ptr<base::OnceClosure> fallback) override;
+      base::OnceClosure fallback) override;
 
   const web::EventTargetListenerInfo::EventListenerScriptValue* oninstall() {
     return GetAttributeEventListener(base::Tokens::install());
diff --git a/cobalt/worker/service_worker_jobs.cc b/cobalt/worker/service_worker_jobs.cc
index b6da4d4..98f0488 100644
--- a/cobalt/worker/service_worker_jobs.cc
+++ b/cobalt/worker/service_worker_jobs.cc
@@ -64,6 +64,7 @@
 #include "url/gurl.h"
 #include "url/origin.h"
 
+
 namespace cobalt {
 namespace worker {
 
@@ -112,17 +113,24 @@
 
   // 8. If origin has been configured as a trustworthy origin, return
   // "Potentially Trustworthy".
+  if (origin.host() == "web-platform.test") {
+    return true;
+  }
 
   // 9. Return "Not Trustworthy".
   return false;
 }
 
-bool PermitAnyURL(const GURL&, bool) { return true; }
+bool PermitAnyNonRedirectedURL(const GURL&, bool did_redirect) {
+  return !did_redirect;
+}
 }  // namespace
 
-ServiceWorkerJobs::ServiceWorkerJobs(network::NetworkModule* network_module,
+ServiceWorkerJobs::ServiceWorkerJobs(web::WebSettings* web_settings,
+                                     network::NetworkModule* network_module,
+                                     web::UserAgentPlatformInfo* platform_info,
                                      base::MessageLoop* message_loop)
-    : network_module_(network_module), message_loop_(message_loop) {
+    : message_loop_(message_loop) {
   DCHECK_EQ(message_loop_, base::MessageLoop::current());
   fetcher_factory_.reset(new loader::FetcherFactory(network_module));
   DCHECK(fetcher_factory_);
@@ -130,12 +138,18 @@
   script_loader_factory_.reset(new loader::ScriptLoaderFactory(
       "ServiceWorkerJobs", fetcher_factory_.get()));
   DCHECK(script_loader_factory_);
+
+  ServiceWorkerPersistentSettings::Options options(web_settings, network_module,
+                                                   platform_info, this);
+  scope_to_registration_map_.reset(new ServiceWorkerRegistrationMap(options));
+  DCHECK(scope_to_registration_map_);
 }
 
 ServiceWorkerJobs::~ServiceWorkerJobs() {
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
-  scope_to_registration_map_.HandleUserAgentShutdown(this);
-  scope_to_registration_map_.AbortAllActive();
+  scope_to_registration_map_->HandleUserAgentShutdown(this);
+  scope_to_registration_map_->AbortAllActive();
+  scope_to_registration_map_.reset();
   while (!web_context_registrations_.empty()) {
     // Wait for web context registrations to be cleared.
     web_context_registrations_cleared_.Wait();
@@ -152,17 +166,17 @@
     const base::Optional<GURL>& maybe_scope_url,
     const GURL& script_url_with_fragment,
     std::unique_ptr<script::ValuePromiseWrappable::Reference> promise_reference,
-    web::EnvironmentSettings* client, const WorkerType& type,
+    web::Context* client, const WorkerType& type,
     const ServiceWorkerUpdateViaCache& update_via_cache) {
   TRACE_EVENT2("cobalt::worker", "ServiceWorkerJobs::StartRegister()", "scope",
                maybe_scope_url.value_or(GURL()).spec(), "script",
                script_url_with_fragment.spec());
   DCHECK_NE(message_loop(), base::MessageLoop::current());
-  DCHECK_EQ(client->context()->message_loop(), base::MessageLoop::current());
+  DCHECK_EQ(client->message_loop(), base::MessageLoop::current());
   // Algorithm for Start Register:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#start-register-algorithm
   // 1. If scriptURL is failure, reject promise with a TypeError and abort these
-  // steps.
+  //    steps.
   if (script_url_with_fragment.is_empty()) {
     promise_reference->value().Reject(script::kTypeError);
     return;
@@ -176,26 +190,42 @@
   DCHECK(!script_url.is_empty());
 
   // 3. If scriptURL’s scheme is not one of "http" and "https", reject promise
-  // with a TypeError and abort these steps.
+  //    with a TypeError and abort these steps.
   if (!script_url.SchemeIsHTTPOrHTTPS()) {
     promise_reference->value().Reject(script::kTypeError);
     return;
   }
 
   // 4. If any of the strings in scriptURL’s path contains either ASCII
-  // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise with
-  // a TypeError and abort these steps.
+  //    case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise
+  //    with a TypeError and abort these steps.
   if (PathContainsEscapedSlash(script_url)) {
     promise_reference->value().Reject(script::kTypeError);
     return;
   }
 
+  DCHECK(client);
+  web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+      client->GetWindowOrWorkerGlobalScope();
+  DCHECK(window_or_worker_global_scope);
+  web::CspDelegate* csp_delegate =
+      window_or_worker_global_scope->csp_delegate();
+  DCHECK(csp_delegate);
+  if (!csp_delegate->CanLoad(web::CspDelegate::kWorker, script_url,
+                             /* did_redirect*/ false)) {
+    promise_reference->value().Reject(new web::DOMException(
+        web::DOMException::kSecurityErr,
+        "Failed to register a ServiceWorker: The provided scriptURL ('" +
+            script_url.spec() + "') violates the Content Security Policy."));
+    return;
+  }
+
   // 5. If scopeURL is null, set scopeURL to the result of parsing the string
-  // "./" with scriptURL.
+  //    "./" with scriptURL.
   GURL scope_url = maybe_scope_url.value_or(script_url.Resolve("./"));
 
   // 6. If scopeURL is failure, reject promise with a TypeError and abort these
-  // steps.
+  //    steps.
   if (scope_url.is_empty()) {
     promise_reference->value().Reject(script::kTypeError);
     return;
@@ -207,26 +237,26 @@
   DCHECK(!scope_url.is_empty());
 
   // 8. If scopeURL’s scheme is not one of "http" and "https", reject promise
-  // with a TypeError and abort these steps.
+  //    with a TypeError and abort these steps.
   if (!scope_url.SchemeIsHTTPOrHTTPS()) {
     promise_reference->value().Reject(script::kTypeError);
     return;
   }
 
   // 9. If any of the strings in scopeURL’s path contains either ASCII
-  // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise with
-  // a TypeError and abort these steps.
+  //    case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise
+  //    with a TypeError and abort these steps.
   if (PathContainsEscapedSlash(scope_url)) {
     promise_reference->value().Reject(script::kTypeError);
     return;
   }
 
   // 10. Let storage key be the result of running obtain a storage key given
-  // client.
-  url::Origin storage_key = client->ObtainStorageKey();
+  //     client.
+  url::Origin storage_key = client->environment_settings()->ObtainStorageKey();
 
   // 11. Let job be the result of running Create Job with register, storage key,
-  // scopeURL, scriptURL, promise, and client.
+  //     scopeURL, scriptURL, promise, and client.
   std::unique_ptr<Job> job =
       CreateJob(kRegister, storage_key, scope_url, script_url,
                 JobPromiseType::Create(std::move(promise_reference)), client);
@@ -242,9 +272,7 @@
   // This is the same value as set in CreateJob().
 
   // 15. Invoke Schedule Job with job.
-  message_loop()->task_runner()->PostTask(
-      FROM_HERE, base::BindOnce(&ServiceWorkerJobs::ScheduleJob,
-                                base::Unretained(this), std::move(job)));
+  ScheduleJob(std::move(job));
   DCHECK(!job.get());
 }
 
@@ -261,7 +289,7 @@
 std::unique_ptr<ServiceWorkerJobs::Job> ServiceWorkerJobs::CreateJob(
     JobType type, const url::Origin& storage_key, const GURL& scope_url,
     const GURL& script_url, std::unique_ptr<JobPromiseType> promise,
-    web::EnvironmentSettings* client) {
+    web::Context* client) {
   TRACE_EVENT2("cobalt::worker", "ServiceWorkerJobs::CreateJob()", "type", type,
                "script_url", script_url.spec());
   // Algorithm for Create Job:
@@ -277,7 +305,7 @@
                                    client, std::move(promise)));
   // 8. If client is not null, set job’s referrer to client’s creation URL.
   if (client) {
-    job->referrer = client->creation_url();
+    job->referrer = client->environment_settings()->creation_url();
   }
   // 9. Return job.
   return job;
@@ -285,8 +313,16 @@
 
 void ServiceWorkerJobs::ScheduleJob(std::unique_ptr<Job> job) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ScheduleJob()");
-  DCHECK_EQ(message_loop(), base::MessageLoop::current());
   DCHECK(job);
+
+  if (base::MessageLoop::current() != message_loop()) {
+    DCHECK(message_loop());
+    message_loop()->task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(&ServiceWorkerJobs::ScheduleJob,
+                                  base::Unretained(this), std::move(job)));
+    return;
+  }
+  DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Schedule Job:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#schedule-job
   // 1. Let jobQueue be null.
@@ -296,21 +332,28 @@
 
   // 3. If scope to job queue map[jobScope] does not exist, set scope to job
   // queue map[jobScope] to a new job queue.
-  if (job_queue_map_.find(job_scope) == job_queue_map_.end()) {
+  auto job_queue_iterator_for_scope = job_queue_map_.find(job_scope);
+  if (job_queue_iterator_for_scope == job_queue_map_.end()) {
     auto insertion = job_queue_map_.emplace(
         job_scope, std::unique_ptr<JobQueue>(new JobQueue()));
     DCHECK(insertion.second);
+    job_queue_iterator_for_scope = insertion.first;
   }
 
   // 4. Set jobQueue to scope to job queue map[jobScope].
-  DCHECK(job_queue_map_.find(job_scope) != job_queue_map_.end());
-  JobQueue* job_queue = job_queue_map_.find(job_scope)->second.get();
+  DCHECK(job_queue_iterator_for_scope != job_queue_map_.end());
+  JobQueue* job_queue = job_queue_iterator_for_scope->second.get();
 
   // 5. If jobQueue is empty, then:
   if (job_queue->empty()) {
     // 5.1. Set job’s containing job queue to jobQueue, and enqueue job to
     // jobQueue.
     job->containing_job_queue = job_queue;
+    if (!IsWebContextRegistered(job->client)) {
+      // Note: The client that requested the job has already exited and isn't
+      // able to handle the promise.
+      job->containing_job_queue->PrepareJobForClientShutdown(job, job->client);
+    }
     job_queue->Enqueue(std::move(job));
 
     // 5.2. Invoke Run Job with jobQueue.
@@ -326,7 +369,7 @@
       // settled, append job to lastJob’s list of equivalent jobs.
       DCHECK(last_job);
       base::AutoLock lock(last_job->equivalent_jobs_promise_mutex);
-      if (EquivalentJobs(job.get(), last_job) && last_job->promise &&
+      if (ReturnJobsAreEquivalent(job.get(), last_job) && last_job->promise &&
           last_job->promise->is_pending()) {
         last_job->equivalent_jobs.push_back(std::move(job));
         return;
@@ -336,12 +379,17 @@
     // 6.3. Else, set job’s containing job queue to jobQueue, and enqueue job to
     // jobQueue.
     job->containing_job_queue = job_queue;
+    if (!IsWebContextRegistered(job->client)) {
+      // Note: The client that requested the job has already exited and isn't
+      // able to handle the promise.
+      job->containing_job_queue->PrepareJobForClientShutdown(job, job->client);
+    }
     job_queue->Enqueue(std::move(job));
   }
   DCHECK(!job);
 }
 
-bool ServiceWorkerJobs::EquivalentJobs(Job* one, Job* two) {
+bool ServiceWorkerJobs::ReturnJobsAreEquivalent(Job* one, Job* two) {
   // Algorithm for Two jobs are equivalent:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-equivalent
   DCHECK(one);
@@ -475,14 +523,15 @@
   // 4. Let registration be the result of running Get Registration given job’s
   // storage key and job’s scope url.
   scoped_refptr<ServiceWorkerRegistrationObject> registration =
-      scope_to_registration_map_.GetRegistration(job->storage_key,
-                                                 job->scope_url);
+      scope_to_registration_map_->GetRegistration(job->storage_key,
+                                                  job->scope_url);
 
   // 5. If registration is not null, then:
   if (registration) {
     // 5.1 Let newestWorker be the result of running the Get Newest Worker
     // algorithm passing registration as the argument.
-    ServiceWorkerObject* newest_worker = registration->GetNewestWorker();
+    const scoped_refptr<ServiceWorkerObject>& newest_worker =
+        registration->GetNewestWorker();
 
     // 5.2 If newestWorker is not null, job’s script url equals newestWorker’s
     // script url, job’s worker type equals newestWorker’s type, and job’s
@@ -501,7 +550,7 @@
 
     // 6.1 Invoke Set Registration algorithm with job’s storage key, job’s scope
     // url, and job’s update via cache mode.
-    registration = scope_to_registration_map_.SetRegistration(
+    registration = scope_to_registration_map_->SetRegistration(
         job->storage_key, job->scope_url, job->update_via_cache);
   }
 
@@ -509,6 +558,43 @@
   Update(job);
 }
 
+void ServiceWorkerJobs::SoftUpdate(
+    scoped_refptr<ServiceWorkerRegistrationObject> registration,
+    bool force_bypass_cache) {
+  TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::SoftUpdate()");
+  DCHECK_EQ(message_loop(), base::MessageLoop::current());
+  DCHECK(registration);
+  // Algorithm for SoftUpdate:
+  //    https://www.w3.org/TR/2022/CRD-service-workers-20220712/#soft-update
+  // 1. Let newestWorker be the result of running Get Newest Worker algorithm
+  // passing registration as its argument.
+  ServiceWorkerObject* newest_worker = registration->GetNewestWorker();
+
+  // 2. If newestWorker is null, abort these steps.
+  if (newest_worker == nullptr) {
+    return;
+  }
+
+  // 3. Let job be the result of running Create Job with update, registration’s
+  // storage key, registration’s scope url, newestWorker’s script url, null, and
+  // null.
+  std::unique_ptr<Job> job =
+      CreateJob(kUpdate, registration->storage_key(), registration->scope_url(),
+                newest_worker->script_url());
+
+  // 4. Set job’s worker type to newestWorker’s type.
+  // Cobalt only supports 'classic' worker type.
+
+  // 5. Set job’s force bypass cache flag if forceBypassCache is true.
+  job->force_bypass_cache_flag = force_bypass_cache;
+
+  // 6. Invoke Schedule Job with job.
+  message_loop()->task_runner()->PostTask(
+      FROM_HERE, base::BindOnce(&ServiceWorkerJobs::ScheduleJob,
+                                base::Unretained(this), std::move(job)));
+  DCHECK(!job.get());
+}
+
 void ServiceWorkerJobs::Update(Job* job) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Update()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -519,8 +605,8 @@
   // 1. Let registration be the result of running Get Registration given job’s
   //    storage key and job’s scope url.
   scoped_refptr<ServiceWorkerRegistrationObject> registration =
-      scope_to_registration_map_.GetRegistration(job->storage_key,
-                                                 job->scope_url);
+      scope_to_registration_map_->GetRegistration(job->storage_key,
+                                                  job->scope_url);
 
   // 2. If registration is null, then:
   if (!registration) {
@@ -533,7 +619,8 @@
   }
   // 3. Let newestWorker be the result of running Get Newest Worker algorithm
   //    passing registration as the argument.
-  ServiceWorkerObject* newest_worker = registration->GetNewestWorker();
+  const scoped_refptr<ServiceWorkerObject>& newest_worker =
+      registration->GetNewestWorker();
 
   // 4. If job’s job type is update, and newestWorker is not null and its script
   //    url does not equal job’s script url, then:
@@ -580,10 +667,11 @@
   //   8.4.  If the is top-level flag is unset, then return the result of
   //         fetching request.
   //   8.5.  Set request’s redirect mode to "error".
+  csp::SecurityCallback csp_callback = base::Bind(&PermitAnyNonRedirectedURL);
   //   8.6.  Fetch request, and asynchronously wait to run the remaining steps
   //         as part of fetch’s process response for the response response.
-  // TODO(b/225037465): Implement CSP check.
-  csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
+  // Note: The CSP check for the script_url is done in StartRegister, where
+  // the client's CSP list can still be referred to.
   loader::Origin origin = loader::Origin(job->script_url.GetOrigin());
   job->loader = script_loader_factory_->CreateScriptLoader(
       job->script_url, origin, csp_callback,
@@ -644,13 +732,14 @@
                                       "JavaScript MIME type."));
     return true;
   }
-  //   8.8.  Let serviceWorkerAllowed be the resulf extracting header list
+  //   8.8.  Let serviceWorkerAllowed be the result of extracting header list
   //         values given `Service-Worker-Allowed` and response’s header list.
   std::string service_worker_allowed;
   bool service_worker_allowed_exists = headers->GetNormalizedHeader(
       "Service-Worker-Allowed", &service_worker_allowed);
   //   8.9.  Set policyContainer to the result of creating a policy container
   //         from a fetch response given response.
+  state->script_headers = headers;
   //   8.10. If serviceWorkerAllowed is failure, then:
   //   8.10.1  Asynchronously complete these steps with a network error.
   //   8.11. Let scopeURL be registration’s scope url.
@@ -702,16 +791,22 @@
     std::unique_ptr<std::string> content) {
   TRACE_EVENT0("cobalt::worker",
                "ServiceWorkerJobs::UpdateOnContentProduced()");
+  DCHECK(content);
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Note: There seems to be missing handling of network errors here.
   //   8.17. Let url be request’s url.
   //   8.18. Set updatedResourceMap[url] to response.
-  auto result = state->updated_resource_map.insert(
-      std::make_pair(state->job->script_url, std::move(content)));
+  auto result = state->updated_resource_map.emplace(std::make_pair(
+      state->job->script_url,
+      ScriptResource(std::move(content), state->script_headers)));
   // Assert that the insert was successful.
   DCHECK(result.second);
+  std::string* updated_script_content =
+      result.second ? result.first->second.content.get() : nullptr;
+  DCHECK(updated_script_content);
   //   8.19. If response’s cache state is not "local", set registration’s last
   //         update check time to the current time.
+  // TODO(b/228904017):
   //   8.20. Set hasUpdatedResources to true if any of the following are true:
   //          - newestWorker is null.
   //          - newestWorker’s script url is not url or newestWorker’s type is
@@ -725,9 +820,14 @@
     if (state->newest_worker->script_url() != state->job->script_url) {
       state->has_updated_resources = true;
     } else {
-      std::string* script_resource =
+      const ScriptResource* newest_worker_script_resource =
           state->newest_worker->LookupScriptResource(state->job->script_url);
-      if (script_resource && content && (*script_resource != *content)) {
+      std::string* newest_worker_script_content =
+          newest_worker_script_resource
+              ? newest_worker_script_resource->content.get()
+              : nullptr;
+      if (!newest_worker_script_content || !updated_script_content ||
+          (*newest_worker_script_content != *updated_script_content)) {
         state->has_updated_resources = true;
       }
     }
@@ -740,9 +840,10 @@
   TRACE_EVENT0("cobalt::worker",
                "ServiceWorkerJobs::UpdateOnLoadingComplete()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
-  if (state->job->promise.get() == nullptr) {
-    // The job is already rejected, which means there was an error, so finish
-    // the job and skip the remaining steps.
+  if (!state->job->promise.get() || !state->job->client) {
+    // The job is already rejected, which means there was an error, or the
+    // client is already shutdown, so finish the job and skip the remaining
+    // steps.
     FinishJob(state->job);
     return;
   }
@@ -752,8 +853,8 @@
         state->job,
         PromiseErrorData(web::DOMException::kNetworkErr, error.value()));
     if (state->newest_worker == nullptr) {
-      scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
-                                                    state->job->scope_url);
+      scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+                                                     state->job->scope_url);
     }
     FinishJob(state->job);
     return;
@@ -765,7 +866,12 @@
       state->newest_worker->classic_scripts_imported()) {
     // This checks if there are any updates to already stored importScripts
     // resources.
-    if (state->newest_worker->worker_global_scope()
+    // TODO(b/259731731): worker_global_scope_ is set in
+    // ServiceWorkerObject::Initialize, part of the RunServiceWorkerAlgorithm.
+    // For persisted service workers this may not be called before SoftUpdate,
+    // find a way to ensure worker_global_scope_ is not null in that case.
+    if (state->newest_worker->worker_global_scope() != nullptr &&
+        state->newest_worker->worker_global_scope()
             ->LoadImportsAndReturnIfUpdated(
                 state->newest_worker->script_resource_map(),
                 &state->updated_resource_map)) {
@@ -778,7 +884,7 @@
   // steps, with script being the asynchronous completion value.
   auto entry = state->updated_resource_map.find(state->job->script_url);
   auto* script = entry != state->updated_resource_map.end()
-                     ? entry->second.get()
+                     ? entry->second.content.get()
                      : nullptr;
   // 9. If script is null or Is Async Module with script’s record, script’s
   //    base URL, and {} it true, then:
@@ -789,8 +895,8 @@
     // 9.2. If newestWorker is null, then remove registration
     //      map[(registration’s storage key, serialized scopeURL)].
     if (state->newest_worker == nullptr) {
-      scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
-                                                    state->job->scope_url);
+      scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+                                                     state->job->scope_url);
     }
     // 9.3. Invoke Finish Job with job and abort these steps.
     FinishJob(state->job);
@@ -814,12 +920,11 @@
 
   // 11. Let worker be a new service worker.
   ServiceWorkerObject::Options options(
-      "ServiceWorker", state->job->client->context()->network_module(),
-      state->registration);
-  options.web_options.platform_info =
-      state->job->client->context()->platform_info();
+      "ServiceWorker", state->job->client->web_settings(),
+      state->job->client->network_module(), state->registration);
+  options.web_options.platform_info = state->job->client->platform_info();
   options.web_options.service_worker_jobs =
-      state->job->client->context()->service_worker_jobs();
+      state->job->client->service_worker_jobs();
   scoped_refptr<ServiceWorkerObject> worker(new ServiceWorkerObject(options));
   // 12. Set worker’s script url to job’s script url, worker’s script
   //     resource to script, worker’s type to job’s worker type, and worker’s
@@ -830,6 +935,7 @@
   // 13. Append url to worker’s set of used scripts.
   worker->AppendToSetOfUsedScripts(state->job->script_url);
   // 14. Set worker’s script resource’s policy container to policyContainer.
+  DCHECK(state->script_headers);
   // 15. Let forceBypassCache be true if job’s force bypass cache flag is
   //     set, and false otherwise.
   bool force_bypass_cache = state->job->force_bypass_cache_flag;
@@ -842,8 +948,8 @@
   // RunServiceWorker, such as for registering the web context, execute first.
   base::MessageLoop::current()->task_runner()->PostTask(
       FROM_HERE, base::Bind(&ServiceWorkerJobs::UpdateOnRunServiceWorker,
-                            base::Unretained(this), state, std::move(worker),
-                            run_result_is_success));
+                            base::Unretained(this), std::move(state),
+                            std::move(worker), run_result_is_success));
 }
 
 void ServiceWorkerJobs::UpdateOnRunServiceWorker(
@@ -856,8 +962,8 @@
     // 17.2. If newestWorker is null, then remove registration
     //       map[(registration’s storage key, serialized scopeURL)].
     if (state->newest_worker == nullptr) {
-      scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
-                                                    state->job->scope_url);
+      scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+                                                     state->job->scope_url);
     }
     // 17.3. Invoke Finish Job with job.
     FinishJob(state->job);
@@ -907,8 +1013,8 @@
 }
 
 void ServiceWorkerJobs::Install(
-    Job* job, scoped_refptr<ServiceWorkerObject> worker,
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    Job* job, const scoped_refptr<ServiceWorkerObject>& worker,
+    const scoped_refptr<ServiceWorkerRegistrationObject>& registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Install()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Install:
@@ -923,7 +1029,8 @@
 
   // 2. Let newestWorker be the result of running Get Newest Worker algorithm
   //    passing registration as its argument.
-  ServiceWorkerObject* newest_worker = registration->GetNewestWorker();
+  const scoped_refptr<ServiceWorkerObject>& newest_worker =
+      registration->GetNewestWorker();
 
   // 3. Set registration’s update via cache mode to job’s update via cache mode.
   registration->set_update_via_cache_mode(job->update_via_cache);
@@ -998,8 +1105,6 @@
     } else {
       // 11.3.1. Queue a task task on installingWorker’s event loop using the
       //         DOM manipulation task source to run the following steps:
-      // Using a shared pointer to ensure that it still exists when it is
-      // signaled from the callback after the timeout.
       DCHECK(done_event_.IsSignaled());
       done_event_.Reset();
       installing_worker->web_agent()
@@ -1076,8 +1181,8 @@
     //       map[(registration’s storage key, serialized registration’s
     //       scope url)].
     if (newest_worker == nullptr) {
-      scope_to_registration_map_.RemoveRegistration(registration->storage_key(),
-                                                    registration->scope_url());
+      scope_to_registration_map_->RemoveRegistration(
+          registration->storage_key(), registration->scope_url());
     }
     // 12.4. Invoke Finish Job with job and abort these steps.
     FinishJob(job);
@@ -1116,10 +1221,14 @@
   // TODO(b/234788479): Wait for tasks.
   // 22. Invoke Try Activate with registration.
   TryActivate(registration);
+
+  // Persist registration since the waiting_worker has been updated.
+  scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+                                                  registration->scope_url());
 }
 
 bool ServiceWorkerJobs::IsAnyClientUsingRegistration(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    ServiceWorkerRegistrationObject* registration) {
   bool any_client_is_using = false;
   for (auto& context : web_context_registrations_) {
     // When a service worker client is controlled by a service worker, it is
@@ -1135,7 +1244,7 @@
 }
 
 void ServiceWorkerJobs::TryActivate(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    ServiceWorkerRegistrationObject* registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::TryActivate()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Try Activate:
@@ -1174,7 +1283,7 @@
 }
 
 void ServiceWorkerJobs::Activate(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    ServiceWorkerRegistrationObject* registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Activate()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Activate:
@@ -1209,7 +1318,7 @@
     url::Origin context_storage_key =
         url::Origin::Create(context->environment_settings()->creation_url());
     scoped_refptr<ServiceWorkerRegistrationObject> matched_registration =
-        scope_to_registration_map_.MatchServiceWorkerRegistration(
+        scope_to_registration_map_->MatchServiceWorkerRegistration(
             context_storage_key, registration->scope_url());
     if (matched_registration == registration) {
       matched_clients.push_back(context);
@@ -1218,7 +1327,7 @@
   // 7. For each client of matchedClients, queue a task on client’s  responsible
   //    event loop, using the DOM manipulation task source, to run the following
   //    substeps:
-  for (auto& context : matched_clients) {
+  for (auto& client : matched_clients) {
     // 7.1. Let readyPromise be client’s global object's
     //      ServiceWorkerContainer object’s ready
     //      promise.
@@ -1228,14 +1337,14 @@
     //      the service worker registration object that
     //      represents registration in readyPromise’s
     //      relevant settings object.
-    context->message_loop()->task_runner()->PostTask(
+    client->message_loop()->task_runner()->PostTask(
         FROM_HERE,
         base::BindOnce(&ServiceWorkerContainer::MaybeResolveReadyPromise,
-                       base::Unretained(context->GetWindowOrWorkerGlobalScope()
+                       base::Unretained(client->GetWindowOrWorkerGlobalScope()
                                             ->navigator_base()
                                             ->service_worker()
                                             .get()),
-                       registration));
+                       base::Unretained(registration)));
   }
   // 8. For each client of matchedClients:
   // 8.1. If client is a window client, unassociate client’s responsible
@@ -1245,16 +1354,25 @@
   // Cobalt doesn't implement 'application cache':
   //   https://www.w3.org/TR/2011/WD-html5-20110525/offline.html#applicationcache
   // 9. For each service worker client client who is using registration:
-  for (auto& context : web_context_registrations_) {
-    web::EnvironmentSettings* client = context->environment_settings();
+  // Note: The spec defines "control" and "use" of a service worker from the
+  // value of the active service worker property of the client environment, but
+  // that property is set here, so here we should not use that exact definition
+  // to determine if the client is using this registration. Instead, we use the
+  // Match Service Worker Registration algorithm to find the registration for a
+  // client and compare it with the registration being activated.
+  //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-use
+  for (const auto& client : web_context_registrations_) {
+    scoped_refptr<ServiceWorkerRegistrationObject> client_registration =
+        scope_to_registration_map_->MatchServiceWorkerRegistration(
+            client->environment_settings()->ObtainStorageKey(),
+            client->environment_settings()->creation_url());
     // When a service worker client is controlled by a service worker, it is
     // said that the service worker client is using the service worker’s
     // containing service worker registration.
     //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-control
-    if (context->is_controlled_by(registration->active_worker())) {
+    if (client_registration.get() == registration) {
       // 9.1. Set client’s active worker to registration’s active worker.
-      client->context()->set_active_service_worker(
-          registration->active_worker());
+      client->set_active_service_worker(registration->active_worker());
       // 9.2. Invoke Notify Controller Change algorithm with client as the
       //      argument.
       NotifyControllerChange(client);
@@ -1327,11 +1445,15 @@
   if (activated && registration->active_worker()) {
     UpdateWorkerState(registration->active_worker(),
                       kServiceWorkerStateActivated);
+
+    // Persist registration since the waiting_worker has been updated to nullptr
+    // and the active_worker has been updated to the previous waiting_worker.
+    scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+                                                    registration->scope_url());
   }
 }
 
-void ServiceWorkerJobs::NotifyControllerChange(
-    web::EnvironmentSettings* client) {
+void ServiceWorkerJobs::NotifyControllerChange(web::Context* client) {
   // Algorithm for Notify Controller Change:
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#notify-controller-change-algorithm
   // 1. Assert: client is not null.
@@ -1340,11 +1462,10 @@
   // 2. If client is an environment settings object, queue a task to fire an
   //    event named controllerchange at the ServiceWorkerContainer object that
   //    client is associated with.
-  client->context()->message_loop()->task_runner()->PostTask(
+  client->message_loop()->task_runner()->PostTask(
       FROM_HERE, base::Bind(
-                     [](web::EnvironmentSettings* client) {
-                       client->context()
-                           ->GetWindowOrWorkerGlobalScope()
+                     [](web::Context* client) {
+                       client->GetWindowOrWorkerGlobalScope()
                            ->navigator_base()
                            ->service_worker()
                            ->DispatchEvent(new web::Event(
@@ -1367,7 +1488,7 @@
 }
 
 void ServiceWorkerJobs::ClearRegistration(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    ServiceWorkerRegistrationObject* registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ClearRegistration()");
   // Algorithm for Clear Registration:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clear-registration-algorithm
@@ -1400,22 +1521,28 @@
     UpdateRegistrationState(registration, kWaiting, nullptr);
   }
 
-  // 3. If registration’s active worker is not null, then:
+  // 4. If registration’s active worker is not null, then:
   ServiceWorkerObject* active_worker = registration->active_worker();
   if (active_worker) {
-    // 3.1. Terminate registration’s active worker.
+    // 4.1. Terminate registration’s active worker.
     TerminateServiceWorker(active_worker);
-    // 3.2. Run the Update Worker State algorithm passing registration’s
+    // 4.2. Run the Update Worker State algorithm passing registration’s
     //      active worker and "redundant" as the arguments.
     UpdateWorkerState(active_worker, kServiceWorkerStateRedundant);
-    // 3.3. Run the Update Registration State algorithm passing registration,
+    // 4.3. Run the Update Registration State algorithm passing registration,
     //      "active" and null as the arguments.
     UpdateRegistrationState(registration, kActive, nullptr);
   }
+
+  // Persist registration since the waiting_worker and active_worker have
+  // been updated to nullptr. This will remove any persisted registration
+  // if one exists.
+  scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+                                                  registration->scope_url());
 }
 
 void ServiceWorkerJobs::TryClearRegistration(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    ServiceWorkerRegistrationObject* registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::TryClearRegistration()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Try Clear Registration:
@@ -1450,8 +1577,8 @@
 }
 
 void ServiceWorkerJobs::UpdateRegistrationState(
-    scoped_refptr<ServiceWorkerRegistrationObject> registration,
-    RegistrationState target, scoped_refptr<ServiceWorkerObject> source) {
+    ServiceWorkerRegistrationObject* registration, RegistrationState target,
+    const scoped_refptr<ServiceWorkerObject>& source) {
   TRACE_EVENT2("cobalt::worker", "ServiceWorkerJobs::UpdateRegistrationState()",
                "target", target, "source", source);
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1476,8 +1603,7 @@
             FROM_HERE,
             base::BindOnce(
                 [](web::Context* context,
-                   scoped_refptr<ServiceWorkerRegistrationObject>
-                       registration) {
+                   ServiceWorkerRegistrationObject* registration) {
                   // 2.2.1. ... set the installing attribute of
                   //        registrationObject to null if registration’s
                   //        installing worker is null, or the result of getting
@@ -1492,7 +1618,7 @@
                             registration->installing_worker()));
                   }
                 },
-                context, registration));
+                context, base::Unretained(registration)));
       }
       break;
     }
@@ -1507,8 +1633,7 @@
             FROM_HERE,
             base::BindOnce(
                 [](web::Context* context,
-                   scoped_refptr<ServiceWorkerRegistrationObject>
-                       registration) {
+                   ServiceWorkerRegistrationObject* registration) {
                   // 3.2.1. ... set the waiting attribute of registrationObject
                   //        to null if registration’s waiting worker is null, or
                   //        the result of getting the service worker object that
@@ -1521,7 +1646,7 @@
                         registration->waiting_worker()));
                   }
                 },
-                context, registration));
+                context, base::Unretained(registration)));
       }
       break;
     }
@@ -1536,8 +1661,7 @@
             FROM_HERE,
             base::BindOnce(
                 [](web::Context* context,
-                   scoped_refptr<ServiceWorkerRegistrationObject>
-                       registration) {
+                   ServiceWorkerRegistrationObject* registration) {
                   // 4.2.1. ... set the active attribute of registrationObject
                   //        to null if registration’s active worker is null, or
                   //        the result of getting the service worker object that
@@ -1550,7 +1674,7 @@
                         registration->active_worker()));
                   }
                 },
-                context, registration));
+                context, base::Unretained(registration)));
       }
       break;
     }
@@ -1615,20 +1739,18 @@
   }
 }
 
-void ServiceWorkerJobs::HandleServiceWorkerClientUnload(
-    web::EnvironmentSettings* client) {
+void ServiceWorkerJobs::HandleServiceWorkerClientUnload(web::Context* client) {
   TRACE_EVENT0("cobalt::worker",
                "ServiceWorkerJobs::HandleServiceWorkerClientUnload()");
   // Algorithm for Handle Servicer Worker Client Unload:
-  //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-user-agent-shutdown-algorithm
+  //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-client-unload-algorithm
   DCHECK(client);
   // 1. Run the following steps atomically.
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
 
   // 2. Let registration be the service worker registration used by client.
   // 3. If registration is null, abort these steps.
-  ServiceWorkerObject* active_service_worker =
-      client->context()->active_service_worker();
+  ServiceWorkerObject* active_service_worker = client->active_service_worker();
   if (!active_service_worker) return;
   ServiceWorkerRegistrationObject* registration =
       active_service_worker->containing_service_worker_registration();
@@ -1638,12 +1760,12 @@
   //    steps.
   // Ensure the client is already removed from the registrations when this runs.
   DCHECK(web_context_registrations_.end() ==
-         web_context_registrations_.find(client->context()));
+         web_context_registrations_.find(client));
   if (IsAnyClientUsingRegistration(registration)) return;
 
   // 5. If registration is unregistered, invoke Try Clear Registration with
   //    registration.
-  if (scope_to_registration_map_.IsUnregistered(registration)) {
+  if (scope_to_registration_map_->IsUnregistered(registration)) {
     TryClearRegistration(registration);
   }
 
@@ -1665,7 +1787,8 @@
       worker->worker_global_scope();
 
   // 1.2. Set serviceWorkerGlobalScope’s closing flag to true.
-  service_worker_global_scope->set_closing_flag(true);
+  if (service_worker_global_scope != nullptr)
+    service_worker_global_scope->set_closing_flag(true);
 
   // 1.3. Remove all the items from serviceWorker’s set of extended events.
   // TODO(b/240174245): Implement 'set of extended events'.
@@ -1698,8 +1821,9 @@
   }
 
   // 1.5. Abort the script currently running in serviceWorker.
-  DCHECK(worker->is_running());
-  worker->Abort();
+  if (worker->is_running()) {
+    worker->Abort();
+  }
 
   // 1.6. Set serviceWorker’s start status to null.
   worker->set_start_status(nullptr);
@@ -1711,7 +1835,10 @@
   // Algorithm for Unregister:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#unregister-algorithm
   // 1. If the origin of job’s scope url is not job’s client's origin, then:
-  if (!url::Origin::Create(GURL(job->client->GetOrigin().SerializedOrigin()))
+  if (job->client &&
+      !url::Origin::Create(GURL(job->client->environment_settings()
+                                    ->GetOrigin()
+                                    .SerializedOrigin()))
            .IsSameOriginWith(url::Origin::Create(job->scope_url))) {
     // 1.1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
     RejectJobPromise(
@@ -1728,8 +1855,8 @@
   // 2. Let registration be the result of running Get Registration given job’s
   //    storage key and job’s scope url.
   scoped_refptr<ServiceWorkerRegistrationObject> registration =
-      scope_to_registration_map_.GetRegistration(job->storage_key,
-                                                 job->scope_url);
+      scope_to_registration_map_->GetRegistration(job->storage_key,
+                                                  job->scope_url);
 
   // 3. If registration is null, then:
   if (!registration) {
@@ -1743,8 +1870,8 @@
 
   // 4. Remove registration map[(registration’s storage key, job’s scope url)].
   // Keep the registration until this algorithm finishes.
-  scope_to_registration_map_.RemoveRegistration(registration->storage_key(),
-                                                job->scope_url);
+  scope_to_registration_map_->RemoveRegistration(registration->storage_key(),
+                                                 job->scope_url);
 
   // 5. Invoke Resolve Job Promise with job and true.
   ResolveJobPromise(job, true);
@@ -1762,94 +1889,84 @@
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Reject Job Promise:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#reject-job-promise
+  base::AutoLock lock(job->equivalent_jobs_promise_mutex);
   // 1. If job’s client is not null, queue a task, on job’s client's responsible
   //    event loop using the DOM manipulation task source, to reject job’s job
   //    promise with a new exception with errorData and a user agent-defined
   //    message, in job’s client's Realm.
-
-  auto reject_task = [](std::unique_ptr<JobPromiseType> promise,
-                        const PromiseErrorData& error_data) {
-    error_data.Reject(std::move(promise));
-  };
-
-  if (job->client) {
-    base::AutoLock lock(job->equivalent_jobs_promise_mutex);
-    job->client->context()->message_loop()->task_runner()->PostTask(
-        FROM_HERE,
-        base::BindOnce(reject_task, std::move(job->promise), error_data));
+  // 2.1. If equivalentJob’s client is null, continue.
+  // 2.2. Queue a task, on equivalentJob’s client's responsible event loop
+  //      using the DOM manipulation task source, to reject equivalentJob’s
+  //      job promise with a new exception with errorData and a user
+  //      agent-defined message, in equivalentJob’s client's Realm.
+  if (job->client && job->promise != nullptr) {
+    DCHECK(IsWebContextRegistered(job->client));
+    job->client->message_loop()->task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(
+                       [](std::unique_ptr<JobPromiseType> promise,
+                          const PromiseErrorData& error_data) {
+                         error_data.Reject(std::move(promise));
+                       },
+                       std::move(job->promise), error_data));
     // Ensure that the promise is cleared, so that equivalent jobs won't get
     // added from this point on.
     CHECK(!job->promise);
   }
   // 2. For each equivalentJob in job’s list of equivalent jobs:
   for (auto& equivalent_job : job->equivalent_jobs) {
-    // Equivalent jobs should never have equivalent jobs of their own.
-    DCHECK(equivalent_job->equivalent_jobs.empty());
-
-    // 2.1. If equivalentJob’s client is null, continue.
-    if (equivalent_job->client) {
-      // 2.2. Queue a task, on equivalentJob’s client's responsible event loop
-      //      using the DOM manipulation task source, to reject equivalentJob’s
-      //      job promise with a new exception with errorData and a user
-      //      agent-defined message, in equivalentJob’s client's Realm.
-      equivalent_job->client->context()
-          ->message_loop()
-          ->task_runner()
-          ->PostTask(
-              FROM_HERE,
-              base::BindOnce(reject_task, std::move(equivalent_job->promise),
-                             error_data));
-      // Check that the promise is cleared.
-      CHECK(!equivalent_job->promise);
-    }
+    // Recurse for the equivalent jobs.
+    RejectJobPromise(equivalent_job.get(), error_data);
   }
   job->equivalent_jobs.clear();
 }
 
 void ServiceWorkerJobs::ResolveJobPromise(
     Job* job, bool value,
-    scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+    const scoped_refptr<ServiceWorkerRegistrationObject>& registration) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ResolveJobPromise()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   DCHECK(job);
   // Algorithm for Resolve Job Promise:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-job-promise-algorithm
-
+  base::AutoLock lock(job->equivalent_jobs_promise_mutex);
   // 1. If job’s client is not null, queue a task, on job’s client's responsible
   //    event loop using the DOM manipulation task source, to run the following
   //    substeps:
-  auto resolve_task =
-      [](JobType type, web::EnvironmentSettings* client,
-         std::unique_ptr<JobPromiseType> promise, bool value,
-         scoped_refptr<ServiceWorkerRegistrationObject> registration) {
-        TRACE_EVENT0("cobalt::worker",
-                     "ServiceWorkerJobs::ResolveJobPromise() ResolveTask");
-        // 1.1./2.2.1. Let convertedValue be null.
-        // 1.2./2.2.2. If job’s job type is either register or update, set
-        //             convertedValue to the result of getting the service
-        //             worker registration object that represents value in job’s
-        //             client.
-        if (type == kRegister || type == kUpdate) {
-          scoped_refptr<cobalt::script::Wrappable> converted_value =
-              client->context()->GetServiceWorkerRegistration(registration);
-          // 1.4./2.2.4. Resolve job’s job promise with convertedValue.
-          promise->Resolve(converted_value);
-        } else {
-          DCHECK_EQ(kUnregister, type);
-          // 1.3./2.2.3. Else, set convertedValue to value, in job’s client's
-          //             Realm.
-          bool converted_value = value;
-          // 1.4./2.2.4. Resolve job’s job promise with convertedValue.
-          promise->Resolve(converted_value);
-        }
-      };
-
-  if (job->client) {
-    base::AutoLock lock(job->equivalent_jobs_promise_mutex);
-    job->client->context()->message_loop()->task_runner()->PostTask(
+  // 2.1 If equivalentJob’s client is null, continue to the next iteration of
+  // the loop.
+  if (job->client && job->promise != nullptr) {
+    DCHECK(IsWebContextRegistered(job->client));
+    job->client->message_loop()->task_runner()->PostTask(
         FROM_HERE,
-        base::BindOnce(resolve_task, job->type, job->client,
-                       std::move(job->promise), value, registration));
+        base::BindOnce(
+            [](JobType type, web::Context* client,
+               std::unique_ptr<JobPromiseType> promise, bool value,
+               scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+              TRACE_EVENT0(
+                  "cobalt::worker",
+                  "ServiceWorkerJobs::ResolveJobPromise() ResolveTask");
+              // 1.1./2.2.1. Let convertedValue be null.
+              // 1.2./2.2.2. If job’s job type is either register or update, set
+              //             convertedValue to the result of getting the service
+              //             worker registration object that represents value in
+              //             job’s client.
+              if (type == kRegister || type == kUpdate) {
+                scoped_refptr<cobalt::script::Wrappable> converted_value =
+                    client->GetServiceWorkerRegistration(registration);
+                // 1.4./2.2.4. Resolve job’s job promise with convertedValue.
+                promise->Resolve(converted_value);
+              } else {
+                DCHECK_EQ(kUnregister, type);
+                // 1.3./2.2.3. Else, set convertedValue to value, in job’s
+                // client's
+                //             Realm.
+                bool converted_value = value;
+                // 1.4./2.2.4. Resolve job’s job promise with convertedValue.
+                promise->Resolve(converted_value);
+              }
+            },
+            job->type, job->client, std::move(job->promise), value,
+            registration));
     // Ensure that the promise is cleared, so that equivalent jobs won't get
     // added from this point on.
     CHECK(!job->promise);
@@ -1857,26 +1974,8 @@
 
   // 2. For each equivalentJob in job’s list of equivalent jobs:
   for (auto& equivalent_job : job->equivalent_jobs) {
-    // Equivalent jobs should never have equivalent jobs of their own.
-    DCHECK(equivalent_job->equivalent_jobs.empty());
-
-    // 2.1. If equivalentJob’s client is null, continue to the next iteration of
-    //      the loop.
-    if (equivalent_job->client) {
-      // 2.2. Queue a task, on equivalentJob’s client's responsible event loop
-      //      using the DOM manipulation task source, to run the following
-      //      substeps:
-      equivalent_job->client->context()
-          ->message_loop()
-          ->task_runner()
-          ->PostTask(FROM_HERE,
-                     base::BindOnce(resolve_task, equivalent_job->type,
-                                    equivalent_job->client,
-                                    std::move(equivalent_job->promise), value,
-                                    registration));
-      // Check that the promise is cleared.
-      CHECK(!equivalent_job->promise);
-    }
+    // Recurse for the equivalent jobs.
+    ResolveJobPromise(equivalent_job.get(), value, registration);
   }
   job->equivalent_jobs.clear();
 }
@@ -1900,8 +1999,7 @@
   }
 }
 
-void ServiceWorkerJobs::MaybeResolveReadyPromiseSubSteps(
-    web::EnvironmentSettings* client) {
+void ServiceWorkerJobs::MaybeResolveReadyPromiseSubSteps(web::Context* client) {
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Algorithm for Sub steps of ServiceWorkerContainer.ready():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-ready
@@ -1909,17 +2007,17 @@
   //    3.1. Let client by this's service worker client.
   //    3.2. Let storage key be the result of running obtain a storage
   //         key given client.
-  url::Origin storage_key = client->ObtainStorageKey();
+  url::Origin storage_key = client->environment_settings()->ObtainStorageKey();
   //    3.3. Let registration be the result of running Match Service
   //         Worker Registration given storage key and client’s
   //         creation URL.
   // TODO(b/234659851): Investigate whether this should use the creation URL
   // directly instead.
-  const GURL& base_url = client->creation_url();
+  const GURL& base_url = client->environment_settings()->creation_url();
   GURL client_url = base_url.Resolve("");
   scoped_refptr<ServiceWorkerRegistrationObject> registration =
-      scope_to_registration_map_.MatchServiceWorkerRegistration(storage_key,
-                                                                client_url);
+      scope_to_registration_map_->MatchServiceWorkerRegistration(storage_key,
+                                                                 client_url);
   //    3.3. If registration is not null, and registration’s active
   //         worker is not null, queue a task on readyPromise’s
   //         relevant settings object's responsible event loop, using
@@ -1928,11 +2026,10 @@
   //         registration object that represents registration in
   //         readyPromise’s relevant settings object.
   if (registration && registration->active_worker()) {
-    client->context()->message_loop()->task_runner()->PostTask(
+    client->message_loop()->task_runner()->PostTask(
         FROM_HERE,
         base::BindOnce(&ServiceWorkerContainer::MaybeResolveReadyPromise,
-                       base::Unretained(client->context()
-                                            ->GetWindowOrWorkerGlobalScope()
+                       base::Unretained(client->GetWindowOrWorkerGlobalScope()
                                             ->navigator_base()
                                             ->service_worker()
                                             .get()),
@@ -1942,7 +2039,7 @@
 
 void ServiceWorkerJobs::GetRegistrationSubSteps(
     const url::Origin& storage_key, const GURL& client_url,
-    web::EnvironmentSettings* client,
+    web::Context* client,
     std::unique_ptr<script::ValuePromiseWrappable::Reference>
         promise_reference) {
   TRACE_EVENT0("cobalt::worker",
@@ -1954,41 +2051,40 @@
   // 8.1. Let registration be the result of running Match Service Worker
   //      Registration algorithm with clientURL as its argument.
   scoped_refptr<ServiceWorkerRegistrationObject> registration =
-      scope_to_registration_map_.MatchServiceWorkerRegistration(storage_key,
-                                                                client_url);
+      scope_to_registration_map_->MatchServiceWorkerRegistration(storage_key,
+                                                                 client_url);
   // 8.2. If registration is null, resolve promise with undefined and abort
   //      these steps.
   // 8.3. Resolve promise with the result of getting the service worker
   //      registration object that represents registration in promise’s
   //      relevant settings object.
-  client->context()->message_loop()->task_runner()->PostTask(
+  client->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
-          [](web::EnvironmentSettings* settings,
+          [](web::Context* client,
              std::unique_ptr<script::ValuePromiseWrappable::Reference> promise,
              scoped_refptr<ServiceWorkerRegistrationObject> registration) {
             TRACE_EVENT0(
                 "cobalt::worker",
                 "ServiceWorkerJobs::GetRegistrationSubSteps() Resolve");
             promise->value().Resolve(
-                settings->context()->GetServiceWorkerRegistration(
-                    registration));
+                client->GetServiceWorkerRegistration(registration));
           },
           client, std::move(promise_reference), registration));
 }
 
 void ServiceWorkerJobs::GetRegistrationsSubSteps(
-    const url::Origin& storage_key, web::EnvironmentSettings* client,
+    const url::Origin& storage_key, web::Context* client,
     std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
         promise_reference) {
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   std::vector<scoped_refptr<ServiceWorkerRegistrationObject>>
       registration_objects =
-          scope_to_registration_map_.GetRegistrations(storage_key);
-  client->context()->message_loop()->task_runner()->PostTask(
+          scope_to_registration_map_->GetRegistrations(storage_key);
+  client->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
-          [](web::EnvironmentSettings* settings,
+          [](web::Context* client,
              std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
                  promise,
              std::vector<scoped_refptr<ServiceWorkerRegistrationObject>>
@@ -1999,8 +2095,7 @@
             script::Sequence<scoped_refptr<script::Wrappable>> registrations;
             for (auto registration_object : registration_objects) {
               registrations.push_back(scoped_refptr<script::Wrappable>(
-                  settings->context()
-                      ->GetServiceWorkerRegistration(registration_object)
+                  client->GetServiceWorkerRegistration(registration_object)
                       .get()));
             }
             promise->value().Resolve(std::move(registrations));
@@ -2010,13 +2105,13 @@
 }
 
 void ServiceWorkerJobs::SkipWaitingSubSteps(
-    web::Context* client_context, ServiceWorkerObject* service_worker,
+    web::Context* worker_context, ServiceWorkerObject* service_worker,
     std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::SkipWaitingSubSteps()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Check if the client web context is still active. This may trigger if
   // skipWaiting() was called and service worker installation fails.
-  if (!IsWebContextRegistered(client_context)) {
+  if (!IsWebContextRegistered(worker_context)) {
     promise_reference.release();
     return;
   }
@@ -2032,7 +2127,7 @@
   TryActivate(service_worker->containing_service_worker_registration());
 
   // 2.3. Resolve promise with undefined.
-  client_context->message_loop()->task_runner()->PostTask(
+  worker_context->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](std::unique_ptr<script::ValuePromiseVoid::Reference> promise) {
@@ -2049,7 +2144,7 @@
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-extendableevent-waituntil
   // 5.2.2. If registration is unregistered, invoke Try Clear Registration
   //        with registration.
-  if (scope_to_registration_map_.IsUnregistered(registration)) {
+  if (scope_to_registration_map_->IsUnregistered(registration)) {
     TryClearRegistration(registration);
   }
   // 5.2.3. If registration is not null, invoke Try Activate with
@@ -2060,7 +2155,7 @@
 }
 
 void ServiceWorkerJobs::ClientsGetSubSteps(
-    web::Context* promise_context,
+    web::Context* worker_context,
     ServiceWorkerObject* associated_service_worker,
     std::unique_ptr<script::ValuePromiseWrappable::Reference> promise_reference,
     const std::string& id) {
@@ -2068,7 +2163,7 @@
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   // Check if the client web context is still active. This may trigger if
   // Clients.get() was called and service worker installation fails.
-  if (!IsWebContextRegistered(promise_context)) {
+  if (!IsWebContextRegistered(worker_context)) {
     promise_reference.release();
     return;
   }
@@ -2080,12 +2175,12 @@
   const url::Origin& storage_key =
       associated_service_worker->containing_service_worker_registration()
           ->storage_key();
-  for (auto& context : web_context_registrations_) {
-    web::EnvironmentSettings* client = context->environment_settings();
-    url::Origin client_storage_key = client->ObtainStorageKey();
+  for (auto& client : web_context_registrations_) {
+    url::Origin client_storage_key =
+        client->environment_settings()->ObtainStorageKey();
     if (client_storage_key.IsSameOriginWith(storage_key)) {
       // 2.1.1. If client’s id is not id, continue.
-      if (client->id() != id) continue;
+      if (client->environment_settings()->id() != id) continue;
 
       // 2.1.2. Wait for either client’s execution ready flag to be set or for
       //        client’s discarded flag to be set.
@@ -2094,13 +2189,13 @@
 
       // 2.1.3. If client’s execution ready flag is set, then invoke Resolve Get
       //        Client Promise with client and promise, and abort these steps.
-      ResolveGetClientPromise(client, promise_context,
+      ResolveGetClientPromise(client, worker_context,
                               std::move(promise_reference));
       return;
     }
   }
   // 2.2. Resolve promise with undefined.
-  promise_context->message_loop()->task_runner()->PostTask(
+  worker_context->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](std::unique_ptr<script::ValuePromiseWrappable::Reference>
@@ -2113,7 +2208,7 @@
 }
 
 void ServiceWorkerJobs::ResolveGetClientPromise(
-    web::EnvironmentSettings* client, web::Context* promise_context,
+    web::Context* client, web::Context* worker_context,
     std::unique_ptr<script::ValuePromiseWrappable::Reference>
         promise_reference) {
   TRACE_EVENT0("cobalt::worker",
@@ -2136,15 +2231,16 @@
 
   // 3. If client is an environment settings object and is not a window client,
   //    then:
-  if (!client->context()->GetWindowOrWorkerGlobalScope()->IsWindow()) {
+  if (!client->GetWindowOrWorkerGlobalScope()->IsWindow()) {
     // 3.1. Let clientObject be the result of running Create Client algorithm
     //      with client as the argument.
-    scoped_refptr<Client> client_object = Client::Create(client);
+    scoped_refptr<Client> client_object =
+        Client::Create(client->environment_settings());
 
     // 3.2. Queue a task to resolve promise with clientObject, on promise’s
     //      relevant settings object's responsible event loop using the DOM
     //      manipulation task source, and abort these steps.
-    promise_context->message_loop()->task_runner()->PostTask(
+    worker_context->message_loop()->task_runner()->PostTask(
         FROM_HERE,
         base::BindOnce(
             [](std::unique_ptr<script::ValuePromiseWrappable::Reference>
@@ -2171,10 +2267,10 @@
   // functionality in the client context. It is included however to help future
   // implementation for fetching values for WindowClient properties, with
   // similar logic existing in ClientsMatchAllSubSteps.
-  client->context()->message_loop()->task_runner()->PostTask(
+  client->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
-          [](web::EnvironmentSettings* client, web::Context* promise_context,
+          [](web::Context* client, web::Context* worker_context,
              std::unique_ptr<script::ValuePromiseWrappable::Reference>
                  promise_reference) {
             // 4.4.1. Let frameType be the result of running Get Frame Type with
@@ -2186,7 +2282,8 @@
             //        steps with browsingContext’s active document as the
             //        argument.
             // Handled in the WindowData constructor.
-            std::unique_ptr<WindowData> window_data(new WindowData(client));
+            std::unique_ptr<WindowData> window_data(
+                new WindowData(client->environment_settings()));
 
             // 4.4.4. Let ancestorOriginsList be the empty list.
             // 4.4.5. If client is a window client, set ancestorOriginsList to
@@ -2198,7 +2295,7 @@
             // 4.4.6. Queue a task to run the following steps on promise’s
             //        relevant settings object's responsible event loop using
             //        the DOM manipulation task source:
-            promise_context->message_loop()->task_runner()->PostTask(
+            worker_context->message_loop()->task_runner()->PostTask(
                 FROM_HERE,
                 base::BindOnce(
                     [](std::unique_ptr<script::ValuePromiseWrappable::Reference>
@@ -2218,12 +2315,12 @@
                     },
                     std::move(promise_reference), std::move(window_data)));
           },
-          client, promise_context, std::move(promise_reference)));
+          client, worker_context, std::move(promise_reference)));
   DCHECK_EQ(nullptr, promise_reference.get());
 }
 
 void ServiceWorkerJobs::ClientsMatchAllSubSteps(
-    web::Context* client_context,
+    web::Context* worker_context,
     ServiceWorkerObject* associated_service_worker,
     std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
         promise_reference,
@@ -2231,9 +2328,9 @@
   TRACE_EVENT0("cobalt::worker",
                "ServiceWorkerJobs::ClientsMatchAllSubSteps()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
-  // Check if the client web context is still active. This may trigger if
+  // Check if the worker web context is still active. This may trigger if
   // Clients.matchAll() was called and service worker installation fails.
-  if (!IsWebContextRegistered(client_context)) {
+  if (!IsWebContextRegistered(worker_context)) {
     promise_reference.release();
     return;
   }
@@ -2241,7 +2338,7 @@
   // Parallel sub steps (2) for algorithm for Clients.matchAll():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-matchall
   // 2.1. Let targetClients be a new list.
-  std::list<web::EnvironmentSettings*> target_clients;
+  std::list<web::Context*> target_clients;
 
   // 2.2. For each service worker client client where the result of running
   //      obtain a storage key given client equals the associated service
@@ -2249,9 +2346,9 @@
   const url::Origin& storage_key =
       associated_service_worker->containing_service_worker_registration()
           ->storage_key();
-  for (auto& context : web_context_registrations_) {
-    web::EnvironmentSettings* client = context->environment_settings();
-    url::Origin client_storage_key = client->ObtainStorageKey();
+  for (auto& client : web_context_registrations_) {
+    url::Origin client_storage_key =
+        client->environment_settings()->ObtainStorageKey();
     if (client_storage_key.IsSameOriginWith(storage_key)) {
       // 2.2.1. If client’s execution ready flag is unset or client’s discarded
       //        flag is set, continue.
@@ -2266,8 +2363,7 @@
       //        active service worker is not the associated service worker,
       //        continue.
       if (!include_uncontrolled &&
-          (client->context()->active_service_worker() !=
-           associated_service_worker)) {
+          (client->active_service_worker() != associated_service_worker)) {
         continue;
       }
 
@@ -2281,12 +2377,12 @@
       new std::vector<WindowData>);
 
   // 2.4. Let matchedClients be a new list.
-  std::unique_ptr<std::vector<web::EnvironmentSettings*>> matched_clients(
-      new std::vector<web::EnvironmentSettings*>);
+  std::unique_ptr<std::vector<web::Context*>> matched_clients(
+      new std::vector<web::Context*>);
 
   // 2.5. For each service worker client client in targetClients:
   for (auto* client : target_clients) {
-    auto* global_scope = client->context()->GetWindowOrWorkerGlobalScope();
+    auto* global_scope = client->GetWindowOrWorkerGlobalScope();
 
     if ((type == kClientTypeWindow || type == kClientTypeAll) &&
         (global_scope->IsWindow())) {
@@ -2295,7 +2391,7 @@
 
       // 2.5.1.1. Let windowData be [ "client" -> client, "ancestorOriginsList"
       //          -> a new list ].
-      WindowData window_data(client);
+      WindowData window_data(client->environment_settings());
 
       // 2.5.1.2. Let browsingContext be null.
 
@@ -2305,9 +2401,8 @@
 
       // 2.5.1.4. If client is an environment settings object, set
       //          browsingContext to client’s global object's browsing context.
-
       // 2.5.1.5. Else, set browsingContext to client’s target browsing context.
-      web::Context* browsing_context = client_context;
+      web::Context* browsing_context = client;
 
       // 2.5.1.6. Queue a task task to run the following substeps on
       //          browsingContext’s event loop using the user interaction task
@@ -2378,14 +2473,13 @@
   // 2.6. Queue a task to run the following steps on promise’s relevant
   // settings object's responsible event loop using the DOM manipulation
   // task source:
-  client_context->message_loop()->task_runner()->PostTask(
+  worker_context->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
                  promise_reference,
              std::unique_ptr<std::vector<WindowData>> matched_window_data,
-             std::unique_ptr<std::vector<web::EnvironmentSettings*>>
-                 matched_clients) {
+             std::unique_ptr<std::vector<web::Context*>> matched_clients) {
             TRACE_EVENT0(
                 "cobalt::worker",
                 "ServiceWorkerJobs::ClientsMatchAllSubSteps() Resolve Promise");
@@ -2415,7 +2509,8 @@
               // 2.6.3.1. Let clientObject be the result of running
               //          Create Client algorithm with client as the
               //          argument.
-              scoped_refptr<Client> client_object = Client::Create(client);
+              scoped_refptr<Client> client_object =
+                  Client::Create(client->environment_settings());
 
               // 2.6.3.2. Append clientObject to clientObjects.
               client_objects.push_back(client_object);
@@ -2441,7 +2536,7 @@
 }
 
 void ServiceWorkerJobs::ClaimSubSteps(
-    web::Context* client_context,
+    web::Context* worker_context,
     ServiceWorkerObject* associated_service_worker,
     std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ClaimSubSteps()");
@@ -2449,14 +2544,14 @@
 
   // Check if the client web context is still active. This may trigger if
   // Clients.claim() was called and service worker installation fails.
-  if (!IsWebContextRegistered(client_context)) {
+  if (!IsWebContextRegistered(worker_context)) {
     promise_reference.release();
     return;
   }
 
   // Parallel sub steps (3) for algorithm for Clients.claim():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-clients-claim
-  std::list<web::EnvironmentSettings*> target_clients;
+  std::list<web::Context*> target_clients;
 
   // 3.1. For each service worker client client where the result of running
   //      obtain a storage key given client equals the service worker's
@@ -2464,11 +2559,11 @@
   const url::Origin& storage_key =
       associated_service_worker->containing_service_worker_registration()
           ->storage_key();
-  for (auto& context : web_context_registrations_) {
+  for (auto& client : web_context_registrations_) {
     // Don't claim to be our own service worker.
-    if (context == client_context) continue;
-    web::EnvironmentSettings* client = context->environment_settings();
-    url::Origin client_storage_key = client->ObtainStorageKey();
+    if (client == worker_context) continue;
+    url::Origin client_storage_key =
+        client->environment_settings()->ObtainStorageKey();
     if (client_storage_key.IsSameOriginWith(storage_key)) {
       // 3.1.1. If client’s execution ready flag is unset or client’s discarded
       //        flag is set, continue.
@@ -2485,10 +2580,10 @@
       //        Registration given storage key and client’s creation URL.
       // TODO(b/234659851): Investigate whether this should use the creation
       // URL directly instead.
-      const GURL& base_url = client->creation_url();
+      const GURL& base_url = client->environment_settings()->creation_url();
       GURL client_url = base_url.Resolve("");
       scoped_refptr<ServiceWorkerRegistrationObject> registration =
-          scope_to_registration_map_.MatchServiceWorkerRegistration(
+          scope_to_registration_map_->MatchServiceWorkerRegistration(
               client_storage_key, client_url);
 
       // 3.1.5. If registration is not the service worker's containing service
@@ -2500,14 +2595,13 @@
 
       // 3.1.6. If client’s active service worker is not the service worker,
       //        then:
-      if (client->context()->active_service_worker() !=
-          associated_service_worker) {
+      if (client->active_service_worker() != associated_service_worker) {
         // 3.1.6.1. Invoke Handle Service Worker Client Unload with client as
         //          the argument.
         HandleServiceWorkerClientUnload(client);
 
         // 3.1.6.2. Set client’s active service worker to service worker.
-        client->context()->set_active_service_worker(associated_service_worker);
+        client->set_active_service_worker(associated_service_worker);
 
         // 3.1.6.3. Invoke Notify Controller Change algorithm with client as the
         //          argument.
@@ -2516,7 +2610,7 @@
     }
   }
   // 3.2. Resolve promise with undefined.
-  client_context->message_loop()->task_runner()->PostTask(
+  worker_context->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](std::unique_ptr<script::ValuePromiseVoid::Reference> promise) {
@@ -2526,87 +2620,103 @@
 }
 
 void ServiceWorkerJobs::ServiceWorkerPostMessageSubSteps(
-    ServiceWorkerObject* service_worker,
-    web::EnvironmentSettings* incumbent_settings,
+    ServiceWorkerObject* service_worker, web::Context* incumbent_client,
     std::unique_ptr<script::DataBuffer> serialize_result) {
   // Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-postmessage-options
   // 3. Let incumbentGlobal be incumbentSettings’s global object.
+  // Note: The 'incumbent' is the sender of the message.
   // 6.1 If the result of running the Run Service Worker algorithm with
   //     serviceWorker is failure, then return.
   auto* run_result = RunServiceWorker(service_worker);
   if (!run_result) return;
+  if (!serialize_result) return;
 
   // 6.2 Queue a task on the DOM manipulation task source to run the following
   //     steps:
-  incumbent_settings->context()->message_loop()->task_runner()->PostTask(
+  incumbent_client->message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](ServiceWorkerObject* service_worker,
-             web::EnvironmentSettings* incumbent_settings,
+             web::Context* incumbent_client,
              std::unique_ptr<script::DataBuffer> serialize_result) {
-            web::WindowOrWorkerGlobalScope* incumbent_global =
-                incumbent_settings->context()->GetWindowOrWorkerGlobalScope();
+            if (!serialize_result) return;
 
             web::EventTarget* event_target =
                 service_worker->worker_global_scope();
             if (!event_target) return;
 
-            ExtendableMessageEventInit init_dict;
-            if (incumbent_global->GetWrappableType() ==
-                base::GetTypeId<ServiceWorkerGlobalScope>()) {
-              // 6.2.1. Let source be determined by switching on the
-              //        type of incumbentGlobal:
-              //        . ServiceWorkerGlobalScope
-              //          The result of getting the service worker
-              //          object that represents incumbentGlobal’s
-              //          service worker in the relevant settings
-              //          object of serviceWorker’s global object.
-              init_dict.set_source(ExtendableMessageEvent::SourceType(
-                  event_target->environment_settings()
-                      ->context()
-                      ->GetServiceWorker(incumbent_global->AsServiceWorker()
-                                             ->service_worker_object())));
-            } else if (incumbent_global->GetWrappableType() ==
-                       base::GetTypeId<dom::Window>()) {
-              //        . Window
-              //          a new WindowClient object that represents
-              //          incumbentGlobal’s relevant settings object.
-              init_dict.set_source(
-                  ExtendableMessageEvent::SourceType(WindowClient::Create(
-                      WindowData(incumbent_global->environment_settings()))));
-            } else {
-              //        . Otherwise
-              //          a new Client object that represents
-              //          incumbentGlobal’s associated worker
-              init_dict.set_source(ExtendableMessageEvent::SourceType(
-                  Client::Create(incumbent_global->environment_settings())));
-            }
-
+            web::WindowOrWorkerGlobalScope* incumbent_global =
+                incumbent_client->GetWindowOrWorkerGlobalScope();
+            DCHECK_EQ(incumbent_client->environment_settings(),
+                      incumbent_global->environment_settings());
+            base::TypeId incumbent_type = incumbent_global->GetWrappableType();
+            ServiceWorkerObject* incumbent_worker =
+                incumbent_global->IsServiceWorker()
+                    ? incumbent_global->AsServiceWorker()
+                          ->service_worker_object()
+                    : nullptr;
             base::MessageLoop* message_loop =
                 event_target->environment_settings()->context()->message_loop();
             if (!message_loop) {
               return;
             }
-            if (!serialize_result) {
-              return;
-            }
             message_loop->task_runner()->PostTask(
                 FROM_HERE,
                 base::BindOnce(
-                    [](const ExtendableMessageEventInit& init_dict,
+                    [](const base::TypeId& incumbent_type,
+                       ServiceWorkerObject* incumbent_worker,
+                       web::Context* incumbent_client,
                        web::EventTarget* event_target,
                        std::unique_ptr<script::DataBuffer> serialize_result) {
+                      ExtendableMessageEventInit init_dict;
+                      if (incumbent_type ==
+                          base::GetTypeId<ServiceWorkerGlobalScope>()) {
+                        // 6.2.1. Let source be determined by switching on the
+                        //        type of incumbentGlobal:
+                        //        . ServiceWorkerGlobalScope
+                        //          The result of getting the service worker
+                        //          object that represents incumbentGlobal’s
+                        //          service worker in the relevant settings
+                        //          object of serviceWorker’s global object.
+                        init_dict.set_source(ExtendableMessageEvent::SourceType(
+                            event_target->environment_settings()
+                                ->context()
+                                ->GetServiceWorker(incumbent_worker)));
+                      } else if (incumbent_type ==
+                                 base::GetTypeId<dom::Window>()) {
+                        //        . Window
+                        //          a new WindowClient object that represents
+                        //          incumbentGlobal’s relevant settings object.
+                        init_dict.set_source(ExtendableMessageEvent::SourceType(
+                            WindowClient::Create(WindowData(
+                                incumbent_client->environment_settings()))));
+                      } else {
+                        //        . Otherwise
+                        //          a new Client object that represents
+                        //          incumbentGlobal’s associated worker
+                        init_dict.set_source(
+                            ExtendableMessageEvent::SourceType(Client::Create(
+                                incumbent_client->environment_settings())));
+                      }
+
                       event_target->DispatchEvent(
                           new worker::ExtendableMessageEvent(
                               base::Tokens::message(), init_dict,
                               std::move(serialize_result)));
                     },
-                    init_dict, base::Unretained(event_target),
+                    incumbent_type, base::Unretained(incumbent_worker),
+                    // Note: These should probably be weak pointers for when
+                    // the message sender disappears before the recipient
+                    // processes the event, but since base::WeakPtr
+                    // dereferencing isn't thread-safe, that can't actually be
+                    // used here.
+                    base::Unretained(incumbent_client),
+                    base::Unretained(event_target),
                     std::move(serialize_result)));
           },
-          base::Unretained(service_worker),
-          base::Unretained(incumbent_settings), std::move(serialize_result)));
+          base::Unretained(service_worker), base::Unretained(incumbent_client),
+          std::move(serialize_result)));
 }
 
 void ServiceWorkerJobs::RegisterWebContext(web::Context* context) {
@@ -2624,6 +2734,27 @@
   web_context_registrations_.insert(context);
 }
 
+void ServiceWorkerJobs::SetActiveWorker(web::EnvironmentSettings* client) {
+  if (!client) return;
+  if (base::MessageLoop::current() != message_loop()) {
+    DCHECK(message_loop());
+    message_loop()->task_runner()->PostTask(
+        FROM_HERE, base::Bind(&ServiceWorkerJobs::SetActiveWorker,
+                              base::Unretained(this), client));
+    return;
+  }
+  DCHECK(scope_to_registration_map_);
+  scoped_refptr<ServiceWorkerRegistrationObject> client_registration =
+      scope_to_registration_map_->MatchServiceWorkerRegistration(
+          client->ObtainStorageKey(), client->creation_url());
+  if (client_registration.get() && client_registration->active_worker()) {
+    client->context()->set_active_service_worker(
+        client_registration->active_worker());
+  } else {
+    client->context()->set_active_service_worker(nullptr);
+  }
+}
+
 void ServiceWorkerJobs::UnregisterWebContext(web::Context* context) {
   DCHECK_NE(nullptr, context);
   if (base::MessageLoop::current() != message_loop()) {
@@ -2637,12 +2768,49 @@
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
   DCHECK_EQ(1, web_context_registrations_.count(context));
   web_context_registrations_.erase(context);
-  HandleServiceWorkerClientUnload(context->environment_settings());
+  HandleServiceWorkerClientUnload(context);
+  PrepareForClientShutdown(context);
   if (web_context_registrations_.empty()) {
     web_context_registrations_cleared_.Signal();
   }
 }
 
+void ServiceWorkerJobs::PrepareForClientShutdown(web::Context* client) {
+  DCHECK(client);
+  if (!client) return;
+  DCHECK(base::MessageLoop::current() == message_loop());
+  // Note: This could be rewritten to use the decomposition declaration
+  // 'const auto& [scope, queue]' after switching to C++17.
+  for (const auto& entry : job_queue_map_) {
+    const std::string& scope = entry.first;
+    const std::unique_ptr<JobQueue>& queue = entry.second;
+    DCHECK(queue.get());
+    queue->PrepareForClientShutdown(client);
+  }
+}
+
+void ServiceWorkerJobs::JobQueue::PrepareForClientShutdown(
+    web::Context* client) {
+  for (const auto& job : jobs_) {
+    PrepareJobForClientShutdown(job, client);
+  }
+}
+
+void ServiceWorkerJobs::JobQueue::PrepareJobForClientShutdown(
+    const std::unique_ptr<Job>& job, web::Context* client) {
+  DCHECK(job);
+  if (!job) return;
+  base::AutoLock lock(job->equivalent_jobs_promise_mutex);
+  if (client == job->client) {
+    job->promise.reset();
+    job->client = nullptr;
+  }
+  for (const auto& equivalent_job : job->equivalent_jobs) {
+    // Recurse for the equivalent jobs.
+    PrepareJobForClientShutdown(equivalent_job, client);
+  }
+}
+
 ServiceWorkerJobs::JobPromiseType::JobPromiseType(
     std::unique_ptr<script::ValuePromiseBool::Reference> promise_reference)
     : promise_bool_reference_(std::move(promise_reference)) {}
diff --git a/cobalt/worker/service_worker_jobs.h b/cobalt/worker/service_worker_jobs.h
index 7eb3370..12181f5 100644
--- a/cobalt/worker/service_worker_jobs.h
+++ b/cobalt/worker/service_worker_jobs.h
@@ -18,7 +18,6 @@
 #include <deque>
 #include <map>
 #include <memory>
-#include <queue>
 #include <set>
 #include <string>
 #include <utility>
@@ -37,8 +36,9 @@
 #include "cobalt/script/promise.h"
 #include "cobalt/script/script_value.h"
 #include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/context.h"
 #include "cobalt/web/dom_exception.h"
-#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/web_settings.h"
 #include "cobalt/worker/client_query_options.h"
 #include "cobalt/worker/frame_type.h"
 #include "cobalt/worker/service_worker.h"
@@ -101,7 +101,7 @@
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job
   struct Job {
     Job(JobType type, const url::Origin& storage_key, const GURL& scope_url,
-        const GURL& script_url, web::EnvironmentSettings* client,
+        const GURL& script_url, web::Context* client,
         std::unique_ptr<JobPromiseType> promise)
         : type(type),
           storage_key(storage_key),
@@ -123,7 +123,7 @@
     GURL scope_url;
     GURL script_url;
     ServiceWorkerUpdateViaCache update_via_cache;
-    web::EnvironmentSettings* client;
+    web::Context* client;
     GURL referrer;
     std::unique_ptr<JobPromiseType> promise;
     JobQueue* containing_job_queue = nullptr;
@@ -150,13 +150,13 @@
     }
     void Enqueue(std::unique_ptr<Job> job) {
       base::AutoLock lock(mutex_);
-      jobs_.push(std::move(job));
+      jobs_.push_back(std::move(job));
     }
     std::unique_ptr<Job> Dequeue() {
       base::AutoLock lock(mutex_);
       std::unique_ptr<Job> job;
       job.swap(jobs_.front());
-      jobs_.pop();
+      jobs_.pop_front();
       return job;
     }
     Job* FirstItem() {
@@ -173,47 +173,57 @@
           job, std::move(lock));
     }
 
+    // Ensure no references are kept to JS objects for a client that is about to
+    // be shutdown.
+    void PrepareForClientShutdown(web::Context* client);
+
+    // Helper method for PrepareForClientShutdown to help with recursion to
+    // equivalent jobs.
+    void PrepareJobForClientShutdown(const std::unique_ptr<Job>& job,
+                                     web::Context* client);
+
    private:
     base::Lock mutex_;
-    std::queue<std::unique_ptr<Job>> jobs_;
+    std::deque<std::unique_ptr<Job>> jobs_;
   };
 
-  ServiceWorkerJobs(network::NetworkModule* network_module,
+  ServiceWorkerJobs(web::WebSettings* web_settings,
+                    network::NetworkModule* network_module,
+                    web::UserAgentPlatformInfo* platform_info,
                     base::MessageLoop* message_loop);
   ~ServiceWorkerJobs();
 
   void Stop();
 
   base::MessageLoop* message_loop() { return message_loop_; }
-  network::NetworkModule* network_module() { return network_module_; }
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#start-register-algorithm
   void StartRegister(const base::Optional<GURL>& scope_url,
                      const GURL& script_url,
                      std::unique_ptr<script::ValuePromiseWrappable::Reference>
                          promise_reference,
-                     web::EnvironmentSettings* client, const WorkerType& type,
+                     web::Context* client, const WorkerType& type,
                      const ServiceWorkerUpdateViaCache& update_via_cache);
 
-  void MaybeResolveReadyPromiseSubSteps(web::EnvironmentSettings* client);
+  void MaybeResolveReadyPromiseSubSteps(web::Context* client);
 
   // Sub steps (8) of ServiceWorkerContainer.getRegistration().
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
   void GetRegistrationSubSteps(
       const url::Origin& storage_key, const GURL& client_url,
-      web::EnvironmentSettings* client,
+      web::Context* client,
       std::unique_ptr<script::ValuePromiseWrappable::Reference>
           promise_reference);
 
   void GetRegistrationsSubSteps(
-      const url::Origin& storage_key, web::EnvironmentSettings* client,
+      const url::Origin& storage_key, web::Context* client,
       std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
           promise_reference);
 
   // Sub steps (2) of ServiceWorkerGlobalScope.skipWaiting().
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworkerglobalscope-skipwaiting
   void SkipWaitingSubSteps(
-      web::Context* client_context, ServiceWorkerObject* service_worker,
+      web::Context* worker_context, ServiceWorkerObject* service_worker,
       std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
 
   // Sub steps for ExtendableEvent.WaitUntil().
@@ -223,7 +233,7 @@
   // Parallel sub steps (2) for algorithm for Clients.get(id):
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-get
   void ClientsGetSubSteps(
-      web::Context* client_context,
+      web::Context* worker_context,
       ServiceWorkerObject* associated_service_worker,
       std::unique_ptr<script::ValuePromiseWrappable::Reference>
           promise_reference,
@@ -232,14 +242,14 @@
   // Algorithm for Resolve Get Client Promise:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-get-client-promise
   void ResolveGetClientPromise(
-      web::EnvironmentSettings* client, web::Context* promise_context,
+      web::Context* client, web::Context* worker_context,
       std::unique_ptr<script::ValuePromiseWrappable::Reference>
           promise_reference);
 
   // Parallel sub steps (2) for algorithm for Clients.matchAll():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-matchall
   void ClientsMatchAllSubSteps(
-      web::Context* client_context,
+      web::Context* worker_context,
       ServiceWorkerObject* associated_service_worker,
       std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
           promise_reference,
@@ -248,31 +258,38 @@
   // Parallel sub steps (3) for algorithm for Clients.claim():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-clients-claim
   void ClaimSubSteps(
-      web::Context* client_context,
+      web::Context* worker_context,
       ServiceWorkerObject* associated_service_worker,
       std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
 
   // Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-postmessage-options
   void ServiceWorkerPostMessageSubSteps(
-      ServiceWorkerObject* service_worker,
-      web::EnvironmentSettings* incumbent_settings,
+      ServiceWorkerObject* service_worker, web::Context* incumbent_client,
       std::unique_ptr<script::DataBuffer> serialize_result);
 
   // Registration of web contexts that may have service workers.
   void RegisterWebContext(web::Context* context);
   void UnregisterWebContext(web::Context* context);
   bool IsWebContextRegistered(web::Context* context) {
+    DCHECK(base::MessageLoop::current() == message_loop());
     return web_context_registrations_.end() !=
            web_context_registrations_.find(context);
   }
 
+  // Ensure no references are kept to JS objects for a client that is about to
+  // be shutdown.
+  void PrepareForClientShutdown(web::Context* client);
+
+  // Set the active worker for a client if there is a matching service worker.
+  void SetActiveWorker(web::EnvironmentSettings* client);
+
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-job
   std::unique_ptr<Job> CreateJob(
       JobType type, const url::Origin& storage_key, const GURL& scope_url,
       const GURL& script_url,
       std::unique_ptr<script::ValuePromiseWrappable::Reference> promise,
-      web::EnvironmentSettings* client) {
+      web::Context* client) {
     return CreateJob(type, storage_key, scope_url, script_url,
                      JobPromiseType::Create(std::move(promise)), client);
   }
@@ -280,36 +297,46 @@
       JobType type, const url::Origin& storage_key, const GURL& scope_url,
       const GURL& script_url,
       std::unique_ptr<script::ValuePromiseBool::Reference> promise,
-      web::EnvironmentSettings* client) {
+      web::Context* client) {
     return CreateJob(type, storage_key, scope_url, script_url,
                      JobPromiseType::Create(std::move(promise)), client);
   }
-  std::unique_ptr<Job> CreateJob(JobType type, const url::Origin& storage_key,
-                                 const GURL& scope_url, const GURL& script_url,
-                                 std::unique_ptr<JobPromiseType> promise,
-                                 web::EnvironmentSettings* client);
+  std::unique_ptr<Job> CreateJob(
+      JobType type, const url::Origin& storage_key, const GURL& scope_url,
+      const GURL& script_url, std::unique_ptr<JobPromiseType> promise = nullptr,
+      web::Context* client = nullptr);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#schedule-job
   void ScheduleJob(std::unique_ptr<Job> job);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#activation-algorithm
-  void Activate(scoped_refptr<ServiceWorkerRegistrationObject> registration);
+  void Activate(ServiceWorkerRegistrationObject* registration);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clear-registration-algorithm
-  void ClearRegistration(
-      scoped_refptr<ServiceWorkerRegistrationObject> registration);
+  void ClearRegistration(ServiceWorkerRegistrationObject* registration);
+
+  // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-state-algorithm
+  void UpdateWorkerState(ServiceWorkerObject* worker, ServiceWorkerState state);
+
+  // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#soft-update
+  void SoftUpdate(scoped_refptr<ServiceWorkerRegistrationObject> registration,
+                  bool force_bypass_cache = false);
 
  private:
   // State used for the 'Update' algorithm.
   struct UpdateJobState : public base::RefCounted<UpdateJobState> {
-    UpdateJobState(Job* job,
-                   scoped_refptr<ServiceWorkerRegistrationObject> registration,
-                   ServiceWorkerObject* newest_worker)
+    UpdateJobState(
+        Job* job,
+        const scoped_refptr<ServiceWorkerRegistrationObject>& registration,
+        ServiceWorkerObject* newest_worker)
         : job(job), registration(registration), newest_worker(newest_worker) {}
     Job* job;
     scoped_refptr<ServiceWorkerRegistrationObject> registration;
     ServiceWorkerObject* newest_worker;
 
+    // Headers received with the main service worker script load.
+    scoped_refptr<net::HttpResponseHeaders> script_headers;
+
     // map of content or resources for the worker.
     ScriptResourceMap updated_resource_map;
 
@@ -346,7 +373,7 @@
   enum RegistrationState { kInstalling, kWaiting, kActive };
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-equivalent
-  bool EquivalentJobs(Job* one, Job* two);
+  bool ReturnJobsAreEquivalent(Job* one, Job* two);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-job-algorithm
   void RunJob(JobQueue* job_queue);
@@ -381,21 +408,17 @@
   void RejectJobPromise(Job* job, const PromiseErrorData& error_data);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-job-promise-algorithm
-  void ResolveJobPromise(Job* job,
-                         scoped_refptr<ServiceWorkerRegistrationObject> value) {
+  void ResolveJobPromise(
+      Job* job, const scoped_refptr<ServiceWorkerRegistrationObject>& value) {
     ResolveJobPromise(job, false, value);
   }
-  void ResolveJobPromise(
-      Job* job, bool value,
-      scoped_refptr<ServiceWorkerRegistrationObject> registration = nullptr);
+  void ResolveJobPromise(Job* job, bool value,
+                         const scoped_refptr<ServiceWorkerRegistrationObject>&
+                             registration = nullptr);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#finish-job-algorithm
   void FinishJob(Job* job);
 
-  // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
-  ServiceWorker* GetNewestWorker(
-      scoped_refptr<ServiceWorkerRegistrationObject> registration);
-
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-service-worker-algorithm
   // The return value is a 'Completion or failure'.
   // A failure is signaled by returning nullptr. Otherwise, the returned string
@@ -405,48 +428,44 @@
                                 bool force_bypass_cache = false);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
-  void Install(Job* job, scoped_refptr<ServiceWorkerObject> worker,
-               scoped_refptr<ServiceWorkerRegistrationObject> registration);
+  void Install(
+      Job* job, const scoped_refptr<ServiceWorkerObject>& worker,
+      const scoped_refptr<ServiceWorkerRegistrationObject>& registration);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-activate-algorithm
-  void TryActivate(scoped_refptr<ServiceWorkerRegistrationObject> registration);
+  void TryActivate(ServiceWorkerRegistrationObject* registration);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-has-no-pending-events
   bool ServiceWorkerHasNoPendingEvents(ServiceWorkerObject* worker);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-registration-state-algorithm
   void UpdateRegistrationState(
-      scoped_refptr<ServiceWorkerRegistrationObject> registration,
-      RegistrationState target, scoped_refptr<ServiceWorkerObject> source);
-
-  // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-state-algorithm
-  void UpdateWorkerState(ServiceWorkerObject* worker, ServiceWorkerState state);
+      ServiceWorkerRegistrationObject* registration, RegistrationState target,
+      const scoped_refptr<ServiceWorkerObject>& source);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-client-unload-algorithm
-  void HandleServiceWorkerClientUnload(web::EnvironmentSettings* client);
+  void HandleServiceWorkerClientUnload(web::Context* client);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#terminate-service-worker
   void TerminateServiceWorker(ServiceWorkerObject* worker);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#notify-controller-change-algorithm
-  void NotifyControllerChange(web::EnvironmentSettings* client);
+  void NotifyControllerChange(web::Context* client);
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-clear-registration-algorithm
-  void TryClearRegistration(
-      scoped_refptr<ServiceWorkerRegistrationObject> registration);
+  void TryClearRegistration(ServiceWorkerRegistrationObject* registration);
 
   bool IsAnyClientUsingRegistration(
-      scoped_refptr<ServiceWorkerRegistrationObject> registration);
+      ServiceWorkerRegistrationObject* registration);
 
   // FetcherFactory that is used to create a fetcher according to URL.
   std::unique_ptr<loader::FetcherFactory> fetcher_factory_;
   // LoaderFactory that is used to acquire references to resources from a URL.
   std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_;
-  network::NetworkModule* network_module_;
   base::MessageLoop* message_loop_;
 
   JobQueueMap job_queue_map_;
-  ServiceWorkerRegistrationMap scope_to_registration_map_;
+  std::unique_ptr<ServiceWorkerRegistrationMap> scope_to_registration_map_;
 
   std::set<web::Context*> web_context_registrations_;
 
diff --git a/cobalt/worker/service_worker_object.cc b/cobalt/worker/service_worker_object.cc
index 4e85305..eb8171c 100644
--- a/cobalt/worker/service_worker_object.cc
+++ b/cobalt/worker/service_worker_object.cc
@@ -46,9 +46,6 @@
 
 ServiceWorkerObject::~ServiceWorkerObject() {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerObject::~ServiceWorkerObject()");
-  // Check that the object isn't destroyed without first calling Abort().
-  DCHECK(!web_agent_);
-  DCHECK(!web_context_);
   Abort();
 }
 
@@ -57,10 +54,13 @@
   if (web_agent_) {
     DCHECK(message_loop());
     DCHECK(web_context_);
-    web_agent_->WaitUntilDone();
-    web_agent_->Stop();
-    web_agent_.reset();
+    std::unique_ptr<web::Agent> web_agent(std::move(web_agent_));
+    DCHECK(web_agent);
+    DCHECK(!web_agent_);
+    web_agent->WaitUntilDone();
     web_context_ = nullptr;
+    web_agent->Stop();
+    web_agent.reset();
   }
 }
 
@@ -71,15 +71,19 @@
   // storing in the map.
   auto entry = script_resource_map_.find(url);
   if (entry != script_resource_map_.end()) {
-    if (entry->second.get() != resource) {
+    if (entry->second.content.get() != resource) {
       // The map has an entry, but it's different than the given one, make a
       // copy and replace.
-      entry->second.reset(new std::string(*resource));
+      entry->second.content.reset(new std::string(*resource));
     }
     return;
   }
 
-  script_resource_map_[url].reset(new std::string(*resource));
+  auto result = script_resource_map_.emplace(std::make_pair(
+      url,
+      ScriptResource(std::make_unique<std::string>(std::string(*resource)))));
+  // Assert that the insert was successful.
+  DCHECK(result.second);
 }
 
 bool ServiceWorkerObject::HasScriptResource() const {
@@ -87,9 +91,10 @@
          script_resource_map_.end() != script_resource_map_.find(script_url_);
 }
 
-std::string* ServiceWorkerObject::LookupScriptResource(const GURL& url) const {
+const ScriptResource* ServiceWorkerObject::LookupScriptResource(
+    const GURL& url) const {
   auto entry = script_resource_map_.find(url);
-  return entry != script_resource_map_.end() ? entry->second.get() : nullptr;
+  return entry != script_resource_map_.end() ? &entry->second : nullptr;
 }
 
 void ServiceWorkerObject::PurgeScriptResourceMap() {
@@ -117,7 +122,6 @@
 #if defined(ENABLE_DEBUGGER)
   debug_module_.reset();
 #endif  // ENABLE_DEBUGGER
-
   worker_global_scope_ = nullptr;
 }
 
@@ -211,7 +215,17 @@
   // 8.10. If the run CSP initialization for a global object algorithm returns
   //       "Blocked" when executed upon workerGlobalScope, set startFailed to
   //       true and abort these steps.
-  // TODO(b/225037465): Implement CSP check.
+  const ScriptResource* script_resource = LookupScriptResource(script_url_);
+  DCHECK(script_resource);
+  csp::ResponseHeaders csp_headers(script_resource->headers);
+  DCHECK(service_worker_global_scope);
+  DCHECK(service_worker_global_scope->csp_delegate());
+  if (!service_worker_global_scope->csp_delegate()->OnReceiveHeaders(
+          csp_headers)) {
+    // https://www.w3.org/TR/service-workers/#content-security-policy
+    DLOG(WARNING) << "Warning: No Content Security Header received for the "
+                     "service worker.";
+  }
   // 8.11. If serviceWorker is an active worker, and there are any tasks queued
   //       in serviceWorker’s containing service worker registration’s task
   //       queues, queue them to serviceWorker’s event loop’s task queues in the
@@ -224,11 +238,11 @@
 
   bool mute_errors = false;
   bool succeeded = false;
-  std::string* content = LookupScriptResource(script_url_);
-  DCHECK(content);
+  DCHECK(script_resource->content.get());
   base::SourceLocation script_location(script_url().spec(), 1, 1);
   std::string retval = web_context_->script_runner()->Execute(
-      *content, script_location, mute_errors, &succeeded);
+      *script_resource->content.get(), script_location, mute_errors,
+      &succeeded);
   // 8.13.2. If evaluationStatus.[[Value]] is empty, this means the script was
   //         not evaluated. Set startFailed to true and abort these steps.
   // We don't actually have access to an 'evaluationStatus' from ScriptRunner,
diff --git a/cobalt/worker/service_worker_object.h b/cobalt/worker/service_worker_object.h
index e1f14e4..88a9d97 100644
--- a/cobalt/worker/service_worker_object.h
+++ b/cobalt/worker/service_worker_object.h
@@ -27,6 +27,7 @@
 #include "base/message_loop/message_loop_current.h"
 #include "cobalt/web/agent.h"
 #include "cobalt/web/context.h"
+#include "cobalt/web/web_settings.h"
 #include "cobalt/worker/service_worker_state.h"
 #include "cobalt/worker/worker_global_scope.h"
 #include "starboard/atomic.h"
@@ -56,12 +57,14 @@
  public:
   // Worker Options needed at thread run time.
   struct Options {
-    Options(
-        const std::string& name, network::NetworkModule* network_module,
-        ServiceWorkerRegistrationObject* containing_service_worker_registration)
+    Options(const std::string& name, web::WebSettings* web_settings,
+            network::NetworkModule* network_module,
+            const scoped_refptr<ServiceWorkerRegistrationObject>&
+                containing_service_worker_registration)
         : name(name),
           containing_service_worker_registration(
               containing_service_worker_registration) {
+      web_options.web_settings = web_settings;
       web_options.network_module = network_module;
     }
 
@@ -98,6 +101,7 @@
   void AppendToSetOfUsedScripts(const GURL& url) {
     set_of_used_scripts_.insert(url);
   }
+  std::set<GURL> set_of_used_scripts() { return set_of_used_scripts_; }
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-script-resource-map
   void set_script_resource_map(ScriptResourceMap&& resource_map) {
@@ -105,7 +109,7 @@
   }
   void SetScriptResource(const GURL& url, std::string* resource);
   bool HasScriptResource() const;
-  std::string* LookupScriptResource(const GURL& url) const;
+  const ScriptResource* LookupScriptResource(const GURL& url) const;
 
   // Steps 13-15 of Algorithm for Install.
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
@@ -115,8 +119,8 @@
   }
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-start-status
-  void set_start_status(std::string* start_status) {
-    start_status_.reset(start_status);
+  void set_start_status(std::unique_ptr<std::string> start_status) {
+    start_status_.reset(start_status.release());
   }
   std::string* start_status() const { return start_status_.get(); }
 
@@ -140,6 +144,8 @@
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#should-skip-event-algorithm
   bool ShouldSkipEvent(base::Token event_name);
 
+  std::string options_name() { return options_.name; }
+
  private:
   // Called by ObtainWebAgentAndWaitUntilDone to perform initialization required
   // on the dedicated thread.
diff --git a/cobalt/worker/service_worker_persistent_settings.cc b/cobalt/worker/service_worker_persistent_settings.cc
new file mode 100644
index 0000000..3befd91
--- /dev/null
+++ b/cobalt/worker/service_worker_persistent_settings.cc
@@ -0,0 +1,436 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/service_worker_persistent_settings.h"
+
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/containers/flat_map.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "base/trace_event/trace_event.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/script/exception_message.h"
+#include "cobalt/script/promise.h"
+#include "cobalt/script/script_value.h"
+#include "cobalt/web/cache_utils.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/worker/service_worker_jobs.h"
+#include "cobalt/worker/service_worker_registration_object.h"
+#include "cobalt/worker/service_worker_update_via_cache.h"
+#include "cobalt/worker/worker_global_scope.h"
+#include "net/base/completion_once_callback.h"
+#include "net/disk_cache/cobalt/resource_type.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace cobalt {
+namespace worker {
+
+namespace {
+// ServiceWorkerRegistrationMap persistent settings keys.
+const char kSettingsJson[] = "service_worker_settings.json";
+const char kSettingsKeyList[] = "key_list";
+
+// ServiceWorkerRegistrationObject persistent settings keys.
+const char kSettingsStorageKeyKey[] = "storage_key";
+const char kSettingsScopeStringKey[] = "scope_string";
+const char kSettingsScopeUrlKey[] = "scope_url";
+const char kSettingsUpdateViaCacheModeKey[] = "update_via_cache_mode";
+const char kSettingsWaitingWorkerKey[] = "waiting_worker";
+const char kSettingsActiveWorkerKey[] = "active_worker";
+const char kSettingsLastUpdateCheckTimeKey[] = "last_update_check_time";
+
+// ServicerWorkerObject persistent settings keys.
+const char kSettingsOptionsNameKey[] = "options_name";
+const char kSettingsScriptUrlKey[] = "script_url";
+const char kSettingsScriptResourceMapScriptUrlsKey[] =
+    "script_resource_map_script_urls";
+const char kSettingsSetOfUsedScriptsKey[] = "set_of_used_scripts";
+const char kSettingsSkipWaitingKey[] = "skip_waiting";
+const char kSettingsClassicScriptsImportedKey[] = "classic_scripts_imported";
+
+bool CheckPersistentValue(
+    std::string key_string, std::string settings_key,
+    base::flat_map<std::string, std::unique_ptr<base::Value>>& dict,
+    base::Value::Type type) {
+  if (!dict.contains(settings_key)) {
+    DLOG(INFO) << "Key: " << key_string << " does not contain " << settings_key;
+    return false;
+  } else if (!(dict[settings_key]->type() == type)) {
+    DLOG(INFO) << "Key: " << key_string << " " << settings_key
+               << " is of type: " << dict[settings_key]->type()
+               << ", but expected type is: " << type;
+    return false;
+  }
+  return true;
+}
+}  // namespace
+
+ServiceWorkerPersistentSettings::ServiceWorkerPersistentSettings(
+    const Options& options)
+    : options_(options) {
+  persistent_settings_.reset(
+      new cobalt::persistent_storage::PersistentSettings(kSettingsJson));
+  persistent_settings_->ValidatePersistentSettings();
+  DCHECK(persistent_settings_);
+
+  cache_.reset(cobalt::cache::Cache::GetInstance());
+  DCHECK(cache_);
+}
+
+void ServiceWorkerPersistentSettings::ReadServiceWorkerRegistrationMapSettings(
+    std::map<RegistrationMapKey,
+             scoped_refptr<ServiceWorkerRegistrationObject>>&
+        registration_map) {
+  std::vector<base::Value> key_list =
+      persistent_settings_->GetPersistentSettingAsList(kSettingsKeyList);
+  std::set<std::string> unverified_key_set;
+  for (auto& key : key_list) {
+    if (key.is_string()) {
+      unverified_key_set.insert(key.GetString());
+    }
+  }
+  for (auto& key_string : unverified_key_set) {
+    auto dict =
+        persistent_settings_->GetPersistentSettingAsDictionary(key_string);
+    if (dict.empty()) {
+      DLOG(INFO) << "Key: " << key_string << " does not exist in "
+                 << kSettingsJson;
+      continue;
+    }
+    if (!CheckPersistentValue(key_string, kSettingsStorageKeyKey, dict,
+                              base::Value::Type::STRING))
+      continue;
+    url::Origin storage_key =
+        url::Origin::Create(GURL(dict[kSettingsStorageKeyKey]->GetString()));
+
+    if (!CheckPersistentValue(key_string, kSettingsScopeUrlKey, dict,
+                              base::Value::Type::STRING))
+      continue;
+    GURL scope(dict[kSettingsScopeUrlKey]->GetString());
+
+    if (!CheckPersistentValue(key_string, kSettingsUpdateViaCacheModeKey, dict,
+                              base::Value::Type::INTEGER))
+      continue;
+    ServiceWorkerUpdateViaCache update_via_cache =
+        static_cast<ServiceWorkerUpdateViaCache>(
+            dict[kSettingsUpdateViaCacheModeKey]->GetInt());
+
+    if (!CheckPersistentValue(key_string, kSettingsScopeStringKey, dict,
+                              base::Value::Type::STRING))
+      continue;
+    std::string scope_string(dict[kSettingsScopeStringKey]->GetString());
+
+    RegistrationMapKey key(storage_key, scope_string);
+    scoped_refptr<ServiceWorkerRegistrationObject> registration(
+        new ServiceWorkerRegistrationObject(storage_key, scope,
+                                            update_via_cache));
+
+    auto worker_key = kSettingsWaitingWorkerKey;
+    if (!CheckPersistentValue(key_string, worker_key, dict,
+                              base::Value::Type::DICTIONARY)) {
+      worker_key = kSettingsActiveWorkerKey;
+      if (!CheckPersistentValue(key_string, worker_key, dict,
+                                base::Value::Type::DICTIONARY))
+        continue;
+    }
+    if (!ReadServiceWorkerObjectSettings(
+            registration, key_string, std::move(dict[worker_key]), worker_key))
+      continue;
+
+    if (CheckPersistentValue(key_string, kSettingsLastUpdateCheckTimeKey, dict,
+                             base::Value::Type::DOUBLE)) {
+      double last_update_check_time =
+          dict[kSettingsLastUpdateCheckTimeKey]->GetDouble();
+      registration->set_last_update_check_time(
+          base::Time::FromDeltaSinceWindowsEpoch(
+              base::TimeDelta::FromMicroseconds(
+                  (int64_t)dict[kSettingsLastUpdateCheckTimeKey]
+                      ->GetDouble())));
+    }
+
+    // Persisted registration and worker are valid, add the registration
+    // to the registration_map and key_set_.
+    key_set_.insert(key_string);
+    registration_map.insert(std::make_pair(key, registration));
+
+    // TODO(b/228904017)
+    // Not in spec. Run SoftUpdate on the new registration.
+    options_.service_worker_jobs->SoftUpdate(registration);
+  }
+}
+
+bool ServiceWorkerPersistentSettings::ReadServiceWorkerObjectSettings(
+    scoped_refptr<ServiceWorkerRegistrationObject> registration,
+    std::string key_string, std::unique_ptr<base::Value> value_dict,
+    std::string worker_key_string) {
+  base::Value* options_name_value = value_dict->FindKeyOfType(
+      kSettingsOptionsNameKey, base::Value::Type::STRING);
+  if (options_name_value == nullptr) return false;
+  ServiceWorkerObject::Options options(options_name_value->GetString(),
+                                       options_.web_settings,
+                                       options_.network_module, registration);
+  options.web_options.platform_info = options_.platform_info;
+  options.web_options.service_worker_jobs = options_.service_worker_jobs;
+  scoped_refptr<ServiceWorkerObject> worker(new ServiceWorkerObject(options));
+
+  base::Value* script_url_value = value_dict->FindKeyOfType(
+      kSettingsScriptUrlKey, base::Value::Type::STRING);
+  if (script_url_value == nullptr) return false;
+  worker->set_script_url(GURL(script_url_value->GetString()));
+
+  base::Value* skip_waiting_value = value_dict->FindKeyOfType(
+      kSettingsSkipWaitingKey, base::Value::Type::BOOLEAN);
+  if (skip_waiting_value == nullptr) return false;
+  if (skip_waiting_value->GetBool()) worker->set_skip_waiting();
+
+  base::Value* classic_scripts_imported_value = value_dict->FindKeyOfType(
+      kSettingsClassicScriptsImportedKey, base::Value::Type::BOOLEAN);
+  if (classic_scripts_imported_value == nullptr) return false;
+  if (classic_scripts_imported_value->GetBool())
+    worker->set_classic_scripts_imported();
+
+  worker->set_start_status(nullptr);
+
+  base::Value* used_scripts_value = value_dict->FindKeyOfType(
+      kSettingsSetOfUsedScriptsKey, base::Value::Type::LIST);
+  if (used_scripts_value == nullptr) return false;
+  std::vector<base::Value> used_scripts_list = used_scripts_value->TakeList();
+  for (int i = 0; i < used_scripts_list.size(); i++) {
+    auto script_value = std::move(used_scripts_list[i]);
+    if (script_value.is_string()) {
+      worker->AppendToSetOfUsedScripts(GURL(script_value.GetString()));
+    }
+  }
+  base::Value* script_urls_value = value_dict->FindKeyOfType(
+      kSettingsScriptResourceMapScriptUrlsKey, base::Value::Type::LIST);
+  if (script_urls_value == nullptr) return false;
+  std::vector<base::Value> script_urls_list = script_urls_value->TakeList();
+  ScriptResourceMap script_resource_map;
+  for (int i = 0; i < script_urls_list.size(); i++) {
+    auto script_url_value = std::move(script_urls_list[i]);
+    if (script_url_value.is_string()) {
+      auto script_url_string = script_url_value.GetString();
+      auto script_url = GURL(script_url_string);
+      std::unique_ptr<std::vector<uint8_t>> data = cache_->Retrieve(
+          disk_cache::ResourceType::kServiceWorkerScript,
+          web::cache_utils::GetKey(key_string + script_url_string));
+      if (data == nullptr) {
+        return false;
+      }
+      std::string script_string(data->begin(), data->end());
+      auto result = script_resource_map.insert(std::make_pair(
+          script_url,
+          ScriptResource(std::make_unique<std::string>(script_string))));
+      DCHECK(result.second);
+    }
+  }
+  if (script_resource_map.size() == 0) {
+    return false;
+  }
+  worker->set_script_resource_map(std::move(script_resource_map));
+
+  options_.service_worker_jobs->UpdateWorkerState(worker,
+                                                  kServiceWorkerStateActivated);
+  registration->set_active_worker(worker);
+
+  return true;
+}
+
+void ServiceWorkerPersistentSettings::
+    WriteServiceWorkerRegistrationObjectSettings(
+        RegistrationMapKey key,
+        scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+  auto key_string = key.first.GetURL().spec() + key.second;
+  base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+
+  // https://w3c.github.io/ServiceWorker/#user-agent-shutdown
+  // An installing worker does not persist, but is discarded.
+  auto waiting_worker = registration->waiting_worker();
+  auto active_worker = registration->active_worker();
+
+  if (waiting_worker == nullptr && active_worker == nullptr) {
+    // If the installing worker was the only service worker for the service
+    // worker registration, the service worker registration is discarded.
+    RemoveServiceWorkerRegistrationObjectSettings(key);
+    return;
+  }
+
+  if (waiting_worker) {
+    // A waiting worker promotes to an active worker. This will be handled
+    // upon restart.
+    dict.try_emplace(
+        kSettingsWaitingWorkerKey,
+        WriteServiceWorkerObjectSettings(key_string, waiting_worker));
+  } else {
+    dict.try_emplace(kSettingsActiveWorkerKey, WriteServiceWorkerObjectSettings(
+                                                   key_string, active_worker));
+  }
+
+  // Add key_string to the registered keys and write to persistent settings.
+  key_set_.insert(key_string);
+  std::vector<base::Value> key_list;
+  for (auto& key : key_set_) {
+    key_list.emplace_back(key);
+  }
+  persistent_settings_->SetPersistentSetting(
+      kSettingsKeyList, std::make_unique<base::Value>(std::move(key_list)));
+
+  // Persist ServiceWorkerRegistrationObject's fields.
+  dict.try_emplace(kSettingsStorageKeyKey,
+                   std::make_unique<base::Value>(
+                       registration->storage_key().GetURL().spec()));
+
+  dict.try_emplace(kSettingsScopeStringKey,
+                   std::make_unique<base::Value>(key.second));
+
+  dict.try_emplace(kSettingsScopeUrlKey, std::make_unique<base::Value>(
+                                             registration->scope_url().spec()));
+
+  dict.try_emplace(
+      kSettingsUpdateViaCacheModeKey,
+      std::make_unique<base::Value>(registration->update_via_cache_mode()));
+
+  dict.try_emplace(kSettingsLastUpdateCheckTimeKey,
+                   std::make_unique<base::Value>(static_cast<double>(
+                       registration->last_update_check_time()
+                           .ToDeltaSinceWindowsEpoch()
+                           .InMicroseconds())));
+
+  persistent_settings_->SetPersistentSetting(
+      key_string, std::make_unique<base::Value>(dict));
+}
+
+std::unique_ptr<base::Value>
+ServiceWorkerPersistentSettings::WriteServiceWorkerObjectSettings(
+    std::string registration_key_string,
+    const scoped_refptr<ServiceWorkerObject>& service_worker_object) {
+  base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+  DCHECK(service_worker_object);
+  dict.try_emplace(
+      kSettingsOptionsNameKey,
+      std::make_unique<base::Value>(service_worker_object->options_name()));
+
+  dict.try_emplace(kSettingsScriptUrlKey,
+                   std::make_unique<base::Value>(
+                       service_worker_object->script_url().spec()));
+
+  dict.try_emplace(
+      kSettingsSkipWaitingKey,
+      std::make_unique<base::Value>(service_worker_object->skip_waiting()));
+
+  dict.try_emplace(kSettingsClassicScriptsImportedKey,
+                   std::make_unique<base::Value>(
+                       service_worker_object->classic_scripts_imported()));
+
+  // Persist set_of_used_scripts as a List.
+  base::Value set_of_used_scripts_value(base::Value::Type::LIST);
+  for (auto script_url : service_worker_object->set_of_used_scripts()) {
+    set_of_used_scripts_value.GetList().push_back(
+        base::Value(script_url.spec()));
+  }
+  dict.try_emplace(
+      kSettingsSetOfUsedScriptsKey,
+      std::make_unique<base::Value>(std::move(set_of_used_scripts_value)));
+
+  // Persist the script_resource_map script urls as a List.
+  base::Value script_urls_value(base::Value::Type::LIST);
+  for (auto const& script_resource :
+       service_worker_object->script_resource_map()) {
+    std::string script_url_string = script_resource.first.spec();
+    script_urls_value.GetList().push_back(base::Value(script_url_string));
+    // Use Cache::Store to persist the script resource.
+    std::string resource = *(script_resource.second.content.get());
+    std::vector<uint8_t> data(resource.begin(), resource.end());
+    cache_->Store(
+        disk_cache::ResourceType::kServiceWorkerScript,
+        web::cache_utils::GetKey(registration_key_string + script_url_string),
+        data,
+        /* metadata */ base::nullopt);
+  }
+  dict.try_emplace(kSettingsScriptResourceMapScriptUrlsKey,
+                   std::make_unique<base::Value>(std::move(script_urls_value)));
+
+  return std::move(std::make_unique<base::Value>(dict));
+}
+
+void ServiceWorkerPersistentSettings::
+    RemoveServiceWorkerRegistrationObjectSettings(RegistrationMapKey key) {
+  auto key_string = key.first.GetURL().spec() + key.second;
+
+  if (key_set_.find(key_string) == key_set_.end()) {
+    // The key does not exist in PersistentSettings.
+    return;
+  }
+
+  // Remove the worker script_resource_map from the Cache.
+  RemoveServiceWorkerObjectSettings(key_string);
+
+  // Remove registration key string.
+  key_set_.erase(key_string);
+  std::vector<base::Value> key_list;
+  for (auto& key : key_set_) {
+    key_list.emplace_back(key);
+  }
+  persistent_settings_->SetPersistentSetting(
+      kSettingsKeyList, std::make_unique<base::Value>(std::move(key_list)));
+
+  // Remove the registration dictionary.
+  persistent_settings_->RemovePersistentSetting(key_string);
+}
+
+void ServiceWorkerPersistentSettings::RemoveServiceWorkerObjectSettings(
+    std::string key_string) {
+  auto dict =
+      persistent_settings_->GetPersistentSettingAsDictionary(key_string);
+  if (dict.empty()) return;
+  std::vector<std::string> worker_keys{kSettingsWaitingWorkerKey,
+                                       kSettingsActiveWorkerKey};
+  for (std::string worker_key : worker_keys) {
+    if (!CheckPersistentValue(key_string, worker_key, dict,
+                              base::Value::Type::DICTIONARY))
+      continue;
+    auto worker_dict = std::move(dict[worker_key]);
+    base::Value* script_urls_value = worker_dict->FindKeyOfType(
+        kSettingsScriptResourceMapScriptUrlsKey, base::Value::Type::LIST);
+    if (script_urls_value == nullptr) return;
+    std::vector<base::Value> script_urls_list = script_urls_value->TakeList();
+
+    for (int i = 0; i < script_urls_list.size(); i++) {
+      auto script_url_value = std::move(script_urls_list[i]);
+      if (script_url_value.is_string()) {
+        auto script_url_string = script_url_value.GetString();
+        cache_->Delete(
+            disk_cache::ResourceType::kServiceWorkerScript,
+            web::cache_utils::GetKey(key_string + script_url_string));
+      }
+    }
+  }
+}
+
+void ServiceWorkerPersistentSettings::RemoveAll() {
+  for (auto& key : key_set_) {
+    persistent_settings_->RemovePersistentSetting(key);
+  }
+}
+
+}  // namespace worker
+}  // namespace cobalt
diff --git a/cobalt/worker/service_worker_persistent_settings.h b/cobalt/worker/service_worker_persistent_settings.h
new file mode 100644
index 0000000..b260d76
--- /dev/null
+++ b/cobalt/worker/service_worker_persistent_settings.h
@@ -0,0 +1,103 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
+#define COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/containers/flat_map.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "base/threading/thread_checker.h"
+#include "cobalt/cache/cache.h"
+#include "cobalt/network/network_module.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/script/exception_message.h"
+#include "cobalt/script/promise.h"
+#include "cobalt/script/script_value.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/web_settings.h"
+#include "cobalt/worker/service_worker_registration_object.h"
+#include "cobalt/worker/service_worker_update_via_cache.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace cobalt {
+namespace worker {
+
+class ServiceWorkerPersistentSettings {
+ public:
+  struct Options {
+    Options(web::WebSettings* web_settings,
+            network::NetworkModule* network_module,
+            web::UserAgentPlatformInfo* platform_info,
+            ServiceWorkerJobs* service_worker_jobs)
+        : web_settings(web_settings),
+          network_module(network_module),
+          platform_info(platform_info),
+          service_worker_jobs(service_worker_jobs) {}
+    web::WebSettings* web_settings;
+    network::NetworkModule* network_module;
+    web::UserAgentPlatformInfo* platform_info;
+    ServiceWorkerJobs* service_worker_jobs;
+  };
+
+  explicit ServiceWorkerPersistentSettings(const Options& options);
+
+  void ReadServiceWorkerRegistrationMapSettings(
+      std::map<RegistrationMapKey,
+               scoped_refptr<ServiceWorkerRegistrationObject>>&
+          registration_map);
+
+  bool ReadServiceWorkerObjectSettings(
+      scoped_refptr<ServiceWorkerRegistrationObject> registration,
+      std::string key_string, std::unique_ptr<base::Value> value_dict,
+      std::string worker_type_string);
+
+  void WriteServiceWorkerRegistrationObjectSettings(
+      RegistrationMapKey key,
+      scoped_refptr<ServiceWorkerRegistrationObject> registration);
+
+  std::unique_ptr<base::Value> WriteServiceWorkerObjectSettings(
+      std::string registration_key_string,
+      const scoped_refptr<ServiceWorkerObject>& service_worker_object);
+
+  void RemoveServiceWorkerRegistrationObjectSettings(RegistrationMapKey key);
+
+  void RemoveServiceWorkerObjectSettings(std::string key_string);
+
+  void RemoveAll();
+
+ private:
+  Options options_;
+
+  std::unique_ptr<cobalt::persistent_storage::PersistentSettings>
+      persistent_settings_;
+
+  std::set<std::string> key_set_;
+
+  std::unique_ptr<cobalt::cache::Cache> cache_;
+};
+
+}  // namespace worker
+}  // namespace cobalt
+
+#endif  // COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
diff --git a/cobalt/worker/service_worker_registration.cc b/cobalt/worker/service_worker_registration.cc
index c63e99c..5452d8f 100644
--- a/cobalt/worker/service_worker_registration.cc
+++ b/cobalt/worker/service_worker_registration.cc
@@ -112,16 +112,14 @@
   std::unique_ptr<ServiceWorkerJobs::Job> job = jobs->CreateJob(
       ServiceWorkerJobs::JobType::kUpdate, registration_->storage_key(),
       registration_->scope_url(), newest_worker->script_url(),
-      std::move(promise_reference), environment_settings());
+      std::move(promise_reference), environment_settings()->context());
   DCHECK(!promise_reference);
 
   // 7. Set job’s worker type to newestWorker’s type.
   // Cobalt only supports 'classic' worker type.
 
   // 8. Invoke Schedule Job with job.
-  jobs->message_loop()->task_runner()->PostTask(
-      FROM_HERE, base::BindOnce(&ServiceWorkerJobs::ScheduleJob,
-                                base::Unretained(jobs), std::move(job)));
+  jobs->ScheduleJob(std::move(job));
   DCHECK(!job.get());
 }
 
@@ -142,36 +140,33 @@
   // past any previously submitted update requests.
   base::MessageLoop::current()->task_runner()->PostTask(
       FROM_HERE,
-      base::BindOnce(&ServiceWorkerRegistration::UnregisterTask,
-                     base::Unretained(this), std::move(promise_reference)));
+      base::BindOnce(
+          [](worker::ServiceWorkerJobs* jobs, const url::Origin& storage_key,
+             const GURL& scope_url,
+             std::unique_ptr<script::ValuePromiseBool::Reference>
+                 promise_reference,
+             web::Context* client) {
+            // 3. Let job be the result of running Create Job with unregister,
+            //    registration’s storage key, registration’s scope url, null,
+            //    promise, and this's relevant settings object.
+            std::unique_ptr<ServiceWorkerJobs::Job> job = jobs->CreateJob(
+                ServiceWorkerJobs::JobType::kUnregister, storage_key, scope_url,
+                GURL(), std::move(promise_reference), client);
+            DCHECK(!promise_reference);
+
+            // 4. Invoke Schedule Job with job.
+            jobs->ScheduleJob(std::move(job));
+            DCHECK(!job.get());
+          },
+          environment_settings()->context()->service_worker_jobs(),
+          registration_->storage_key(), registration_->scope_url(),
+          std::move(promise_reference), environment_settings()->context()));
   // 5. Return promise.
   return promise;
 }
 
-void ServiceWorkerRegistration::UnregisterTask(
-    std::unique_ptr<script::ValuePromiseBool::Reference> promise_reference) {
-  // Algorithm for unregister():
-  //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-unregister
-  // 3. Let job be the result of running Create Job with unregister,
-  //    registration’s storage key, registration’s scope url, null, promise, and
-  //    this's relevant settings object.
-  worker::ServiceWorkerJobs* jobs =
-      environment_settings()->context()->service_worker_jobs();
-  std::unique_ptr<ServiceWorkerJobs::Job> job = jobs->CreateJob(
-      ServiceWorkerJobs::JobType::kUnregister, registration_->storage_key(),
-      registration_->scope_url(), GURL(), std::move(promise_reference),
-      environment_settings());
-  DCHECK(!promise_reference);
-
-  // 4. Invoke Schedule Job with job.
-  jobs->message_loop()->task_runner()->PostTask(
-      FROM_HERE, base::BindOnce(&ServiceWorkerJobs::ScheduleJob,
-                                base::Unretained(jobs), std::move(job)));
-  DCHECK(!job.get());
-}
-
 std::string ServiceWorkerRegistration::scope() const {
-  return registration_->scope_url().GetContent();
+  return registration_->scope_url().spec();
 }
 
 ServiceWorkerUpdateViaCache ServiceWorkerRegistration::update_via_cache()
diff --git a/cobalt/worker/service_worker_registration.h b/cobalt/worker/service_worker_registration.h
index 202647c..cd7795f 100644
--- a/cobalt/worker/service_worker_registration.h
+++ b/cobalt/worker/service_worker_registration.h
@@ -80,9 +80,6 @@
   void UpdateTask(std::unique_ptr<script::ValuePromiseWrappable::Reference>
                       promise_reference);
 
-  void UnregisterTask(
-      std::unique_ptr<script::ValuePromiseBool::Reference> promise_reference);
-
   scoped_refptr<worker::ServiceWorkerRegistrationObject> registration_;
   scoped_refptr<ServiceWorker> installing_;
   scoped_refptr<ServiceWorker> waiting_;
diff --git a/cobalt/worker/service_worker_registration_map.cc b/cobalt/worker/service_worker_registration_map.cc
index 68ede06..b2da495 100644
--- a/cobalt/worker/service_worker_registration_map.cc
+++ b/cobalt/worker/service_worker_registration_map.cc
@@ -54,6 +54,22 @@
 
 }  // namespace
 
+ServiceWorkerRegistrationMap::ServiceWorkerRegistrationMap(
+    const ServiceWorkerPersistentSettings::Options& options) {
+  service_worker_persistent_settings_.reset(
+      new ServiceWorkerPersistentSettings(options));
+  DCHECK(service_worker_persistent_settings_);
+
+  // TODO(b/259731731) For now do not read from persisted settings until
+  // activation of persisted registrations works.
+  ReadPersistentSettings();
+}
+
+void ServiceWorkerRegistrationMap::ReadPersistentSettings() {
+  service_worker_persistent_settings_->ReadServiceWorkerRegistrationMapSettings(
+      registration_map_);
+}
+
 scoped_refptr<ServiceWorkerRegistrationObject>
 ServiceWorkerRegistrationMap::MatchServiceWorkerRegistration(
     const url::Origin& storage_key, const GURL& client_url) {
@@ -113,7 +129,6 @@
                 url::Origin::Create(client_url));
     }
   }
-
   // 9. Return the result of running Get Registration given storage key and
   // matchingScope.
   return GetRegistration(storage_key, matching_scope);
@@ -140,7 +155,7 @@
     scope_string = SerializeExcludingFragment(scope);
   }
 
-  Key registration_key(storage_key, scope_string);
+  RegistrationMapKey registration_key(storage_key, scope_string);
   // 4. For each (entry storage key, entry scope) → registration of registration
   // map:
   for (const auto& entry : registration_map_) {
@@ -194,7 +209,7 @@
                                           update_via_cache));
 
   // 4. Set registration map[(storage key, scopeString)] to registration.
-  Key registration_key(storage_key, scope_string);
+  RegistrationMapKey registration_key(storage_key, scope_string);
   registration_map_.insert(std::make_pair(
       registration_key,
       scoped_refptr<ServiceWorkerRegistrationObject>(registration)));
@@ -207,11 +222,13 @@
     const url::Origin& storage_key, const GURL& scope) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   std::string scope_string = SerializeExcludingFragment(scope);
-  Key registration_key(storage_key, scope_string);
+  RegistrationMapKey registration_key(storage_key, scope_string);
   auto entry = registration_map_.find(registration_key);
   DCHECK(entry != registration_map_.end());
   if (entry != registration_map_.end()) {
     registration_map_.erase(entry);
+    service_worker_persistent_settings_
+        ->RemoveServiceWorkerRegistrationObjectSettings(registration_key);
   }
 }
 
@@ -222,8 +239,10 @@
   // is not this service worker registration.
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-service-worker-registration-unregistered
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  Key registration_key(registration->storage_key(),
-                       registration->scope_url().spec());
+  std::string scope_string =
+      SerializeExcludingFragment(registration->scope_url());
+  RegistrationMapKey registration_key(registration->storage_key(),
+                                      scope_string);
   auto entry = registration_map_.find(registration_key);
   if (entry == registration_map_.end()) return true;
 
@@ -268,12 +287,25 @@
 }
 
 void ServiceWorkerRegistrationMap::AbortAllActive() {
-  for (auto& entry : registration_map_) {
-    const scoped_refptr<ServiceWorkerRegistrationObject>& registration =
-        entry.second;
-    if (registration->active_worker()) {
-      registration->active_worker()->Abort();
-    }
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  for (auto entry : registration_map_) {
+    entry.second->AbortAll();
+  }
+}
+
+void ServiceWorkerRegistrationMap::PersistRegistration(
+    const url::Origin& storage_key, const GURL& scope) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  std::string scope_string = SerializeExcludingFragment(scope);
+  RegistrationMapKey registration_key(storage_key, scope_string);
+  auto entry = registration_map_.find(registration_key);
+  if (entry != registration_map_.end()) {
+    service_worker_persistent_settings_
+        ->WriteServiceWorkerRegistrationObjectSettings(registration_key,
+                                                       entry->second);
+  } else {
+    service_worker_persistent_settings_
+        ->RemoveServiceWorkerRegistrationObjectSettings(registration_key);
   }
 }
 
diff --git a/cobalt/worker/service_worker_registration_map.h b/cobalt/worker/service_worker_registration_map.h
index 45f34e2..a81bacc 100644
--- a/cobalt/worker/service_worker_registration_map.h
+++ b/cobalt/worker/service_worker_registration_map.h
@@ -24,11 +24,13 @@
 #include "base/memory/ref_counted.h"
 #include "base/synchronization/lock.h"
 #include "base/threading/thread_checker.h"
+#include "cobalt/network/network_module.h"
 #include "cobalt/script/exception_message.h"
 #include "cobalt/script/promise.h"
 #include "cobalt/script/script_value.h"
 #include "cobalt/script/script_value_factory.h"
 #include "cobalt/web/environment_settings.h"
+#include "cobalt/worker/service_worker_persistent_settings.h"
 #include "cobalt/worker/service_worker_registration_object.h"
 #include "cobalt/worker/service_worker_update_via_cache.h"
 #include "url/gurl.h"
@@ -42,7 +44,9 @@
 //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-scope-to-registration-map
 class ServiceWorkerRegistrationMap {
  public:
-  using Key = std::pair<url::Origin, std::string>;
+  explicit ServiceWorkerRegistrationMap(
+      const ServiceWorkerPersistentSettings::Options& options);
+  ~ServiceWorkerRegistrationMap() { AbortAllActive(); }
 
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-registration-algorithm
   scoped_refptr<ServiceWorkerRegistrationObject> GetRegistration(
@@ -70,6 +74,13 @@
 
   void AbortAllActive();
 
+  // Called from the end of ServiceWorkerJobs Install, Activate, and Clear
+  // Registration since these are the cases in which a service worker
+  // registration's active_worker or waiting_worker are updated.
+  void PersistRegistration(const url::Origin& storage_key, const GURL& scope);
+
+  void ReadPersistentSettings();
+
  private:
   // ThreadChecker for use by the methods operating on the registration map.
   THREAD_CHECKER(thread_checker_);
@@ -77,11 +88,14 @@
   // A registration map is an ordered map where the keys are (storage key,
   // serialized scope urls) and the values are service worker registrations.
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-scope-to-registration-map
-  std::map<Key, scoped_refptr<ServiceWorkerRegistrationObject>>
+  std::map<RegistrationMapKey, scoped_refptr<ServiceWorkerRegistrationObject>>
       registration_map_;
 
   // This lock is to allow atomic operations on the registration map.
   base::Lock mutex_;
+
+  std::unique_ptr<ServiceWorkerPersistentSettings>
+      service_worker_persistent_settings_;
 };
 
 }  // namespace worker
diff --git a/cobalt/worker/service_worker_registration_object.cc b/cobalt/worker/service_worker_registration_object.cc
index ab444e9..0124895 100644
--- a/cobalt/worker/service_worker_registration_object.cc
+++ b/cobalt/worker/service_worker_registration_object.cc
@@ -28,33 +28,51 @@
     const ServiceWorkerUpdateViaCache& update_via_cache_mode)
     : storage_key_(storage_key),
       scope_url_(scope_url),
-      update_via_cache_mode_(update_via_cache_mode) {}
+      update_via_cache_mode_(update_via_cache_mode),
+      last_update_check_time_(base::Time()) {}
 
-ServiceWorkerObject* ServiceWorkerRegistrationObject::GetNewestWorker() {
+ServiceWorkerRegistrationObject::~ServiceWorkerRegistrationObject() {
+  AbortAll();
+}
+
+void ServiceWorkerRegistrationObject::AbortAll() {
+  if (installing_worker()) {
+    installing_worker()->Abort();
+  }
+  if (waiting_worker()) {
+    waiting_worker()->Abort();
+  }
+  if (active_worker()) {
+    active_worker()->Abort();
+  }
+}
+
+scoped_refptr<ServiceWorkerObject>
+ServiceWorkerRegistrationObject::GetNewestWorker() {
   // Algorithm for Get Newest Worker:
   //   https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
   // 1. Run the following steps atomically.
   base::AutoLock lock(mutex_);
-
-  // 2. Let newestWorker be null.
-  ServiceWorkerObject* newest_worker = nullptr;
-
-  // 3. If registration’s installing worker is not null, set newestWorker to
-  // registration’s installing worker.
   if (installing_worker_) {
-    newest_worker = installing_worker_;
-    // 4. Else if registration’s waiting worker is not null, set newestWorker to
-    // registration’s waiting worker.
+    // 3. If registration’s installing worker is not null, set newestWorker to
+    //    registration’s installing worker.
+    // 6. Return newestWorker.
+    return installing_worker_;
   } else if (waiting_worker_) {
-    newest_worker = waiting_worker_;
-    // 5. Else if registration’s active worker is not null, set newestWorker to
-    // registration’s active worker.
+    // 4. Else if registration’s waiting worker is not null, set newestWorker to
+    //    registration’s waiting worker.
+    // 6. Return newestWorker.
+    return waiting_worker_;
   } else if (active_worker_) {
-    newest_worker = active_worker_;
+    // 5. Else if registration’s active worker is not null, set newestWorker to
+    //    registration’s active worker.
+    // 6. Return newestWorker.
+    return active_worker_;
   }
 
+  // 2. Let newestWorker be null.
   // 6. Return newestWorker.
-  return newest_worker;
+  return nullptr;
 }
 
 }  // namespace worker
diff --git a/cobalt/worker/service_worker_registration_object.h b/cobalt/worker/service_worker_registration_object.h
index 0a399e0..e36bebe 100644
--- a/cobalt/worker/service_worker_registration_object.h
+++ b/cobalt/worker/service_worker_registration_object.h
@@ -42,39 +42,59 @@
   ServiceWorkerRegistrationObject(
       const url::Origin& storage_key, const GURL& scope_url,
       const ServiceWorkerUpdateViaCache& update_via_cache_mode);
-  ~ServiceWorkerRegistrationObject() {}
+  ~ServiceWorkerRegistrationObject();
+
+  void AbortAll();
 
   const url::Origin& storage_key() const { return storage_key_; }
+
   const GURL& scope_url() const { return scope_url_; }
+
   void set_update_via_cache_mode(
       const ServiceWorkerUpdateViaCache& update_via_cache_mode) {
     update_via_cache_mode_ = update_via_cache_mode;
   }
+
   const ServiceWorkerUpdateViaCache& update_via_cache_mode() const {
     return update_via_cache_mode_;
   }
 
-  void set_installing_worker(scoped_refptr<ServiceWorkerObject> worker) {
+  void set_installing_worker(const scoped_refptr<ServiceWorkerObject>& worker) {
     installing_worker_ = worker;
   }
   const scoped_refptr<ServiceWorkerObject>& installing_worker() const {
     return installing_worker_;
   }
-  void set_waiting_worker(scoped_refptr<ServiceWorkerObject> worker) {
+  void set_waiting_worker(const scoped_refptr<ServiceWorkerObject>& worker) {
     waiting_worker_ = worker;
   }
   const scoped_refptr<ServiceWorkerObject>& waiting_worker() const {
     return waiting_worker_;
   }
-  void set_active_worker(scoped_refptr<ServiceWorkerObject> worker) {
+  void set_active_worker(const scoped_refptr<ServiceWorkerObject>& worker) {
     active_worker_ = worker;
   }
   const scoped_refptr<ServiceWorkerObject>& active_worker() const {
     return active_worker_;
   }
 
+  // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-registration-stale
+  bool stale() {
+    return !last_update_check_time_.is_null() &&
+           (base::Time::Now() - last_update_check_time_).InSeconds() >
+               kStaleServiceWorkerRegistrationTimeout;
+  }
+
+  base::Time last_update_check_time() { return last_update_check_time_; }
+
+  void set_last_update_check_time(base::Time time) {
+    last_update_check_time_ = time;
+  }
+
   // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
-  ServiceWorkerObject* GetNewestWorker();
+  scoped_refptr<ServiceWorkerObject> GetNewestWorker();
+
+  const int kStaleServiceWorkerRegistrationTimeout = 86400;
 
  private:
   // This lock is to allow atomic operations on the registration object.
@@ -86,6 +106,8 @@
   scoped_refptr<ServiceWorkerObject> installing_worker_;
   scoped_refptr<ServiceWorkerObject> waiting_worker_;
   scoped_refptr<ServiceWorkerObject> active_worker_;
+
+  base::Time last_update_check_time_;
 };
 
 }  // namespace worker
diff --git a/cobalt/worker/testing/test_with_javascript.h b/cobalt/worker/testing/test_with_javascript.h
index 72dda4c..e40483d 100644
--- a/cobalt/worker/testing/test_with_javascript.h
+++ b/cobalt/worker/testing/test_with_javascript.h
@@ -62,7 +62,8 @@
                                               kServiceWorkerUpdateViaCacheNone);
       service_worker_object_ =
           new ServiceWorkerObject(ServiceWorkerObject::Options(
-              "TestServiceWorkerObject", web_context_->network_module(),
+              "TestServiceWorkerObject", web_context_->web_settings(),
+              web_context_->network_module(),
               containing_service_worker_registration_));
       service_worker_global_scope_ = new ServiceWorkerGlobalScope(
           web_context_->environment_settings(), service_worker_object_);
diff --git a/cobalt/worker/worker.cc b/cobalt/worker/worker.cc
index 870783d..a27476f 100644
--- a/cobalt/worker/worker.cc
+++ b/cobalt/worker/worker.cc
@@ -49,7 +49,7 @@
   // 1. Let is shared be true if worker is a SharedWorker object, and false
   //    otherwise.
   is_shared_ = options.is_shared;
-  // 2. Let owner be the relevant owner to add given outside settings.
+  // 2. Moved to below.
   // 3. Let parent worker global scope be null.
   // 4. If owner is a WorkerGlobalScope object (i.e., we are creating a nested
   //    dedicated worker), then set parent worker global scope to owner.
@@ -97,7 +97,8 @@
   // The origin return a unique opaque origin if worker global scope's url's
   // scheme is "data", and inherited origin otherwise.
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
-  worker_settings->set_origin(options_.outside_settings->GetOrigin());
+  worker_settings->set_origin(
+      options_.outside_context->environment_settings()->GetOrigin());
   web_context_->setup_environment_settings(worker_settings);
   // From algorithm for to setup up a worker environment settings object:
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
@@ -133,9 +134,18 @@
 
   // 10. Set worker global scope's name to the value of options's name member.
   dedicated_worker_global_scope->set_name(options_.options.name());
+  // (Moved) 2. Let owner be the relevant owner to add given outside settings.
+  web::WindowOrWorkerGlobalScope* owner =
+      options_.outside_context->GetWindowOrWorkerGlobalScope();
+  if (!owner) {
+    // There is not a running owner.
+    return;
+  }
   // 11. Append owner to worker global scope's owner set.
+  dedicated_worker_global_scope->owner_set()->insert(owner);
   // 12. If is shared is true, then:
-  //     1. Set worker global scope's constructor origin to outside settings's
+  //     1. Set worker global scope's constructor origin to outside
+  //     settings's
   //        origin.
   //     2. Set worker global scope's constructor url to url.
   //     3. Set worker global scope's type to the value of options's type
@@ -170,8 +180,11 @@
   const GURL& url = web_context_->environment_settings()->creation_url();
   loader::Origin origin = loader::Origin(url.GetOrigin());
 
-  // Todo: implement csp check (b/225037465)
-  csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
+  csp::SecurityCallback csp_callback = base::Bind(
+      &web::CspDelegate::CanLoad,
+      base::Unretained(options_.outside_context->GetWindowOrWorkerGlobalScope()
+                           ->csp_delegate()),
+      web::CspDelegate::kWorker);
 
   loader_ = web_context_->script_loader_factory()->CreateScriptLoader(
       url, origin, csp_callback,
@@ -205,16 +218,22 @@
     //     1. Queue a global task on the DOM manipulation task source given
     //        worker's relevant global object to fire an event named error at
     //        worker.
-    options_.outside_settings->context()
-        ->message_loop()
-        ->task_runner()
-        ->PostTask(FROM_HERE,
-                   base::BindOnce(
-                       [](web::WindowOrWorkerGlobalScope* global_scope) {
-                         global_scope->DispatchEvent(new web::ErrorEvent());
-                       },
-                       base::Unretained(options_.outside_settings->context()
-                                            ->GetWindowOrWorkerGlobalScope())));
+    options_.outside_context->message_loop()->task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](web::WindowOrWorkerGlobalScope* global_scope,
+               const base::Optional<std::string>& message,
+               const base::SourceLocation& location) {
+              web::ErrorEventInit error;
+              error.set_message(message.value_or("No content for worker."));
+              error.set_filename(location.file_path);
+              error.set_lineno(location.line_number);
+              error.set_colno(location.column_number);
+              global_scope->DispatchEvent(new web::ErrorEvent(error));
+            },
+            base::Unretained(
+                options_.outside_context->GetWindowOrWorkerGlobalScope()),
+            error, options_.construction_location));
     if (error_) {
       LOG(WARNING) << "Script loading failed : " << *error;
     } else {
@@ -267,18 +286,19 @@
   std::string retval = web_context_->script_runner()->Execute(
       content, script_location, mute_errors, &succeeded);
   if (!succeeded) {
-    options_.outside_settings->context()
-        ->message_loop()
-        ->task_runner()
-        ->PostTask(FROM_HERE,
-                   base::BindOnce(
-                       [](web::Context* context, const std::string& message) {
-                         web::ErrorEventInit error;
-                         error.set_message(message);
-                         context->GetWindowOrWorkerGlobalScope()->DispatchEvent(
-                             new web::ErrorEvent(error));
-                       },
-                       options_.outside_settings->context(), retval));
+    options_.outside_context->message_loop()->task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](web::Context* context, const std::string& message,
+               const std::string& filename) {
+              web::ErrorEventInit error;
+              error.set_message(message);
+              error.set_filename(filename);
+              context->GetWindowOrWorkerGlobalScope()->DispatchEvent(
+                  new web::ErrorEvent(error));
+            },
+            options_.outside_context, retval,
+            web_context_->environment_settings()->creation_url().spec()));
   }
 
   // 24. Enable outside port's port message queue.
@@ -303,8 +323,7 @@
   // Algorithm for 'run a worker'
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
   // 29. Clear the worker global scope's map of active timers.
-  if (worker_global_scope_) {
-    DCHECK(message_loop());
+  if (worker_global_scope_ && message_loop()) {
     message_loop()->task_runner()->PostBlockingTask(
         FROM_HERE, base::Bind(
                        [](WorkerGlobalScope* worker_global_scope) {
@@ -314,6 +333,9 @@
   }
   // 30. Disentangle all the ports in the list of the worker's ports.
   // 31. Empty worker global scope's owner set.
+  if (worker_global_scope_) {
+    worker_global_scope_->owner_set()->clear();
+  }
   if (web_agent_) {
     DCHECK(message_loop());
     web_agent_->WaitUntilDone();
diff --git a/cobalt/worker/worker.h b/cobalt/worker/worker.h
index feea239..9d7043c 100644
--- a/cobalt/worker/worker.h
+++ b/cobalt/worker/worker.h
@@ -25,6 +25,7 @@
 #include "base/message_loop/message_loop_current.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/threading/thread.h"
+#include "cobalt/base/source_location.h"
 #include "cobalt/csp/content_security_policy.h"
 #include "cobalt/loader/script_loader_factory.h"
 #include "cobalt/script/environment_settings.h"
@@ -57,13 +58,16 @@
   struct Options {
     web::Agent::Options web_options;
 
+    // Holds the source location where the worker was constructed.
+    base::SourceLocation construction_location;
+
     // True if worker is a SharedWorker object, and false otherwise.
     bool is_shared;
 
     // Parameters from 'Run a worker' step 9.1 in the spec.
     //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#dom-worker
     GURL url;
-    web::EnvironmentSettings* outside_settings = nullptr;
+    web::Context* outside_context = nullptr;
     web::MessagePort* outside_port = nullptr;
     WorkerOptions options;
   };
diff --git a/cobalt/worker/worker.idl b/cobalt/worker/worker.idl
index e05669f..db7918a 100644
--- a/cobalt/worker/worker.idl
+++ b/cobalt/worker/worker.idl
@@ -15,12 +15,11 @@
 // https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
 // https://html.spec.whatwg.org/dev/workers.html#workers
 
-[Exposed=Window,
- ImplementedAs=DedicatedWorker,
- ConstructorCallWith=EnvironmentSettings,
- Constructor(USVString scriptURL, optional WorkerOptions options)
-]
-interface Worker : EventTarget {
+[
+  Exposed = Window, ImplementedAs = DedicatedWorker,
+  ConstructorCallWith = EnvironmentSettings, RaisesException = Constructor,
+  Constructor(USVString scriptURL, optional WorkerOptions options)
+] interface Worker : EventTarget {
   void terminate();
 
   void postMessage(any message);
diff --git a/cobalt/worker/worker_global_scope.cc b/cobalt/worker/worker_global_scope.cc
index c483c56..d9025b9 100644
--- a/cobalt/worker/worker_global_scope.cc
+++ b/cobalt/worker/worker_global_scope.cc
@@ -157,7 +157,7 @@
     }
   }
 
-  const std::unique_ptr<std::string>& GetContents(int index) {
+  std::unique_ptr<std::string>& GetContents(int index) {
     return contents_[index];
   }
 
@@ -236,11 +236,14 @@
   // Only NetFetchers are expected to call this, since only they have the
   // response headers.
   loader::NetFetcher* net_fetcher =
-      base::polymorphic_downcast<loader::NetFetcher*>(fetcher);
-  net::URLFetcher* url_fetcher = net_fetcher->url_fetcher();
-  LOG(INFO) << "Failure receiving Content Security Policy headers "
-               "for URL: "
-            << url_fetcher->GetURL() << ".";
+      fetcher ? base::polymorphic_downcast<loader::NetFetcher*>(fetcher)
+              : nullptr;
+  net::URLFetcher* url_fetcher =
+      net_fetcher ? net_fetcher->url_fetcher() : nullptr;
+  const GURL& url = url_fetcher ? url_fetcher->GetURL()
+                                : environment_settings()->creation_url();
+  LOG(INFO) << "Failure receiving Content Security Policy headers for URL: "
+            << url << ".";
   // Return true regardless of CSP headers being received to continue loading
   // the response.
   return true;
@@ -283,6 +286,7 @@
   web::EnvironmentSettings* settings = environment_settings();
   const GURL& base_url = settings->base_url();
   loader::Origin origin = loader::Origin(base_url.GetOrigin());
+  // TODO(b/241801523): Apply CSP.
   ScriptLoader script_loader(settings->context());
   script_loader.Load(origin, request_urls);
 
@@ -290,11 +294,13 @@
     const auto& error = script_loader.GetError(index);
     if (error) continue;
     const GURL& url = request_urls[index];
-    const std::unique_ptr<std::string>& script =
-        script_loader.GetContents(index);
     //   8.21.1.5. Set updatedResourceMap[importRequest’s url] to
     //             fetchedResponse.
-    (*new_resource_map)[url].reset(new std::string(*script.get()));
+    // Note: The headers of imported scripts aren't used anywhere.
+    auto result = new_resource_map->insert(std::make_pair(
+        url, ScriptResource(std::move(script_loader.GetContents(index)))));
+    // Assert that the insert was successful.
+    DCHECK(result.second);
     //   8.21.1.6. Set fetchedResponse to fetchedResponse’s unsafe response.
     //   8.21.1.7. If fetchedResponse’s cache state is not
     //             "local", set registration’s last update check time to the
@@ -304,7 +310,8 @@
     //             storedResponse’s unsafe response's body, set
     //             hasUpdatedResources to true.
     DCHECK(previous_resource_map.find(url) != previous_resource_map.end());
-    if (*script != *(previous_resource_map.find(url)->second)) {
+    if (*result.first->second.content !=
+        *(previous_resource_map.find(url)->second.content)) {
       has_updated_resources = true;
     }
   }
@@ -371,6 +378,7 @@
   //      object, passing along any custom perform the fetch steps provided.
   //      If this succeeds, let script be the result. Otherwise, rethrow the
   //      exception.
+  // TODO(b/241801523): Apply CSP.
   ScriptLoader script_loader(settings->context());
   script_loader.Load(origin, request_urls);
 
diff --git a/cobalt/worker/worker_global_scope.h b/cobalt/worker/worker_global_scope.h
index 345220b..e387991 100644
--- a/cobalt/worker/worker_global_scope.h
+++ b/cobalt/worker/worker_global_scope.h
@@ -17,10 +17,13 @@
 
 #include <map>
 #include <memory>
+#include <set>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
 #include "cobalt/base/tokens.h"
 #include "cobalt/script/environment_settings.h"
 #include "cobalt/script/exception_state.h"
@@ -34,15 +37,30 @@
 #include "cobalt/web/window_timers.h"
 #include "cobalt/worker/worker_location.h"
 #include "cobalt/worker/worker_navigator.h"
+#include "net/http/http_response_headers.h"
 #include "net/url_request/url_request.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace cobalt {
 namespace worker {
 // Implementation of the WorkerGlobalScope common interface.
 //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#the-workerglobalscope-common-interface
 
-using ScriptResourceMap = std::map<GURL, std::unique_ptr<std::string>>;
+struct ScriptResource {
+  explicit ScriptResource(std::unique_ptr<std::string> content)
+      : content(std::move(content)) {}
+  ScriptResource(std::unique_ptr<std::string> content,
+                 const scoped_refptr<net::HttpResponseHeaders>& headers)
+      : content(std::move(content)), headers(headers) {}
+  std::unique_ptr<std::string> content;
+  scoped_refptr<net::HttpResponseHeaders> headers;
+};
+
+using ScriptResourceMap = std::map<GURL, ScriptResource>;
+// A registration map is an ordered map where the keys are (storage key,
+// serialized scope urls) and the values are service worker registrations.
+using RegistrationMapKey = std::pair<url::Origin, std::string>;
 
 class WorkerGlobalScope : public web::WindowOrWorkerGlobalScope {
  public:
@@ -79,6 +97,8 @@
   virtual void ImportScripts(const std::vector<std::string>& urls,
                              script::ExceptionState* exception_state);
 
+  std::set<WindowOrWorkerGlobalScope*>* owner_set() { return &owner_set_; }
+
   void set_url(const GURL& url) { url_ = url; }
 
   const GURL& Url() const { return url_; }
@@ -134,6 +154,13 @@
                                      ResponseCallback response_callback);
 
  private:
+  // WorkerGlobalScope Infrastructure
+  //   https://html.spec.whatwg.org/multipage/workers.html#workerglobalscope
+  // owner_set_ would typically have a union of Document and WorkerGlobalScope
+  // objects in this set, but we use WindowOrWorkerGlobalScope here since we
+  // have easy access from a Window to its associated Document.
+  std::set<WindowOrWorkerGlobalScope*> owner_set_;
+
   // WorkerGlobalScope url
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#concept-workerglobalscope-url
   GURL url_;
diff --git a/cobalt/worker/worker_settings.cc b/cobalt/worker/worker_settings.cc
index df42733..96d52ae 100644
--- a/cobalt/worker/worker_settings.cc
+++ b/cobalt/worker/worker_settings.cc
@@ -27,6 +27,7 @@
 
 namespace cobalt {
 namespace worker {
+
 WorkerSettings::WorkerSettings() : web::EnvironmentSettings() {}
 
 WorkerSettings::WorkerSettings(web::MessagePort* message_port)
diff --git a/cobalt/xhr/BUILD.gn b/cobalt/xhr/BUILD.gn
index 75a2c80..17ad212 100644
--- a/cobalt/xhr/BUILD.gn
+++ b/cobalt/xhr/BUILD.gn
@@ -12,12 +12,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+source_set("xhr_settings") {
+  has_pedantic_warnings = true
+  sources = [ "xhr_settings.h" ]
+  public_deps = [ "//cobalt/base" ]
+}
+
 static_library("xhr") {
   # Creates cycle with //cobalt/dom through xml_document.h
   check_includes = false
 
   has_pedantic_warnings = true
   sources = [
+    "fetch_buffer_pool.cc",
+    "fetch_buffer_pool.h",
     "url_fetcher_buffer_writer.cc",
     "url_fetcher_buffer_writer.h",
     "xml_http_request.cc",
@@ -26,6 +34,8 @@
     "xml_http_request_event_target.h",
   ]
 
+  public_deps = [ ":xhr_settings" ]
+
   deps = [
     ":global_stats",
     "//cobalt/base",
@@ -39,12 +49,6 @@
     "//third_party/protobuf:protobuf_lite",
     "//url",
   ]
-
-  if (enable_xhr_header_filtering && !sb_is_evergreen) {
-    sources = [ "xhr_modify_headers.h" ]
-    defines = [ "COBALT_ENABLE_XHR_HEADER_FILTERING" ]
-    deps += cobalt_platform_dependencies
-  }
 }
 
 static_library("global_stats") {
@@ -60,13 +64,18 @@
 target(gtest_target_type, "xhr_test") {
   testonly = true
   has_pedantic_warnings = true
-  sources = [ "xml_http_request_test.cc" ]
+  sources = [
+    "fetch_buffer_pool_test.cc",
+    "xhr_settings_test.cc",
+    "xml_http_request_test.cc",
+  ]
   deps = [
     ":xhr",
     "//cobalt/base",
     "//cobalt/browser",
     "//cobalt/debug",
     "//cobalt/dom/testing:dom_testing",
+    "//cobalt/script",
     "//cobalt/test:run_all_unittests",
     "//cobalt/web:dom_exception",
     "//cobalt/web/testing:web_testing",
diff --git a/cobalt/xhr/fetch_buffer_pool.cc b/cobalt/xhr/fetch_buffer_pool.cc
new file mode 100644
index 0000000..b67a270
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool.cc
@@ -0,0 +1,125 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/fetch_buffer_pool.h"
+
+#include <algorithm>
+#include <utility>
+
+namespace cobalt {
+namespace xhr {
+
+int FetchBufferPool::GetSize() const {
+  int size = 0;
+  for (const auto& buffer : buffers_) {
+    size += buffer.byte_length();
+  }
+
+  // The last buffer may not be fully populated.
+  if (current_buffer_) {
+    DCHECK_EQ(current_buffer_, &buffers_.back());
+    DCHECK_LE(current_buffer_offset_, current_buffer_->byte_length());
+
+    auto remaining = current_buffer_->byte_length() - current_buffer_offset_;
+    size -= remaining;
+  }
+
+  DCHECK_GE(size, 0);
+  return size;
+}
+
+void FetchBufferPool::Clear() {
+  buffers_.clear();
+  current_buffer_ = nullptr;
+  current_buffer_offset_ = 0;
+}
+
+int FetchBufferPool::Write(const void* data, int num_bytes) {
+  DCHECK(num_bytes >= 0);
+
+  if (num_bytes > 0) {
+    DCHECK(data);
+  }
+
+  auto source_bytes_remaining = num_bytes;
+  while (source_bytes_remaining > 0) {
+    EnsureCurrentBufferAllocated(source_bytes_remaining);
+
+    int destination_bytes_remaining =
+        current_buffer_->byte_length() - current_buffer_offset_;
+    int bytes_to_copy =
+        std::min(destination_bytes_remaining, source_bytes_remaining);
+
+    memcpy(current_buffer_->data() + current_buffer_offset_, data,
+           bytes_to_copy);
+
+    data = static_cast<const uint8_t*>(data) + bytes_to_copy;
+    current_buffer_offset_ += bytes_to_copy;
+    source_bytes_remaining -= bytes_to_copy;
+
+    if (current_buffer_offset_ == current_buffer_->byte_length()) {
+      current_buffer_ = nullptr;
+      current_buffer_offset_ = 0;
+    }
+  }
+
+  return num_bytes;
+}
+
+void FetchBufferPool::ResetAndReturnAsArrayBuffers(
+    bool return_all,
+    std::vector<script::PreallocatedArrayBufferData>* buffers) {
+  DCHECK(buffers);
+
+  buffers->clear();
+
+  if (!return_all && buffers_.size() > 1 && current_buffer_) {
+    // Don't return the last buffer when:
+    //   1. `return_all` is set to false
+    //   2. the last buffer is not full (current_buffer_ is not null)
+    //   3. There are more than one buffer cached so the caller will receive
+    //      something.
+    auto saved_last_buffer = std::move(buffers_.back());
+    buffers_.pop_back();
+    buffers->swap(buffers_);
+    buffers_.push_back(std::move(saved_last_buffer));
+    current_buffer_ = &buffers_.back();
+    return;
+  }
+
+  if (current_buffer_) {
+    DCHECK(return_all || buffers_.size() == 1);
+    current_buffer_->Resize(current_buffer_offset_);
+  }
+
+  buffers->swap(buffers_);
+
+  Clear();
+}
+
+void FetchBufferPool::EnsureCurrentBufferAllocated(int expected_buffer_size) {
+  if (current_buffer_ != nullptr) {
+    DCHECK_EQ(current_buffer_, &buffers_.back());
+    DCHECK_LE(current_buffer_offset_, current_buffer_->byte_length());
+    return;
+  }
+
+  buffers_.emplace_back(std::max(default_buffer_size_, expected_buffer_size));
+
+  current_buffer_ = &buffers_.back();
+  current_buffer_offset_ = 0;
+}
+
+}  // namespace xhr
+}  // namespace cobalt
diff --git a/cobalt/xhr/fetch_buffer_pool.h b/cobalt/xhr/fetch_buffer_pool.h
new file mode 100644
index 0000000..13158a3
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool.h
@@ -0,0 +1,80 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_XHR_FETCH_BUFFER_POOL_H_
+#define COBALT_XHR_FETCH_BUFFER_POOL_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/logging.h"
+#include "base/optional.h"
+#include "cobalt/script/array_buffer.h"
+
+namespace cobalt {
+namespace xhr {
+
+// Manages buffer used by the Fetch api.
+// The Fetch api is implemented as a polyfill on top of XMLHttpRequest, where
+// the fetched data is sent to the web app in chunks represented as Uint8Array
+// objects.  This class allows to minimizing copying by appending the incoming
+// network data to PreallocatedArrayBufferData.
+class FetchBufferPool {
+ public:
+  static constexpr int kDefaultBufferSize = 1024 * 1024;
+
+  explicit FetchBufferPool(
+      const base::Optional<int>& default_buffer_size = base::Optional<int>())
+      // Use "kDefaultBufferSize + 0" to force using kDefaultBufferSize as
+      // r-value to avoid link error.
+      : default_buffer_size_(
+            default_buffer_size.value_or(kDefaultBufferSize + 0)) {
+    DCHECK_GT(default_buffer_size_, 0);
+  }
+  ~FetchBufferPool() = default;
+
+  int GetSize() const;
+  void Clear();
+
+  int Write(const void* data, int num_bytes);
+
+  // When `return_all` is set to true, the function should return all data
+  // buffered, otherwise the function may decide not to return some buffers so
+  // it can return larger buffers in future.
+  // The user of this class should call the function with `return_all` set to
+  // true at least for the last call to this function during a request.
+  void ResetAndReturnAsArrayBuffers(
+      bool return_all,
+      std::vector<script::PreallocatedArrayBufferData>* buffers);
+
+ private:
+  FetchBufferPool(const FetchBufferPool&) = delete;
+  FetchBufferPool& operator=(const FetchBufferPool&) = delete;
+
+  void EnsureCurrentBufferAllocated(int expected_buffer_size);
+
+  const int default_buffer_size_ = 0;
+  std::vector<script::PreallocatedArrayBufferData> buffers_;
+
+  // Either nullptr or points to the last element of |buffers_|, using a member
+  // variable explicitly to keep it in sync with |current_buffer_offset_|.
+  script::PreallocatedArrayBufferData* current_buffer_ = nullptr;
+  size_t current_buffer_offset_ = 0;
+};
+
+}  // namespace xhr
+}  // namespace cobalt
+
+#endif  // COBALT_XHR_FETCH_BUFFER_POOL_H_
diff --git a/cobalt/xhr/fetch_buffer_pool_test.cc b/cobalt/xhr/fetch_buffer_pool_test.cc
new file mode 100644
index 0000000..9a7b12e
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool_test.cc
@@ -0,0 +1,230 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/fetch_buffer_pool.h"
+
+#include <vector>
+
+#include "cobalt/script/array_buffer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace xhr {
+namespace {
+
+void FillTestData(std::vector<uint8_t>* buffer) {
+  for (size_t i = 0; i < buffer->size(); ++i) {
+    buffer->at(i) = static_cast<char>(i);
+  }
+}
+
+void AppendBuffersTo(
+    const std::vector<script::PreallocatedArrayBufferData>& sources,
+    std::vector<uint8_t>* destination) {
+  ASSERT_NE(destination, nullptr);
+
+  for (const auto& buffer : sources) {
+    destination->insert(destination->end(), buffer.data(),
+                        buffer.data() + buffer.byte_length());
+  }
+}
+
+TEST(FetchBufferPoolTest, Empty) {
+  FetchBufferPool empty_buffer_pool;
+  EXPECT_EQ(empty_buffer_pool.GetSize(), 0);
+
+  std::vector<script::PreallocatedArrayBufferData> buffers;
+
+  empty_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+  EXPECT_TRUE(buffers.empty());
+
+  empty_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+  EXPECT_TRUE(buffers.empty());
+
+  empty_buffer_pool.Write(nullptr, 0);  // still empty
+  EXPECT_EQ(empty_buffer_pool.GetSize(), 0);
+
+  empty_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+  EXPECT_TRUE(buffers.empty());
+
+  empty_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+  EXPECT_TRUE(buffers.empty());
+}
+
+TEST(FetchBufferPoolTest, SunnyDay) {
+  FetchBufferPool fetch_buffer_pool;
+
+  std::vector<uint8_t> source_buffer(1024 * 1024 + 1023);
+  FillTestData(&source_buffer);
+
+  std::vector<uint8_t> reference_buffer;
+  for (int i = 0; i < 10; ++i) {
+    fetch_buffer_pool.Write(source_buffer.data(),
+                            static_cast<int>(source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), source_buffer.begin(),
+                            source_buffer.end());
+  }
+
+  std::vector<script::PreallocatedArrayBufferData> buffers;
+  fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+  ASSERT_FALSE(buffers.empty());
+  ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+  std::vector<uint8_t> collected_buffer;
+  AppendBuffersTo(buffers, &collected_buffer);
+  ASSERT_EQ(collected_buffer, reference_buffer);
+}
+
+TEST(FetchBufferPoolTest, Clear) {
+  FetchBufferPool fetch_buffer_pool;
+
+  std::vector<uint8_t> source_buffer(1024);
+  FillTestData(&source_buffer);
+
+  fetch_buffer_pool.Write(source_buffer.data(),
+                          static_cast<int>(source_buffer.size()));
+  EXPECT_EQ(fetch_buffer_pool.GetSize(), source_buffer.size());
+
+  fetch_buffer_pool.Clear();
+
+  EXPECT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+  std::vector<script::PreallocatedArrayBufferData> buffers;
+  fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+  ASSERT_TRUE(buffers.empty());
+  fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+  ASSERT_TRUE(buffers.empty());
+}
+
+TEST(FetchBufferPoolTest, IncrementalWriteAndRead) {
+  FetchBufferPool fetch_buffer_pool;
+
+  std::vector<uint8_t> source_buffer(4097);
+  FillTestData(&source_buffer);
+
+  std::vector<uint8_t> reference_buffer;
+  std::vector<uint8_t> collected_buffer;
+  while (reference_buffer.size() < 1024 * 1024 * 10) {
+    int bytes_to_write =
+        static_cast<int>((reference_buffer.size() + 1) % source_buffer.size());
+    fetch_buffer_pool.Write(source_buffer.data(), bytes_to_write);
+    reference_buffer.insert(reference_buffer.end(), source_buffer.begin(),
+                            source_buffer.begin() + bytes_to_write);
+
+    std::vector<script::PreallocatedArrayBufferData> buffers;
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+    ASSERT_FALSE(buffers.empty());
+    AppendBuffersTo(buffers, &collected_buffer);
+  }
+
+  std::vector<script::PreallocatedArrayBufferData> buffers;
+  fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+  ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+  AppendBuffersTo(buffers, &collected_buffer);
+
+  ASSERT_EQ(collected_buffer, reference_buffer);
+}
+
+TEST(FetchBufferPoolTest, SingleLargeWrite) {
+  FetchBufferPool fetch_buffer_pool;
+
+  std::vector<uint8_t> large_source_buffer(1024 * 1024 * 16 + 1023);
+  FillTestData(&large_source_buffer);
+
+  {
+    std::vector<uint8_t> reference_buffer;
+    std::vector<uint8_t> collected_buffer;
+
+    fetch_buffer_pool.Write(large_source_buffer.data(),
+                            static_cast<int>(large_source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+                            large_source_buffer.end());
+
+    std::vector<script::PreallocatedArrayBufferData> buffers;
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+    ASSERT_FALSE(buffers.empty());
+    ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+    AppendBuffersTo(buffers, &collected_buffer);
+    ASSERT_EQ(collected_buffer, reference_buffer);
+  }
+}
+
+TEST(FetchBufferPoolTest, MixedSizeWrites) {
+  FetchBufferPool fetch_buffer_pool;
+
+  std::vector<uint8_t> small_source_buffer(1023);
+  std::vector<uint8_t> large_source_buffer(1024 * 1024 * 16 + 1023);
+
+  FillTestData(&small_source_buffer);
+  FillTestData(&large_source_buffer);
+
+  {
+    // Small then large
+    std::vector<uint8_t> reference_buffer;
+    std::vector<uint8_t> collected_buffer;
+
+    fetch_buffer_pool.Write(small_source_buffer.data(),
+                            static_cast<int>(small_source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), small_source_buffer.begin(),
+                            small_source_buffer.end());
+
+    fetch_buffer_pool.Write(large_source_buffer.data(),
+                            static_cast<int>(large_source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+                            large_source_buffer.end());
+
+    std::vector<script::PreallocatedArrayBufferData> buffers;
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+    ASSERT_FALSE(buffers.empty());
+    AppendBuffersTo(buffers, &collected_buffer);
+
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+    ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+    AppendBuffersTo(buffers, &collected_buffer);
+
+    ASSERT_EQ(collected_buffer, reference_buffer);
+  }
+
+  {
+    // Large then small
+    std::vector<uint8_t> reference_buffer;
+    std::vector<uint8_t> collected_buffer;
+
+    fetch_buffer_pool.Write(large_source_buffer.data(),
+                            static_cast<int>(large_source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+                            large_source_buffer.end());
+
+    fetch_buffer_pool.Write(small_source_buffer.data(),
+                            static_cast<int>(small_source_buffer.size()));
+    reference_buffer.insert(reference_buffer.end(), small_source_buffer.begin(),
+                            small_source_buffer.end());
+
+    std::vector<script::PreallocatedArrayBufferData> buffers;
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+    ASSERT_FALSE(buffers.empty());
+    AppendBuffersTo(buffers, &collected_buffer);
+
+    fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+    ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+    AppendBuffersTo(buffers, &collected_buffer);
+
+    ASSERT_EQ(collected_buffer, reference_buffer);
+  }
+}
+
+}  // namespace
+}  // namespace xhr
+}  // namespace cobalt
diff --git a/cobalt/xhr/url_fetcher_buffer_writer.cc b/cobalt/xhr/url_fetcher_buffer_writer.cc
index d841e3b..3876437 100644
--- a/cobalt/xhr/url_fetcher_buffer_writer.cc
+++ b/cobalt/xhr/url_fetcher_buffer_writer.cc
@@ -46,7 +46,23 @@
 
 }  // namespace
 
-URLFetcherResponseWriter::Buffer::Buffer(Type type) : type_(type) {}
+URLFetcherResponseWriter::Buffer::Buffer(
+    Type type, const base::Optional<bool>& enable_try_lock_for_progress_check)
+    : type_(type),
+      enable_try_lock_for_progress_check_(
+          enable_try_lock_for_progress_check.value_or(false)) {
+  DCHECK_NE(type_, kBufferPool);
+}
+
+URLFetcherResponseWriter::Buffer::Buffer(
+    Type type, const base::Optional<bool>& enable_try_lock_for_progress_check,
+    const base::Optional<int>& fetch_buffer_size)
+    : type_(type),
+      enable_try_lock_for_progress_check_(
+          enable_try_lock_for_progress_check.value_or(true)),
+      data_as_buffer_pool_(fetch_buffer_size) {
+  DCHECK_EQ(type_, kBufferPool);
+}
 
 void URLFetcherResponseWriter::Buffer::DisablePreallocate() {
   base::AutoLock auto_lock(lock_);
@@ -72,9 +88,28 @@
   return static_cast<int64_t>(download_progress_);
 }
 
-bool URLFetcherResponseWriter::Buffer::HasProgressSinceLastGetAndReset() const {
-  base::AutoLock auto_lock(lock_);
-  return GetSize_Locked() > download_progress_;
+bool URLFetcherResponseWriter::Buffer::HasProgressSinceLastGetAndReset(
+    bool request_done) const {
+  if (!enable_try_lock_for_progress_check_) {
+    base::AutoLock auto_lock(lock_);
+    return GetSize_Locked() > download_progress_;
+  }
+
+  // The |lock_| might be held on the network thread to copy data, which can be
+  // slow sometimes.  Waiting for |lock_| may block the JS thread for extended
+  // amount of time, so we only wait for it when the |request_done| is true,
+  // where we absolutely have to know the correct progress.
+  if (request_done) {
+    base::AutoLock auto_lock(lock_);
+    return GetSize_Locked() > download_progress_;
+  }
+  if (!lock_.Try()) {
+    // Failed to acquire |lock_|, assume there is no progress.
+    return false;
+  }
+  bool has_progress = GetSize_Locked() > download_progress_;
+  lock_.Release();
+  return has_progress;
 }
 
 const std::string&
@@ -88,20 +123,26 @@
 
 const std::string&
 URLFetcherResponseWriter::Buffer::GetTemporaryReferenceOfString() {
+  DCHECK_NE(type_, kBufferPool);
+
   base::AutoLock auto_lock(lock_);
 
   // This function can be further optimized by always return reference of
   // |data_as_string_|, and only make a copy when |data_as_string_| is extended.
-  //  It is not done as GetTemporaryReferenceOfString() is currently not
+  // It is not done as GetTemporaryReferenceOfString() is currently not
   // triggered.  It will only be called when JS app is retrieving responseText
   // while the request is still in progress.
-
-  if (type_ == kString) {
-    copy_of_data_as_string_ = data_as_string_;
-  } else {
-    DCHECK_EQ(type_, kArrayBuffer);
-    const char* begin = static_cast<const char*>(data_as_array_buffer_.data());
-    copy_of_data_as_string_.assign(begin, begin + data_as_array_buffer_size_);
+  switch (type_) {
+    case kString:
+      copy_of_data_as_string_ = data_as_string_;
+      break;
+    case kArrayBuffer: {
+      auto data = data_as_array_buffer_.data();
+      copy_of_data_as_string_.assign(data, data + data_as_array_buffer_size_);
+    } break;
+    case kBufferPool:
+      NOTREACHED();
+      break;
   }
 
   return copy_of_data_as_string_;
@@ -132,6 +173,25 @@
   download_progress_ = 0;
 }
 
+void URLFetcherResponseWriter::Buffer::GetAndResetDataAndDownloadProgress(
+    bool request_done,
+    std::vector<script::PreallocatedArrayBufferData>* buffers) {
+  DCHECK(buffers);
+
+  buffers->clear();
+
+  base::AutoLock auto_lock(lock_);
+
+  DCHECK_EQ(type_, kBufferPool);
+  data_as_buffer_pool_.ResetAndReturnAsArrayBuffers(request_done, buffers);
+
+  // It is important to reset the |download_progress_| and return the data in
+  // one function to avoid potential race condition that may prevent the last
+  // bit of data of Fetcher from being downloaded, because the data download is
+  // guarded by HasProgressSinceLastGetAndReset().
+  download_progress_ = 0;
+}
+
 void URLFetcherResponseWriter::Buffer::GetAndResetData(
     PreallocatedArrayBufferData* data) {
   DCHECK(data);
@@ -186,6 +246,9 @@
       DCHECK_EQ(data_as_array_buffer_size_, 0u);
       data_as_array_buffer_.Resize(capacity);
       return;
+    case kBufferPool:
+      // FetchBufferPool don't need preallocate.
+      return;
   }
   NOTREACHED();
 }
@@ -198,6 +261,8 @@
     return;
   }
 
+  const char* data = static_cast<const char*>(buffer);
+
   base::AutoLock auto_lock(lock_);
 
   DCHECK(allow_write_);
@@ -212,33 +277,39 @@
       SB_LOG(WARNING) << "Data written is larger than the preset capacity "
                       << data_as_string_.capacity();
     }
-    data_as_string_.append(static_cast<const char*>(buffer), num_bytes);
+    data_as_string_.append(data, num_bytes);
     return;
   }
 
-  DCHECK_EQ(type_, kArrayBuffer);
-  if (data_as_array_buffer_size_ + num_bytes >
-      data_as_array_buffer_.byte_length()) {
-    if (capacity_known_) {
-      SB_LOG(WARNING) << "Data written is larger than the preset capacity "
-                      << data_as_array_buffer_.byte_length();
+  if (type_ == kArrayBuffer) {
+    if (data_as_array_buffer_size_ + num_bytes >
+        data_as_array_buffer_.byte_length()) {
+      if (capacity_known_) {
+        SB_LOG(WARNING) << "Data written is larger than the preset capacity "
+                        << data_as_array_buffer_.byte_length();
+      }
+      size_t new_size = std::max(
+          std::min(data_as_array_buffer_.byte_length() * kResizingMultiplier,
+                   desired_capacity_),
+          data_as_array_buffer_size_ + num_bytes);
+      if (new_size > desired_capacity_) {
+        // Content-length is wrong, response size is completely unknown.
+        // Double the capacity to avoid frequent resizing.
+        new_size *= kResizingMultiplier;
+      }
+      data_as_array_buffer_.Resize(new_size);
     }
-    size_t new_size = std::max(
-        std::min(data_as_array_buffer_.byte_length() * kResizingMultiplier,
-                 desired_capacity_),
-        data_as_array_buffer_size_ + num_bytes);
-    if (new_size > desired_capacity_) {
-      // Content-length is wrong, response size is completely unknown.
-      // Double the capacity to avoid frequent resizing.
-      new_size *= kResizingMultiplier;
-    }
-    data_as_array_buffer_.Resize(new_size);
+
+    auto destination = static_cast<uint8_t*>(data_as_array_buffer_.data()) +
+                       data_as_array_buffer_size_;
+    memcpy(destination, data, num_bytes);
+    data_as_array_buffer_size_ += num_bytes;
+
+    return;
   }
 
-  auto destination = static_cast<uint8_t*>(data_as_array_buffer_.data()) +
-                     data_as_array_buffer_size_;
-  memcpy(destination, buffer, num_bytes);
-  data_as_array_buffer_size_ += num_bytes;
+  DCHECK_EQ(type_, kBufferPool);
+  data_as_buffer_pool_.Write(data, num_bytes);
 }
 
 size_t URLFetcherResponseWriter::Buffer::GetSize_Locked() const {
@@ -249,6 +320,8 @@
       return data_as_string_.size();
     case kArrayBuffer:
       return data_as_array_buffer_size_;
+    case kBufferPool:
+      return data_as_buffer_pool_.GetSize();
   }
   NOTREACHED();
   return 0;
@@ -261,6 +334,10 @@
     return;
   }
 
+  // kBufferPool cannot be converted to or from other types.
+  DCHECK_NE(type_, kBufferPool);
+  DCHECK_NE(type, kBufferPool);
+
   DCHECK(allow_write_);
 
   DLOG_IF(WARNING, GetSize_Locked() > 0)
@@ -291,8 +368,9 @@
   }
 
   data_as_string_.reserve(data_as_array_buffer_.byte_length());
-  data_as_string_.append(static_cast<const char*>(data_as_array_buffer_.data()),
-                         data_as_array_buffer_size_);
+  data_as_string_.append(
+      reinterpret_cast<const char*>(data_as_array_buffer_.data()),
+      data_as_array_buffer_size_);
 
   ReleaseMemory(&data_as_array_buffer_);
   data_as_array_buffer_size_ = 0;
diff --git a/cobalt/xhr/url_fetcher_buffer_writer.h b/cobalt/xhr/url_fetcher_buffer_writer.h
index 35058ab..245709c 100644
--- a/cobalt/xhr/url_fetcher_buffer_writer.h
+++ b/cobalt/xhr/url_fetcher_buffer_writer.h
@@ -1,4 +1,4 @@
-// Copyright 2019 Google Inc. All Rights Reserved.
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,12 +17,15 @@
 
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "base/callback.h"
 #include "base/memory/ref_counted.h"
+#include "base/optional.h"
 #include "base/synchronization/lock.h"
 #include "base/task_runner.h"
 #include "cobalt/script/array_buffer.h"
+#include "cobalt/xhr/fetch_buffer_pool.h"
 #include "net/base/io_buffer.h"
 #include "net/url_request/url_fetcher_response_writer.h"
 
@@ -38,15 +41,25 @@
     enum Type {
       kString,
       kArrayBuffer,
+      kBufferPool,
     };
 
-    explicit Buffer(Type type);
+    // This ctor should be used when |type| isn't |kBufferPool|, it's checked in
+    // the implementation.
+    Buffer(Type type,
+           const base::Optional<bool>& enable_try_lock_for_progress_check);
+    // This ctor should be used when |type| is |kBufferPool|, it's checked in
+    // the implementation.
+    Buffer(Type type,
+           const base::Optional<bool>& enable_try_lock_for_progress_check,
+           const base::Optional<int>& fetch_buffer_size);
 
     void DisablePreallocate();
     void Clear();
 
+    Type type() const { return type_; }
     int64_t GetAndResetDownloadProgress();
-    bool HasProgressSinceLastGetAndReset() const;
+    bool HasProgressSinceLastGetAndReset(bool request_done) const;
 
     // When the following function is called, Write() can no longer be called to
     // append more data.  It is the responsibility of the user of this class to
@@ -58,6 +71,9 @@
     const std::string& GetTemporaryReferenceOfString();
 
     void GetAndResetDataAndDownloadProgress(std::string* str);
+    void GetAndResetDataAndDownloadProgress(
+        bool request_done,
+        std::vector<script::PreallocatedArrayBufferData>* buffers);
     void GetAndResetData(PreallocatedArrayBufferData* data);
 
     void MaybePreallocate(int64_t capacity);
@@ -72,6 +88,7 @@
     void UpdateType_Locked(Type type);
 
     Type type_;
+    const bool enable_try_lock_for_progress_check_;
     bool allow_preallocate_ = true;
     bool capacity_known_ = false;
     size_t desired_capacity_ = 0;
@@ -84,11 +101,17 @@
 
     // Data is stored in one of the following buffers, depends on the value of
     // |type_|.
+    // When |type_| is kString:
     std::string data_as_string_;
     // For use in GetReferenceOfString() so it can return a reference.
     std::string copy_of_data_as_string_;
+
+    // When |type_| is kArrayBuffer:
     PreallocatedArrayBufferData data_as_array_buffer_;
     size_t data_as_array_buffer_size_ = 0;
+
+    // When |type_| is kBufferPool:
+    FetchBufferPool data_as_buffer_pool_;
   };
 
   explicit URLFetcherResponseWriter(const scoped_refptr<Buffer>& buffer);
diff --git a/cobalt/xhr/xhr_modify_headers.h b/cobalt/xhr/xhr_modify_headers.h
deleted file mode 100644
index 69c4f69..0000000
--- a/cobalt/xhr/xhr_modify_headers.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_XHR_XHR_MODIFY_HEADERS_H_
-#define COBALT_XHR_XHR_MODIFY_HEADERS_H_
-
-#include "net/http/http_request_headers.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace xhr {
-
-void CobaltXhrModifyHeader(const GURL& request_url,
-                           net::HttpRequestHeaders* headers);
-
-}  // namespace xhr
-}  // namespace cobalt
-
-#endif  // COBALT_XHR_XHR_MODIFY_HEADERS_H_
diff --git a/cobalt/xhr/xhr_settings.h b/cobalt/xhr/xhr_settings.h
new file mode 100644
index 0000000..be07a62
--- /dev/null
+++ b/cobalt/xhr/xhr_settings.h
@@ -0,0 +1,103 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_XHR_XHR_SETTINGS_H_
+#define COBALT_XHR_XHR_SETTINGS_H_
+
+#include <string>
+
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+
+namespace cobalt {
+namespace xhr {
+
+// Holds browser wide settings for XMLHttpRequest.  Their default values are set
+// in XMLHttpRequest related classes, and the default values will be overridden
+// if the return values of the member functions are non-empty.
+// Please refer to where these functions are called for the particular
+// XMLHttpRequest behaviors being controlled by them.
+class XhrSettings {
+ public:
+  virtual base::Optional<bool> IsFetchBufferPoolEnabled() const = 0;
+  virtual base::Optional<int> GetDefaultFetchBufferSize() const = 0;
+  virtual base::Optional<bool> IsTryLockForProgressCheckEnabled() const = 0;
+
+ protected:
+  XhrSettings() = default;
+  ~XhrSettings() = default;
+
+  XhrSettings(const XhrSettings&) = delete;
+  XhrSettings& operator=(const XhrSettings&) = delete;
+};
+
+// Allows setting the values of XMLHttpRequest settings via a name and an int
+// value.
+// This class is thread safe.
+class XhrSettingsImpl : public XhrSettings {
+ public:
+  base::Optional<bool> IsFetchBufferPoolEnabled() const override {
+    base::AutoLock auto_lock(lock_);
+    return is_fetch_buffer_pool_enabled_;
+  }
+  base::Optional<int> GetDefaultFetchBufferSize() const override {
+    base::AutoLock auto_lock(lock_);
+    return default_fetch_buffer_size_;
+  }
+  base::Optional<bool> IsTryLockForProgressCheckEnabled() const override {
+    base::AutoLock auto_lock(lock_);
+    return is_try_lock_for_progress_check_enabled_;
+  }
+
+  bool Set(const std::string& name, int32_t value) {
+    if (name == "XHR.EnableFetchBufferPool") {
+      if (value == 0 || value == 1) {
+        LOG(INFO) << name << ": set to " << value;
+
+        base::AutoLock auto_lock(lock_);
+        is_fetch_buffer_pool_enabled_ = value != 0;
+        return true;
+      }
+    } else if (name == "XHR.DefaultFetchBufferSize") {
+      if (value > 0) {
+        LOG(INFO) << name << ": set to " << value;
+
+        base::AutoLock auto_lock(lock_);
+        default_fetch_buffer_size_ = value;
+        return true;
+      }
+    } else if (name == "XHR.EnableTryLockForProgressCheck") {
+      if (value == 0 || value == 1) {
+        LOG(INFO) << name << ": set to " << value;
+
+        base::AutoLock auto_lock(lock_);
+        is_try_lock_for_progress_check_enabled_ = value != 0;
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+ private:
+  mutable base::Lock lock_;
+  base::Optional<bool> is_fetch_buffer_pool_enabled_;
+  base::Optional<int> default_fetch_buffer_size_;
+  base::Optional<bool> is_try_lock_for_progress_check_enabled_;
+};
+
+}  // namespace xhr
+}  // namespace cobalt
+
+#endif  // COBALT_XHR_XHR_SETTINGS_H_
diff --git a/cobalt/xhr/xhr_settings_test.cc b/cobalt/xhr/xhr_settings_test.cc
new file mode 100644
index 0000000..9a2ceda
--- /dev/null
+++ b/cobalt/xhr/xhr_settings_test.cc
@@ -0,0 +1,91 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/xhr_settings.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace xhr {
+namespace {
+
+TEST(XhrSettingsImplTest, Empty) {
+  XhrSettingsImpl impl;
+
+  EXPECT_FALSE(impl.IsFetchBufferPoolEnabled());
+  EXPECT_FALSE(impl.GetDefaultFetchBufferSize());
+  EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, SunnyDay) {
+  XhrSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 1));
+  ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024 * 1024));
+  ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 1));
+
+  EXPECT_TRUE(impl.IsFetchBufferPoolEnabled().value());
+  EXPECT_EQ(impl.GetDefaultFetchBufferSize().value(), 1024 * 1024);
+  EXPECT_TRUE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, RainyDay) {
+  XhrSettingsImpl impl;
+
+  ASSERT_FALSE(impl.Set("XHR.EnableFetchBufferPool", 2));
+  ASSERT_FALSE(impl.Set("XHR.DefaultFetchBufferSize", -100));
+  ASSERT_FALSE(impl.Set("XHR.EnableTryLockForProgressCheck", 3));
+
+  EXPECT_FALSE(impl.IsFetchBufferPoolEnabled());
+  EXPECT_FALSE(impl.GetDefaultFetchBufferSize());
+  EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, ZeroValuesWork) {
+  XhrSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 0));
+  // O is an invalid value for "XHR.DefaultFetchBufferSize".
+  ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 0));
+
+  EXPECT_FALSE(impl.IsFetchBufferPoolEnabled().value());
+  EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled().value());
+}
+
+TEST(XhrSettingsImplTest, Updatable) {
+  XhrSettingsImpl impl;
+
+  ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 0));
+  ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024));
+  ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 0));
+
+  ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 1));
+  ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024 * 2));
+  ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 1));
+
+  EXPECT_TRUE(impl.IsFetchBufferPoolEnabled().value());
+  EXPECT_EQ(impl.GetDefaultFetchBufferSize().value(), 1024 * 2);
+  EXPECT_TRUE(impl.IsTryLockForProgressCheckEnabled().value());
+}
+
+TEST(XhrSettingsImplTest, InvalidSettingNames) {
+  XhrSettingsImpl impl;
+
+  ASSERT_FALSE(impl.Set("XHR.Invalid", 0));
+  ASSERT_FALSE(impl.Set("Invalid.EnableFetchBufferPool", 0));
+}
+
+}  // namespace
+}  // namespace xhr
+}  // namespace cobalt
diff --git a/cobalt/xhr/xml_http_request.cc b/cobalt/xhr/xml_http_request.cc
index b731be3..8b45ac0 100644
--- a/cobalt/xhr/xml_http_request.cc
+++ b/cobalt/xhr/xml_http_request.cc
@@ -42,7 +42,6 @@
 #include "cobalt/web/csp_delegate.h"
 #include "cobalt/web/environment_settings.h"
 #include "cobalt/xhr/global_stats.h"
-#include "cobalt/xhr/xhr_modify_headers.h"
 #include "nb/memory_scope.h"
 #include "net/http/http_util.h"
 
@@ -66,7 +65,9 @@
 };
 
 const char* kForbiddenMethods[] = {
-    "connect", "trace", "track",
+    "connect",
+    "trace",
+    "track",
 };
 
 // https://www.w3.org/TR/resource-timing-1/#dom-performanceresourcetiming-initiatortype
@@ -306,7 +307,8 @@
                                                 int64_t current, int64_t total,
                                                 int64_t current_network_bytes) {
   xhr_impl_->OnURLFetchDownloadProgress(source, current, total,
-                                        current_network_bytes);
+                                        current_network_bytes,
+                                        /* request_done = */ false);
 }
 void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) {
   xhr_impl_->OnURLFetchComplete(source);
@@ -346,7 +348,12 @@
       is_redirect_(false),
       method_(net::URLFetcher::GET),
       response_body_(new URLFetcherResponseWriter::Buffer(
-          URLFetcherResponseWriter::Buffer::kString)),
+          URLFetcherResponseWriter::Buffer::kString,
+          xhr->environment_settings()
+              ->context()
+              ->web_settings()
+              ->xhr_settings()
+              .IsTryLockForProgressCheckEnabled())),
       response_type_(XMLHttpRequest::kDefault),
       state_(XMLHttpRequest::kUnsent),
       upload_listener_(false),
@@ -512,16 +519,12 @@
   if (!in_service_worker && method_ == net::URLFetcher::GET) {
     loader::FetchInterceptorCoordinator::GetInstance()->TryIntercept(
         request_url_,
-        std::make_unique<
-            base::OnceCallback<void(std::unique_ptr<std::string>)>>(
-            base::BindOnce(&XMLHttpRequestImpl::SendIntercepted,
-                           base::Unretained(this))),
-        std::make_unique<base::OnceCallback<void(const net::LoadTimingInfo&)>>(
-            base::BindOnce(&XMLHttpRequestImpl::ReportLoadTimingInfo,
-                           base::Unretained(this))),
-        std::make_unique<base::OnceClosure>(base::BindOnce(
-            &XMLHttpRequestImpl::SendFallback, base::Unretained(this),
-            request_body, exception_state)));
+        base::BindOnce(&XMLHttpRequestImpl::SendIntercepted,
+                       base::Unretained(this)),
+        base::BindOnce(&XMLHttpRequestImpl::ReportLoadTimingInfo,
+                       base::Unretained(this)),
+        base::BindOnce(&XMLHttpRequestImpl::SendFallback,
+                       base::Unretained(this), request_body, exception_state));
     return;
   }
   SendFallback(request_body, exception_state);
@@ -556,8 +559,11 @@
 
   // OnURLFetchDownloadProgress().
   ChangeState(XMLHttpRequest::kLoading);
+  const auto& xhr_settings =
+      environment_settings()->context()->web_settings()->xhr_settings();
   response_body_ = new URLFetcherResponseWriter::Buffer(
-      URLFetcherResponseWriter::Buffer::kString);
+      URLFetcherResponseWriter::Buffer::kString,
+      xhr_settings.IsTryLockForProgressCheckEnabled());
   response_body_->Write(response->data(), response->size());
   if (fetch_callback_) {
     script::Handle<script::Uint8Array> data =
@@ -618,10 +624,6 @@
   error_ = false;
   upload_complete_ = false;
 
-#if defined(COBALT_ENABLE_XHR_HEADER_FILTERING)
-  CobaltXhrModifyHeader(request_url_, &request_headers_);
-#endif
-
   // Add request body, if appropriate.
   if ((method_ == net::URLFetcher::POST || method_ == net::URLFetcher::PUT) &&
       request_body) {
@@ -972,13 +974,13 @@
 }
 
 void XMLHttpRequestImpl::OnURLFetchDownloadProgress(
-    const net::URLFetcher* source, int64_t current, int64_t total,
-    int64_t current_network_bytes) {
+    const net::URLFetcher* source, int64_t /*current*/, int64_t /*total*/,
+    int64_t /*current_network_bytes*/, bool request_done) {
   TRACK_MEMORY_SCOPE("XHR");
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   DCHECK_NE(state_, XMLHttpRequest::kDone);
 
-  if (response_body_->HasProgressSinceLastGetAndReset() == 0) {
+  if (response_body_->HasProgressSinceLastGetAndReset(request_done) == 0) {
     return;
   }
 
@@ -986,12 +988,29 @@
   ChangeState(XMLHttpRequest::kLoading);
 
   if (fetch_callback_) {
-    std::string downloaded_data;
-    response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data);
-    script::Handle<script::Uint8Array> data = script::Uint8Array::New(
-        environment_settings()->context()->global_environment(),
-        downloaded_data.data(), downloaded_data.size());
-    fetch_callback_->value().Run(data);
+    if (response_body_->type() == URLFetcherResponseWriter::Buffer::kString) {
+      std::string downloaded_data;
+      response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data);
+      script::Handle<script::Uint8Array> data = script::Uint8Array::New(
+          environment_settings()->context()->global_environment(),
+          downloaded_data.data(), downloaded_data.size());
+      fetch_callback_->value().Run(data);
+    } else {
+      DCHECK_EQ(response_body_->type(),
+                URLFetcherResponseWriter::Buffer::kBufferPool);
+      std::vector<script::PreallocatedArrayBufferData> downloaded_buffers;
+      response_body_->GetAndResetDataAndDownloadProgress(request_done,
+                                                         &downloaded_buffers);
+      for (auto& downloaded_buffer : downloaded_buffers) {
+        auto array_buffer =
+            script::ArrayBuffer::New(settings_->context()->global_environment(),
+                                     std::move(downloaded_buffer));
+        script::Handle<script::Uint8Array> data = script::Uint8Array::New(
+            settings_->context()->global_environment(), array_buffer, 0,
+            array_buffer->ByteLength());
+        fetch_callback_->value().Run(data);
+      }
+    }
   }
 
   // Send a progress notification if at least 50ms have elapsed.
@@ -1038,7 +1057,7 @@
 
     // Ensure all fetched data is read and transferred to this XHR. This should
     // only be done for successful and error-free fetches.
-    OnURLFetchDownloadProgress(source, 0, 0, 0);
+    OnURLFetchDownloadProgress(source, 0, 0, 0, /* request_done = */ true);
 
     // The request may have completed too quickly, before URLFetcher's upload
     // progress timer had a chance to inform us upload is finished.
@@ -1295,9 +1314,8 @@
     // The request is done so it is safe to only keep the ArrayBuffer and clear
     // |response_body_|.  As |response_body_| will not be used unless the
     // request is re-opened.
-    std::unique_ptr<script::PreallocatedArrayBufferData> downloaded_data(
-        new script::PreallocatedArrayBufferData());
-    response_body_->GetAndResetData(downloaded_data.get());
+    script::PreallocatedArrayBufferData downloaded_data;
+    response_body_->GetAndResetData(&downloaded_data);
     auto array_buffer = script::ArrayBuffer::New(
         environment_settings()->context()->global_environment(),
         std::move(downloaded_data));
@@ -1338,7 +1356,18 @@
 
 void XMLHttpRequestImpl::StartRequest(const std::string& request_body) {
   TRACK_MEMORY_SCOPE("XHR");
-  LOG(INFO) << "Fetching: " << ClipUrl(request_url_, 200);
+
+  const auto& xhr_settings =
+      environment_settings()->context()->web_settings()->xhr_settings();
+  const bool fetch_buffer_pool_enabled =
+      xhr_settings.IsFetchBufferPoolEnabled().value_or(false);
+
+  if (fetch_callback_ && fetch_buffer_pool_enabled) {
+    LOG(INFO) << "Fetching (backed by BufferPool): "
+              << ClipUrl(request_url_, 200);
+  } else {
+    LOG(INFO) << "Fetching: " << ClipUrl(request_url_, 200);
+  }
 
   response_array_buffer_reference_.reset();
 
@@ -1347,15 +1376,30 @@
   url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_);
   ++url_fetcher_generation_;
   url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
+
   if (fetch_callback_) {
-    response_body_ = new URLFetcherResponseWriter::Buffer(
-        URLFetcherResponseWriter::Buffer::kString);
-    response_body_->DisablePreallocate();
+    // FetchBufferPool is by default disabled, but can be explicitly overridden.
+    if (fetch_buffer_pool_enabled) {
+      response_body_ = new URLFetcherResponseWriter::Buffer(
+          URLFetcherResponseWriter::Buffer::kBufferPool,
+          xhr_settings.IsTryLockForProgressCheckEnabled(),
+          xhr_settings.GetDefaultFetchBufferSize());
+    } else {
+      response_body_ = new URLFetcherResponseWriter::Buffer(
+          URLFetcherResponseWriter::Buffer::kString,
+          xhr_settings.IsTryLockForProgressCheckEnabled());
+      response_body_->DisablePreallocate();
+    }
   } else {
-    response_body_ = new URLFetcherResponseWriter::Buffer(
-        response_type_ == XMLHttpRequest::kArrayBuffer
-            ? URLFetcherResponseWriter::Buffer::kArrayBuffer
-            : URLFetcherResponseWriter::Buffer::kString);
+    if (response_type_ == XMLHttpRequest::kArrayBuffer) {
+      response_body_ = new URLFetcherResponseWriter::Buffer(
+          URLFetcherResponseWriter::Buffer::kArrayBuffer,
+          xhr_settings.IsTryLockForProgressCheckEnabled());
+    } else {
+      response_body_ = new URLFetcherResponseWriter::Buffer(
+          URLFetcherResponseWriter::Buffer::kString,
+          xhr_settings.IsTryLockForProgressCheckEnabled());
+    }
   }
   std::unique_ptr<net::URLFetcherResponseWriter> download_data_writer(
       new URLFetcherResponseWriter(response_body_));
diff --git a/cobalt/xhr/xml_http_request.h b/cobalt/xhr/xml_http_request.h
index 0b0926a..30c606d 100644
--- a/cobalt/xhr/xml_http_request.h
+++ b/cobalt/xhr/xml_http_request.h
@@ -290,7 +290,8 @@
   void OnURLFetchResponseStarted(const net::URLFetcher* source);
   void OnURLFetchDownloadProgress(const net::URLFetcher* source,
                                   int64_t current, int64_t total,
-                                  int64_t current_network_bytes);
+                                  int64_t current_network_bytes,
+                                  bool request_done);
   void OnURLFetchComplete(const net::URLFetcher* source);
 
   void OnURLFetchUploadProgress(const net::URLFetcher* source, int64 current,
diff --git a/components/prefs/json_pref_store.cc b/components/prefs/json_pref_store.cc
index ef86d93..7d8d189 100644
--- a/components/prefs/json_pref_store.cc
+++ b/components/prefs/json_pref_store.cc
@@ -206,7 +206,9 @@
   base::Value* old_value = nullptr;
   prefs_->Get(key, &old_value);
   if (!old_value || !value->Equals(old_value)) {
-    prefs_->Set(key, std::move(value));
+    // Value::DictionaryValue::Set creates a nested dictionary treating a URL
+    // key as a path, SetKey avoids this.
+    prefs_->SetKey(key, std::move(*value.get()));
     ReportValueChanged(key, flags);
   }
 }
@@ -220,7 +222,7 @@
   base::Value* old_value = nullptr;
   prefs_->Get(key, &old_value);
   if (!old_value || !value->Equals(old_value)) {
-    prefs_->Set(key, std::move(value));
+    prefs_->SetPath({key}, base::Value::FromUniquePtrValue(std::move(value)));
     ScheduleWrite(flags);
   }
 }
@@ -228,15 +230,16 @@
 void JsonPrefStore::RemoveValue(const std::string& key, uint32_t flags) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  if (prefs_->RemovePath(key, nullptr))
+  if (prefs_->RemovePath({key})) {
     ReportValueChanged(key, flags);
+  }
 }
 
 void JsonPrefStore::RemoveValueSilently(const std::string& key,
                                         uint32_t flags) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  prefs_->RemovePath(key, nullptr);
+  prefs_->RemovePath({key});
   ScheduleWrite(flags);
 }
 
diff --git a/components/update_client/cobalt_slot_management.h b/components/update_client/cobalt_slot_management.h
index 8eb40c9..2efa5e2 100644
--- a/components/update_client/cobalt_slot_management.h
+++ b/components/update_client/cobalt_slot_management.h
@@ -19,7 +19,7 @@
 
 #include "base/files/file_path.h"
 #include "base/version.h"
-#include "cobalt/extension/installation_manager.h"
+#include "starboard/extension/installation_manager.h"
 
 namespace update_client {
 
diff --git a/components/update_client/cobalt_slot_management_test.cc b/components/update_client/cobalt_slot_management_test.cc
index 8568c31..21ebfd3 100644
--- a/components/update_client/cobalt_slot_management_test.cc
+++ b/components/update_client/cobalt_slot_management_test.cc
@@ -18,8 +18,8 @@
 #include <vector>
 
 #include "base/strings/string_util.h"
-#include "cobalt/extension/free_space.h"
 #include "starboard/common/file.h"
+#include "starboard/extension/free_space.h"
 #include "starboard/loader_app/app_key_files.h"
 #include "starboard/loader_app/drain_file.h"
 #include "starboard/loader_app/installation_manager.h"
diff --git a/components/update_client/component.h b/components/update_client/component.h
index 4944204..ddefa13 100644
--- a/components/update_client/component.h
+++ b/components/update_client/component.h
@@ -26,7 +26,7 @@
 #include "url/gurl.h"
 
 #if defined(STARBOARD)
-#include "cobalt/extension/installation_manager.h"
+#include "starboard/extension/installation_manager.h"
 #endif
 
 namespace base {
diff --git a/components/update_client/crx_downloader.h b/components/update_client/crx_downloader.h
index ae19a4a..76c2e6a 100644
--- a/components/update_client/crx_downloader.h
+++ b/components/update_client/crx_downloader.h
@@ -20,8 +20,8 @@
 #include "url/gurl.h"
 
 #if defined(STARBOARD)
-#include "cobalt/extension/installation_manager.h"
 #include "components/update_client/configurator.h"
+#include "starboard/extension/installation_manager.h"
 #endif
 
 namespace update_client {
diff --git a/components/update_client/update_checker.cc b/components/update_client/update_checker.cc
index 8c18587..1ecf44f 100644
--- a/components/update_client/update_checker.cc
+++ b/components/update_client/update_checker.cc
@@ -20,9 +20,6 @@
 #include "base/strings/stringprintf.h"
 #include "base/task/post_task.h"
 #include "base/threading/thread_checker.h"
-#if defined(STARBOARD)
-#include "cobalt/extension/free_space.h"
-#endif
 #include "base/threading/thread_task_runner_handle.h"
 #include "build/build_config.h"
 #if defined(STARBOARD)
@@ -40,6 +37,9 @@
 #include "components/update_client/update_client.h"
 #include "components/update_client/updater_state.h"
 #include "components/update_client/utils.h"
+#if defined(STARBOARD)
+#include "starboard/extension/free_space.h"
+#endif
 #include "url/gurl.h"
 
 namespace update_client {
diff --git a/components/update_client/update_checker.h b/components/update_client/update_checker.h
index 7c7c062..b3b7221 100644
--- a/components/update_client/update_checker.h
+++ b/components/update_client/update_checker.h
@@ -19,7 +19,7 @@
 #include "url/gurl.h"
 
 #if defined(STARBOARD)
-#include "cobalt/extension/installation_manager.h"
+#include "starboard/extension/installation_manager.h"
 #endif
 
 namespace update_client {
diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml
index 6aff57b..7999d4b 100644
--- a/docker-compose-windows.yml
+++ b/docker-compose-windows.yml
@@ -38,6 +38,11 @@
   NINJA_FLAGS: ${NINJA_FLAGS}
 
 services:
+
+  # ----------------------------------------
+  # Base images for Visual Studio dependency
+  # ----------------------------------------
+
   visual-studio-base:
     build:
       context: ./docker/windows/base/visualstudio2017
@@ -47,6 +52,28 @@
     image: visual-studio-base
     scale: 0
 
+  visual-studio-win32-base:
+    build:
+      context: ./docker/windows/base/visualstudio2017
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=mcr.microsoft.com/windows:1809
+    image: visual-studio-win32-base
+    scale: 0
+
+  visual-studio-2022-win32-base:
+    build:
+      context: ./docker/windows/base/visualstudio2022
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=mcr.microsoft.com/windows:1809
+    image: visual-studio-2022-win32-base
+    scale: 0
+
+  # -----------------------------------------------
+  # Cobalt-Build images for windows-based platforms
+  # -----------------------------------------------
+
   cobalt-build-win-base:
     build:
       context: ./docker/windows/base/build
@@ -58,15 +85,6 @@
     depends_on:
       - visual-studio-base
 
-  visual-studio-win32-base:
-    build:
-      context: ./docker/windows/base/visualstudio2017
-      dockerfile: ./Dockerfile
-      args:
-        - FROM_IMAGE=mcr.microsoft.com/windows:1809
-    image: visual-studio-win32-base
-    scale: 0
-
   cobalt-build-win32-base:
     build:
       context: ./docker/windows/base/build
@@ -78,6 +96,17 @@
     depends_on:
       - visual-studio-win32-base
 
+  cobalt-build-vs2022-win32-base:
+    build:
+      context: ./docker/windows/base/build
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=visual-studio-2022-win32-base
+    depends_on:
+      - visual-studio-2022-win32-base
+    image: cobalt-build-vs2022-win32-base
+    scale: 0
+
   build-win-win32:
     <<: *common-definitions
     build:
@@ -87,11 +116,17 @@
       - cobalt-build-win32-base
     image: cobalt-build-win-win32
 
+  # -----------------------------------------
+  # Win32 Platform Images for Building Cobalt
+  # -----------------------------------------
+
   win-win32:
     <<: *common-definitions
     build:
       context: ./docker/windows/win32
       dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=cobalt-build-win32-base
     depends_on:
       - cobalt-build-win32-base
     environment:
@@ -99,6 +134,20 @@
       PLATFORM: win-win32
     image: cobalt-build-win32
 
+  win-win32-vs2022:
+    <<: *common-definitions
+    build:
+      context: ./docker/windows/win32
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=cobalt-build-vs2022-win32-base
+    depends_on:
+      - cobalt-build-vs2022-win32-base
+    environment:
+      <<: *shared-build-env
+      PLATFORM: win-win32
+    image: cobalt-build-win32-vs2022
+
   runner-win-win32:
     <<: *common-definitions
     build:
diff --git a/docker/linux/android/Dockerfile b/docker/linux/android/Dockerfile
index f370c42..5a14a61 100644
--- a/docker/linux/android/Dockerfile
+++ b/docker/linux/android/Dockerfile
@@ -18,6 +18,7 @@
     && apt install -qqy --no-install-recommends \
         libxml2-dev \
         default-jdk \
+        binutils-arm-linux-gnueabi \
         g++-multilib \
     && /opt/clean-after-apt.sh
 
diff --git a/docker/windows/base/build/Dockerfile b/docker/windows/base/build/Dockerfile
index 19aa9b6..00da540 100644
--- a/docker/windows/base/build/Dockerfile
+++ b/docker/windows/base/build/Dockerfile
@@ -71,7 +71,7 @@
 
 # Set up GN
 RUN (New-Object Net.WebClient).DownloadFile(`
-    'https://chrome-infra-packages.appspot.com/dl/gn/gn/windows-amd64/+/c7arrb3NphJV3Hzo5oKj94VeQgVYY6AoatHY39wlWAEC',`
+    'https://chrome-infra-packages.appspot.com/dl/gn/gn/windows-amd64/+/ur-MX9ARZXAVL1MusvU3v4YebmmerRPLDsJQrTLvN1cC',`
     'C:\gn.zip') ; `
     Expand-Archive -Force C:\gn.zip C:\gn\ ; `
     Remove-Item -Path C:\gn.zip ; `
diff --git a/docker/windows/base/visualstudio2022/Dockerfile b/docker/windows/base/visualstudio2022/Dockerfile
new file mode 100644
index 0000000..f7e0530
--- /dev/null
+++ b/docker/windows/base/visualstudio2022/Dockerfile
@@ -0,0 +1,44 @@
+# escape=`
+
+# Copyright 2021 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ARG FROM_IMAGE
+FROM ${FROM_IMAGE}
+
+# Dockerfile for image used to install Visual Studio.
+# WARNING: Changes to this file will result in an extremely long image rebuild.
+
+SHELL ["powershell", "-ExecutionPolicy", "unrestricted", "-Command"]
+
+RUN mkdir C:\TEMP;`
+    Write-Host ('Downloading vs_buildtools.exe');`
+    Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe `
+      -OutFile C:\TEMP\vs_buildtools.exe
+
+RUN Write-Host ('Installing vs_buildtools.exe');`
+    Start-Process C:\TEMP\vs_buildtools.exe -Wait -NoNewWindow`
+        -ArgumentList '--quiet --wait --norestart --nocache`
+        --installPath C:\BuildTools `
+        --add Microsoft.VisualStudio.Component.VC.CoreIde `
+        --add Microsoft.VisualStudio.Component.VC.14.34.17.4.x86.x64 `
+        --add Microsoft.VisualStudio.Component.Windows10SDK.18362';`
+    Write-Host ('Cleaning up vs_buildtools.exe');`
+    Remove-Item -Force -Recurse ${env:ProgramFiles(x86)}\'Microsoft Visual Studio'\Installer;`
+    Remove-Item -Force -Recurse $env:TEMP\*;`
+    Remove-Item C:\TEMP\vs_buildtools.exe
+
+ENV VSINSTALLDIR "C:\BuildTools"
+ENV VS_INSTALL_DIR "C:\BuildTools"
+
+ENV VISUAL_STUDIO_VERSION "2022"
diff --git a/docker/windows/runner/Dockerfile b/docker/windows/runner/Dockerfile
index 1df562f..a494811 100644
--- a/docker/windows/runner/Dockerfile
+++ b/docker/windows/runner/Dockerfile
@@ -17,12 +17,12 @@
 
 ARG RUNNER_VERSION
 
-RUN Invoke-WebRequest -Uri 'https://aka.ms/install-powershell.ps1' -OutFile install-powershell.ps1;`
+RUN Invoke-WebRequest -Uri 'https://aka.ms/install-powershell.ps1' -OutFile install-powershell.ps1; \
     powershell -ExecutionPolicy Unrestricted -File ./install-powershell.ps1 -AddToPath
 
-RUN Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v$env:RUNNER_VERSION/actions-runner-win-x64-$env:RUNNER_VERSION.zip -OutFile runner.zip;`
-    Expand-Archive -Path C:/runner.zip -DestinationPath C:/actions-runner;`
-    Remove-Item -Path C:\runner.zip;`
+RUN Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v$env:RUNNER_VERSION/actions-runner-win-x64-$env:RUNNER_VERSION.zip -OutFile runner.zip; \
+    Expand-Archive -Path C:/runner.zip -DestinationPath C:/actions-runner; \
+    Remove-Item -Path C:\runner.zip; \
     setx /M PATH $(${Env:PATH} + \";${Env:ProgramFiles}\Git\bin\")
 
 # Required for packaging artifacts.
diff --git a/docker/windows/win32/Dockerfile b/docker/windows/win32/Dockerfile
index b993b9e..794bb4b 100644
--- a/docker/windows/win32/Dockerfile
+++ b/docker/windows/win32/Dockerfile
@@ -13,7 +13,9 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-FROM cobalt-build-win32-base
+ARG FROM_IMAGE
+FROM ${FROM_IMAGE}
+
 
 SHELL ["powershell", "-ExecutionPolicy", "Unrestricted", "-Command", `
        "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
diff --git a/download_resources.py b/download_resources.py
index 688f4e8..db26837 100644
--- a/download_resources.py
+++ b/download_resources.py
@@ -16,7 +16,6 @@
 
 import logging
 import os
-import platform
 import subprocess
 try:
   import urllib.request as urllib
@@ -26,25 +25,6 @@
 from tools import download_from_gcs
 
 
-def DownloadClangFormat(force=False):
-  clang_format_sha_path = os.path.join('buildtools', '{}', 'clang-format')
-
-  system = platform.system()
-  if system == 'Linux':
-    clang_format_sha_path = clang_format_sha_path.format('linux64')
-  elif system == 'Darwin':
-    clang_format_sha_path = clang_format_sha_path.format('mac')
-  elif system == 'Windows':
-    clang_format_sha_path = clang_format_sha_path.format('win') + '.exe'
-  else:
-    logging.error('Unknown system: %s', system)
-    return
-
-  download_from_gcs.MaybeDownloadFileFromGcs('chromium-clang-format',
-                                             clang_format_sha_path + '.sha1',
-                                             clang_format_sha_path, force)
-
-
 def DownloadGerritCommitMsgHook(force=False):
   git_dir = subprocess.check_output(['git', 'rev-parse', '--git-common-dir'
                                     ]).strip().decode('utf-8')
@@ -78,5 +58,4 @@
   logging.basicConfig(
       level=logging.INFO, format=logging_format, datefmt='%H:%M:%S')
 
-  DownloadClangFormat()
   DownloadGerritCommitMsgHook()
diff --git a/net/dial/dial_udp_server.cc b/net/dial/dial_udp_server.cc
index 3ad43c0..7173221 100644
--- a/net/dial/dial_udp_server.cc
+++ b/net/dial/dial_udp_server.cc
@@ -32,7 +32,7 @@
 namespace {  // anonymous
 
 const char* kDialStRequest = "urn:dial-multiscreen-org:service:dial:1";
-const int kReadBufferSize = 500 * 1024;
+const int kReadBufferSize = 50 * 1024;
 
 // Get the INADDR_ANY address.
 IPEndPoint GetAddressForAllInterfaces(unsigned short port) {
diff --git a/net/disk_cache/cobalt/resource_type.h b/net/disk_cache/cobalt/resource_type.h
index 6397a9b..f291033 100644
--- a/net/disk_cache/cobalt/resource_type.h
+++ b/net/disk_cache/cobalt/resource_type.h
@@ -30,7 +30,8 @@
   kUncompiledScript = 6,
   kCompiledScript = 7,
   kCacheApi = 8,
-  kTypeCount = 9
+  kServiceWorkerScript = 9,
+  kTypeCount = 10,
 };
 
 struct ResourceTypeMetadata {
@@ -43,10 +44,10 @@
 // persisted values saved in settings.json.
 static ResourceTypeMetadata kTypeMetadata[] = {
     {"other", kInitialBytes},         {"html", 2 * 1024 * 1024},
-    {"css", 1 * 1024 * 1024},         {"image", kInitialBytes},
+    {"css", 1 * 1024 * 1024},         {"image", 0},
     {"font", kInitialBytes},          {"splash", 2 * 1024 * 1024},
     {"uncompiled_js", kInitialBytes}, {"compiled_js", kInitialBytes},
-    {"cache_api", kInitialBytes},
+    {"cache_api", kInitialBytes},     {"service_worker_js", kInitialBytes},
 };
 
 }  // namespace disk_cache
diff --git a/net/spdy/spdy_network_transaction_unittest.cc b/net/spdy/spdy_network_transaction_unittest.cc
index 6353a46..e58d681 100644
--- a/net/spdy/spdy_network_transaction_unittest.cc
+++ b/net/spdy/spdy_network_transaction_unittest.cc
@@ -4676,7 +4676,7 @@
 }
 
 // Verify the case where we buffer data and cancel the transaction.
-TEST_F(SpdyNetworkTransactionTest, BufferedCancelled) {
+TEST_F(SpdyNetworkTransactionTest, DISABLED_BufferedCancelled) {
   spdy::SpdySerializedFrame req(
       spdy_util_.ConstructSpdyGet(nullptr, 0, 1, LOWEST));
   spdy::SpdySerializedFrame rst(
diff --git a/net/url_request/url_fetcher_core.h b/net/url_request/url_fetcher_core.h
index 4359ac5..2c4da97 100644
--- a/net/url_request/url_fetcher_core.h
+++ b/net/url_request/url_fetcher_core.h
@@ -15,7 +15,7 @@
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "base/timer/timer.h"
-#include "cobalt/extension/url_fetcher_observer.h"
+#include "starboard/extension/url_fetcher_observer.h"
 #include "net/base/chunked_upload_data_stream.h"
 #include "net/base/host_port_pair.h"
 #if defined(STARBOARD)
diff --git a/precommit_hooks/run_python2_unittests.py b/precommit_hooks/run_python2_unittests.py
deleted file mode 100644
index 427b952..0000000
--- a/precommit_hooks/run_python2_unittests.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python2
-# Copyright 2021 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Runs all Python 2 unit tests in a directory with a changed file."""
-
-import glob
-import os
-import re
-import subprocess
-import sys
-
-PROJECT_ROOT = os.path.abspath(
-    os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
-
-
-def RunPyTests(files):
-  """Checks that Python unit tests pass.
-
-  Use unit test discovery to find and run all tests in directories
-  with changed files.
-  Args:
-    input_api: presubmit_support input API object
-    output_api: presubmit_support output API object
-    source_file_filter: FileFilterGenerator
-
-  Returns:
-    List of failed checks
-  """
-  print('Running Python unit tests.')
-
-  # Get the set of directories to look in.
-  dirs_to_check = set(os.path.dirname(f) for f in files)
-  unittest_re = re.compile(r'(import\s+unittest)|(from\s+unittest\s+import)')
-
-  def _ImportsUnittestAndIsPython2Compatible(filename):
-    with open(filename) as f:
-      for line_number, line in enumerate(f):
-        if line_number == 0 and line.startswith('#!') and 'python3' in line:
-          return False
-        if unittest_re.search(line):
-          return True
-    return False
-
-  # Discover and run in each directory.
-  for d in dirs_to_check:
-    tests = glob.glob(d + '/*_test.py')
-    if not tests:
-      continue
-
-    # Check if each one actually import the unittest module.
-    # If not, don't run it.
-    tests = [x for x in tests if _ImportsUnittestAndIsPython2Compatible(x)]
-    for test in tests:
-      res = subprocess.call([sys.executable, test])
-      if res:
-        return True
-
-  return False
-
-
-if __name__ == '__main__':
-  sys.exit(RunPyTests(sys.argv[1:]))
diff --git a/requirements.txt b/requirements.txt
index 2732a91..ac34bbc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+clang-format
 cpplint<2
 pre-commit<3
 pylint<3
@@ -7,3 +8,4 @@
 selenium==3.141.0
 Brotli==1.0.9
 six<2
+pytest-pythonpath<0.8
diff --git a/setup.cfg b/setup.cfg
index 855b071..d1c0964 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,10 +2,10 @@
 name = cobalt
 
 [tool:pytest]
+python_paths=.
 norecursedirs=
   .*
   build
-  cobalt/bindings
   cobalt/black_box_tests/tests
   cobalt/media_integration_tests
   out
@@ -13,8 +13,6 @@
   testing/gtest
   testing/gmock
   third_party
-  tools/idl_parser
-  tools/lbshell/coverage
 
 # Ignore non-test files with test_* names that pytest would otherwise pick up.
 addopts=
diff --git a/starboard/BUILD.gn b/starboard/BUILD.gn
index 741b336..cb5ecb1 100644
--- a/starboard/BUILD.gn
+++ b/starboard/BUILD.gn
@@ -22,7 +22,9 @@
     "//starboard/client_porting/eztime:eztime_test",
     "//starboard/client_porting/icu_init",
     "//starboard/client_porting/poem:poem_unittests",
+    "//starboard/examples/hello_world:starboard_hello_world_example",
     "//starboard/examples/window:starboard_window_example",
+    "//starboard/extension:extension_test",
     "//starboard/loader_app:app_key_files_test",
     "//starboard/nplb",
     "//starboard/nplb/nplb_evergreen_compat_tests",
@@ -145,7 +147,6 @@
     "queue.h",
     "socket.h",
     "socket_waiter.h",
-    "spin_lock.h",
     "storage.h",
     "string.h",
     "system.h",
diff --git a/starboard/CHANGELOG.md b/starboard/CHANGELOG.md
index fddc2c3..49438ee 100644
--- a/starboard/CHANGELOG.md
+++ b/starboard/CHANGELOG.md
@@ -14,10 +14,16 @@
 can be found in the comments of the "Experimental Feature Defines" section of
 [configuration.h](configuration.h).
 
-## Version 14
-### Require kSbSystemPathStorageDirectory on all platforms.
-Path to directory for permanent persistent storage.
+### Cobalt extensions are now Starboard extensions
+Previously named Cobalt extensions are now found under `starboard/extensions`.
+The mechanism extends platform-specific functionality of Starboard via runtime
+resolution, and hence more properly belongs in Starboard codebase.
+This also helps break the dependency cycle between Starboard and Cobalt for
+cleaner component layering.
+For existing uses in Starboard ports, fallback forwarding headers are provided
+in the previous location of the code in `cobalt/extensions`.
 
+## Version 14
 ### Add MP3, FLAC, and PCM values to SbMediaAudioCodec.
 This makes it possible to support these codecs in the future.
 
diff --git a/starboard/android/apk/app/build.gradle b/starboard/android/apk/app/build.gradle
index 832b8fe..c88e2d3 100644
--- a/starboard/android/apk/app/build.gradle
+++ b/starboard/android/apk/app/build.gradle
@@ -180,13 +180,14 @@
 
 dependencies {
     implementation fileTree(include: ['*.jar'], dir: 'libs')
+    // GameActivity dependency. Follow the steps in
+    // //starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
+    // if you want to update to a new version.
+    implementation fileTree(dir: 'src/main/java/dev/cobalt/libraries/game_activity',
+                            include: ['games-activity*.aar'])
     implementation 'androidx.annotation:annotation:1.1.0'
     implementation 'androidx.appcompat:appcompat:1.4.2'
     implementation 'androidx.core:core:1.8.0'
-    // When updating the games-activity version here, make sure to update
-    // the C++ sources in //third_party/android_game_activity from the same
-    // release package.
-    implementation 'androidx.games:games-activity:1.2.1'
     implementation 'androidx.leanback:leanback:1.0.0'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
     implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
diff --git a/starboard/android/apk/app/proguard-rules.pro b/starboard/android/apk/app/proguard-rules.pro
index 7038356..a4b8a54 100644
--- a/starboard/android/apk/app/proguard-rules.pro
+++ b/starboard/android/apk/app/proguard-rules.pro
@@ -33,15 +33,3 @@
 -keepclasseswithmembers class * {
   @dev.cobalt.util.UsedByNative <fields>;
 }
-
-# Keep GameActivity APIs used by JNI (b/254102295).
--keepclassmembers class com.google.androidgamesdk.GameActivity {
-   void setWindowFlags(int, int);
-   public androidx.core.graphics.Insets getWindowInsets(int);
-   public androidx.core.graphics.Insets getWaterfallInsets();
-   public void setImeEditorInfo(android.view.inputmethod.EditorInfo);
-   public void setImeEditorInfoFields(int, int, int);
-}
--keep class androidx.core.graphics.Insets** { *; }
--keep class androidx.core.view.WindowInsetsCompat** { *; }
--keep class com.google.androidgamesdk.gametextinput.** { *; }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
index bde7982..983eec6 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
@@ -17,6 +17,7 @@
 import static dev.cobalt.util.Log.TAG;
 
 import android.content.Context;
+import androidx.annotation.GuardedBy;
 import com.google.android.gms.ads.identifier.AdvertisingIdClient;
 import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
 import com.google.android.gms.common.GooglePlayServicesRepairableException;
@@ -29,27 +30,28 @@
 public class AdvertisingId {
   private final Context context;
   private final ExecutorService singleThreadExecutor;
-  private long lastRefreshed;
-  private final long cacheTtlMs = 1000 * 60 * 10; // 10 minutes.
-  private AdvertisingIdClient.Info advertisingIdInfo;
+
+  @GuardedBy("advertisingIdInfoLock")
+  private volatile AdvertisingIdClient.Info advertisingIdInfo;
+  // Controls access to advertisingIdInfo
+  private final Object advertisingIdInfoLock = new Object();
 
   public AdvertisingId(Context context) {
     this.context = context;
-    this.lastRefreshed = 0;
     this.singleThreadExecutor = Executors.newSingleThreadExecutor();
+    this.advertisingIdInfo = null;
     refresh();
   }
 
-  private void refresh() {
-    if (System.currentTimeMillis() - lastRefreshed < cacheTtlMs) {
-      // Cache is up to date.
-      return;
-    }
+  public void refresh() {
     singleThreadExecutor.execute(
         () -> {
           try {
-            advertisingIdInfo = AdvertisingIdClient.getAdvertisingIdInfo(context);
-            lastRefreshed = System.currentTimeMillis();
+            // The following statement may be slow.
+            AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(context);
+            synchronized (advertisingIdInfoLock) {
+              advertisingIdInfo = info;
+            }
             Log.i(TAG, "Successfully retrieved Advertising ID (IfA).");
           } catch (IOException
               | GooglePlayServicesNotAvailableException
@@ -61,20 +63,24 @@
 
   public String getId() {
     String result = "";
-    if (lastRefreshed != 0) {
-      result = advertisingIdInfo.getId();
-      refresh();
+    synchronized (advertisingIdInfoLock) {
+      if (advertisingIdInfo != null) {
+        result = advertisingIdInfo.getId();
+      }
     }
+    refresh();
     Log.d(TAG, "Returning IfA getId: " + result);
     return result;
   }
 
   public boolean isLimitAdTrackingEnabled() {
     boolean result = false;
-    if (lastRefreshed != 0) {
-      result = advertisingIdInfo.isLimitAdTrackingEnabled();
-      refresh();
+    synchronized (advertisingIdInfoLock) {
+      if (advertisingIdInfo != null) {
+        result = advertisingIdInfo.isLimitAdTrackingEnabled();
+      }
     }
+    refresh();
     Log.d(TAG, "Returning IfA LimitedAdTrackingEnabled: " + Boolean.toString(result));
     return result;
   }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
index f956eff..b323fcb 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
@@ -104,6 +104,7 @@
 
   private final HashMap<String, CobaltService.Factory> cobaltServiceFactories = new HashMap<>();
   private final HashMap<String, CobaltService> cobaltServices = new HashMap<>();
+  private final HashMap<String, String> crashContext = new HashMap<>();
 
   private static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("America/Los_Angeles");
   private final long timeNanosecondsPerMicrosecond = 1000;
@@ -227,6 +228,7 @@
     for (CobaltService service : cobaltServices.values()) {
       service.beforeStartOrResume();
     }
+    advertisingId.refresh();
   }
 
   @SuppressWarnings("unused")
@@ -808,4 +810,24 @@
     }
     return 0;
   }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void reportFullyDrawn() {
+    Activity activity = activityHolder.get();
+    if (activity != null) {
+      activity.reportFullyDrawn();
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void setCrashContext(String key, String value) {
+    Log.i(TAG, "setCrashContext Called: " + key + ", " + value);
+    crashContext.put(key, value);
+  }
+
+  public HashMap<String, String> getCrashContext() {
+    return this.crashContext;
+  }
 }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
new file mode 100644
index 0000000..99d8e12
--- /dev/null
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
@@ -0,0 +1,25 @@
+# Android GameActivity
+
+The library in this directory is copied from the AndroidX GameActivity
+release package.
+
+To learn more about GameActivity, refer to [the official GameActivity
+documentation](https://d.android.com/games/agdk/game-activity).
+
+The library in this directory is used by the Android in
+//starboard/android/apk/app/build.gradle and also the native files are
+automatically extracted and used in //starboard/android/shared/BUILD.gn.
+
+## Updating instructions
+
+To update GameActivity to the latest version, do the following:
+
+1. Download the .aar file from Google Maven at
+   https://maven.google.com/web/index.html#androidx.games:games-activity
+   into this directory.
+
+1. Update `game_activity_aar_file` in //starboard/android/shared/BUILD.gn
+   to reflect the new .aar filename.
+
+1. Delete the old .aar file -- there should only be a single .aar in this
+   directory.
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.1.aar b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.1.aar
deleted file mode 100644
index c7d40aa..0000000
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.1.aar
+++ /dev/null
Binary files differ
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar
new file mode 100644
index 0000000..3b4cb1f
--- /dev/null
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar
Binary files differ
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
index 5050078..2b63cd8 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -157,8 +157,11 @@
       }
       // AudioTrack ctor can fail in multiple, platform specific ways, so do a thorough check
       // before proceed.
-      if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
-        break;
+      if (audioTrack != null) {
+        if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
+          break;
+        }
+        audioTrack = null;
       }
       audioTrackBufferSize /= 2;
     }
@@ -194,6 +197,7 @@
     return audioTrack.setVolume(gain);
   }
 
+  // TODO (b/262608024): Have this method return a boolean and return false on failure.
   @SuppressWarnings("unused")
   @UsedByNative
   private void play() {
@@ -201,9 +205,14 @@
       Log.e(TAG, "Unable to play with NULL audio track.");
       return;
     }
-    audioTrack.play();
+    try {
+      audioTrack.play();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, String.format("Unable to play audio track, error: %s", e.toString()));
+    }
   }
 
+  // TODO (b/262608024): Have this method return a boolean and return false on failure.
   @SuppressWarnings("unused")
   @UsedByNative
   private void pause() {
@@ -211,9 +220,14 @@
       Log.e(TAG, "Unable to pause with NULL audio track.");
       return;
     }
-    audioTrack.pause();
+    try {
+      audioTrack.pause();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, String.format("Unable to pause audio track, error: %s", e.toString()));
+    }
   }
 
+  // TODO (b/262608024): Have this method return a boolean and return false on failure.
   @SuppressWarnings("unused")
   @UsedByNative
   private void stop() {
@@ -221,7 +235,11 @@
       Log.e(TAG, "Unable to stop with NULL audio track.");
       return;
     }
-    audioTrack.stop();
+    try {
+      audioTrack.stop();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, String.format("Unable to stop audio track, error: %s", e.toString()));
+    }
   }
 
   @SuppressWarnings("unused")
diff --git a/starboard/android/arm/platform_configuration/configuration.gni b/starboard/android/arm/platform_configuration/configuration.gni
index a4cf9ff..a77aa89 100644
--- a/starboard/android/arm/platform_configuration/configuration.gni
+++ b/starboard/android/arm/platform_configuration/configuration.gni
@@ -16,4 +16,8 @@
 
 android_abi = "armeabi-v7a"
 arm_version = 7
+arm_float_abi = "softfp"
+
+sb_evergreen_compatible_package = true
+
 sabi_path = "//starboard/sabi/arm/softfp/sabi-v$sb_api_version.json"
diff --git a/starboard/android/shared/BUILD.gn b/starboard/android/shared/BUILD.gn
index ed227f0..c000807 100644
--- a/starboard/android/shared/BUILD.gn
+++ b/starboard/android/shared/BUILD.gn
@@ -17,6 +17,39 @@
 import("//starboard/shared/starboard/player/buildfiles.gni")
 import("//starboard/shared/starboard/player/player_tests.gni")
 
+##########################################################
+# Configuration to extract GameActivity native files.
+##########################################################
+
+game_activity_aar_file = "//starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar"
+
+game_activity_source_files = [
+  "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.cpp",
+  "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.h",
+  "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gamecommon.h",
+  "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.cpp",
+  "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.h",
+]
+
+game_activity_include_dirs =
+    [ "$target_gen_dir/game_activity/prefab/modules/game-activity/include" ]
+
+action("game_activity_sources") {
+  script = "//starboard/tools/unzip_file.py"
+  sources = [ game_activity_aar_file ]
+  outputs = game_activity_source_files
+  args = [
+    "--zip_file",
+    rebase_path(game_activity_aar_file, root_build_dir),
+    "--output_dir",
+    rebase_path(target_gen_dir, root_build_dir) + "/game_activity",
+  ]
+}
+
+##########################################################
+# Configuration for overall Android Starboard Platform.
+##########################################################
+
 static_library("starboard_platform") {
   sources = [
     "$target_gen_dir/ndk-sources/cpu-features.c",
@@ -262,11 +295,6 @@
     "//starboard/shared/stub/thread_sampler_is_supported.cc",
     "//starboard/shared/stub/thread_sampler_thaw.cc",
     "//starboard/shared/stub/ui_nav_get_interface.cc",
-    "//third_party/android_game_activity/include/game-activity/GameActivity.cpp",
-    "//third_party/android_game_activity/include/game-activity/GameActivity.h",
-    "//third_party/android_game_activity/include/game-text-input/gamecommon.h",
-    "//third_party/android_game_activity/include/game-text-input/gametextinput.cpp",
-    "//third_party/android_game_activity/include/game-text-input/gametextinput.h",
     "accessibility_get_caption_settings.cc",
     "accessibility_get_display_settings.cc",
     "accessibility_get_text_to_speech_settings.cc",
@@ -296,6 +324,8 @@
     "configuration.h",
     "configuration_constants.cc",
     "configuration_public.h",
+    "crash_handler.cc",
+    "crash_handler.h",
     "decode_target_create.cc",
     "decode_target_create.h",
     "decode_target_get_info.cc",
@@ -414,6 +444,7 @@
     "window_update_on_screen_keyboard_suggestions.cc",
   ]
 
+  sources += game_activity_source_files
   sources += common_player_sources
 
   sources -= [
@@ -424,11 +455,12 @@
     "//starboard/shared/starboard/player/player_set_playback_rate.cc",
   ]
 
-  include_dirs = [ "//third_party/android_game_activity/include" ]
+  include_dirs = game_activity_include_dirs
 
   configs += [ "//starboard/build/config:starboard_implementation" ]
 
   public_deps = [
+    ":game_activity_sources",
     ":ndk_sources",
     ":starboard_base_symbolize",
     "//starboard/common",
diff --git a/starboard/android/shared/android_main.cc b/starboard/android/shared/android_main.cc
index 3890b50..755210d 100644
--- a/starboard/android/shared/android_main.cc
+++ b/starboard/android/shared/android_main.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "game-activity/GameActivity.h"
 #include "starboard/android/shared/application_android.h"
 #include "starboard/android/shared/jni_env_ext.h"
 #include "starboard/android/shared/jni_utils.h"
@@ -21,7 +22,6 @@
 #include "starboard/log.h"
 #include "starboard/shared/starboard/command_line.h"
 #include "starboard/thread.h"
-#include "third_party/android_game_activity/include/game-activity/GameActivity.h"
 
 namespace starboard {
 namespace android {
diff --git a/starboard/android/shared/android_media_session_client.cc b/starboard/android/shared/android_media_session_client.cc
index 3ddd6f6..c0986b8 100644
--- a/starboard/android/shared/android_media_session_client.cc
+++ b/starboard/android/shared/android_media_session_client.cc
@@ -127,9 +127,9 @@
 // In practice, only one MediaSessionClient will become active at a time.
 // Protected by "mutex"
 CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback
-    g_update_platform_playback_state_callback;
-CobaltExtensionMediaSessionInvokeActionCallback g_invoke_action_callback;
-void* g_callback_context;
+    g_update_platform_playback_state_callback = NULL;
+CobaltExtensionMediaSessionInvokeActionCallback g_invoke_action_callback = NULL;
+void* g_callback_context = NULL;
 
 void OnceInit() {
   SbMutexCreate(&mutex);
@@ -263,6 +263,17 @@
   SbMutexRelease(&mutex);
 }
 
+void DestroyMediaSessionClientCallback() {
+  SbOnce(&once_flag, OnceInit);
+  SbMutexAcquire(&mutex);
+
+  g_callback_context = NULL;
+  g_invoke_action_callback = NULL;
+  g_update_platform_playback_state_callback = NULL;
+
+  SbMutexRelease(&mutex);
+}
+
 }  // namespace
 
 const CobaltExtensionMediaSessionApi kMediaSessionApi = {
@@ -270,7 +281,7 @@
     1,
     &OnMediaSessionStateChanged,
     &RegisterMediaSessionCallbacks,
-    NULL,
+    &DestroyMediaSessionClientCallback,
     &UpdateActiveSessionPlatformPlaybackState};
 
 const void* GetMediaSessionApi() {
diff --git a/starboard/android/shared/android_media_session_client.h b/starboard/android/shared/android_media_session_client.h
index faeba0d..36f018b 100644
--- a/starboard/android/shared/android_media_session_client.h
+++ b/starboard/android/shared/android_media_session_client.h
@@ -15,7 +15,7 @@
 #ifndef STARBOARD_ANDROID_SHARED_ANDROID_MEDIA_SESSION_CLIENT_H_
 #define STARBOARD_ANDROID_SHARED_ANDROID_MEDIA_SESSION_CLIENT_H_
 
-#include "cobalt/extension/media_session.h"
+#include "starboard/extension/media_session.h"
 
 namespace starboard {
 namespace android {
diff --git a/starboard/android/shared/application_android.cc b/starboard/android/shared/application_android.cc
index 0f4cdb3..37a8077 100644
--- a/starboard/android/shared/application_android.cc
+++ b/starboard/android/shared/application_android.cc
@@ -19,6 +19,7 @@
 #include <time.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <string>
 #include <vector>
 
@@ -160,6 +161,7 @@
   if (SbWindowIsValid(window_)) {
     return kSbWindowInvalid;
   }
+  ScopedLock lock(input_mutex_);
   window_ = new SbWindowPrivate;
   window_->native_window = native_window_;
   input_events_generator_.reset(new InputEventsGenerator(window_));
@@ -171,6 +173,7 @@
     return false;
   }
 
+  ScopedLock lock(input_mutex_);
   input_events_generator_.reset();
 
   SB_DCHECK(window == window_);
@@ -180,12 +183,21 @@
 }
 
 Event* ApplicationAndroid::WaitForSystemEventWithTimeout(SbTime time) {
+  // Limit the polling time in case some non-system event is injected.
+  const int kMaxPollingTimeMillisecond = 10;
+
   // Convert from microseconds to milliseconds, taking the ceiling value.
   // If we take the floor, or round, then we end up busy looping every time
   // the next event time is less than one millisecond.
   int timeout_millis = (time + kSbTimeMillisecond - 1) / kSbTimeMillisecond;
   int looper_events;
-  int ident = ALooper_pollAll(timeout_millis, NULL, &looper_events, NULL);
+  int ident = ALooper_pollAll(
+      std::min(std::max(timeout_millis, 0), kMaxPollingTimeMillisecond), NULL,
+      &looper_events, NULL);
+
+  // Ignore new system events while processing one.
+  handle_system_events_ = false;
+
   switch (ident) {
     case kLooperIdAndroidCommand:
       ProcessAndroidCommand();
@@ -195,6 +207,8 @@
       break;
   }
 
+  handle_system_events_ = true;
+
   // Always return NULL since we already dispatched our own system events.
   return NULL;
 }
@@ -273,12 +287,16 @@
       // early in SendAndroidCommand().
       {
         ScopedLock lock(android_command_mutex_);
-        // Cobalt can't keep running without a window, even if the Activity
-        // hasn't stopped yet. Block until conceal event has been processed.
+// Cobalt can't keep running without a window, even if the Activity
+// hasn't stopped yet. Block until conceal event has been processed.
 
-        // Only process injected events -- don't check system events since
-        // that may try to acquire the already-locked android_command_mutex_.
+// Only process injected events -- don't check system events since
+// that may try to acquire the already-locked android_command_mutex_.
+#if SB_API_VERSION >= 13
         InjectAndProcess(kSbEventTypeConceal, /* checkSystemEvents */ false);
+#else
+        InjectAndProcess(kSbEventTypeSuspend, /* checkSystemEvents */ false);
+#endif
 
         if (window_) {
           window_->native_window = NULL;
@@ -315,10 +333,12 @@
     }
 
     // Remember the Android activity state to sync to when we have a window.
+    case AndroidCommand::kStop:
+      SbAtomicNoBarrier_Increment(&android_stop_count_, -1);
+    // Intentional fall-through.
     case AndroidCommand::kStart:
     case AndroidCommand::kResume:
     case AndroidCommand::kPause:
-    case AndroidCommand::kStop:
       sync_state = activity_state_ = cmd.type;
       break;
     case AndroidCommand::kDeepLink: {
@@ -344,9 +364,16 @@
     }
   }
 
+  // If there's an outstanding "stop" command, then don't update the app state
+  // since it'll be overridden by the upcoming "stop" state.
+  if (SbAtomicNoBarrier_Load(&android_stop_count_) > 0) {
+    return;
+  }
+
   // If there's a window, sync the app state to the Activity lifecycle.
   if (native_window_) {
     switch (sync_state) {
+#if SB_API_VERSION >= 13
       case AndroidCommand::kStart:
         Inject(new Event(kSbEventTypeReveal, NULL, NULL));
         break;
@@ -359,6 +386,20 @@
       case AndroidCommand::kStop:
         Inject(new Event(kSbEventTypeConceal, NULL, NULL));
         break;
+#else
+      case AndroidCommand::kStart:
+        Inject(new Event(kSbEventTypeResume, NULL, NULL));
+        break;
+      case AndroidCommand::kResume:
+        Inject(new Event(kSbEventTypeUnpause, NULL, NULL));
+        break;
+      case AndroidCommand::kPause:
+        Inject(new Event(kSbEventTypePause, NULL, NULL));
+        break;
+      case AndroidCommand::kStop:
+        Inject(new Event(kSbEventTypeSuspend, NULL, NULL));
+        break;
+#endif
       default:
         break;
     }
@@ -379,6 +420,9 @@
         android_command_condition_.Wait();
       }
       break;
+    case AndroidCommand::kStop:
+      SbAtomicNoBarrier_Increment(&android_stop_count_, 1);
+      break;
     default:
       break;
   }
@@ -388,6 +432,11 @@
     const GameActivityMotionEvent* event) {
   bool result = false;
 
+  ScopedLock lock(input_mutex_);
+  if (!input_events_generator_) {
+    return false;
+  }
+
   // add motion event into the queue.
   InputEventsGenerator::Events app_events;
   result = input_events_generator_->CreateInputEventsFromGameActivityEvent(
@@ -410,6 +459,11 @@
   }
 #endif
 
+  ScopedLock lock(input_mutex_);
+  if (!input_events_generator_) {
+    return false;
+  }
+
   // Add key event to the application queue.
   InputEventsGenerator::Events app_events;
   result = input_events_generator_->CreateInputEventsFromGameActivityEvent(
@@ -426,6 +480,7 @@
   int err = read(keyboard_inject_readfd_, &key, sizeof(key));
   SB_DCHECK(err >= 0) << "Keyboard inject read failed: errno=" << errno;
   SB_LOG(INFO) << "Keyboard inject: " << key;
+  ScopedLock lock(input_mutex_);
   if (!input_events_generator_) {
     SB_DLOG(WARNING) << "Injected input event ignored without an SbWindow.";
     return;
diff --git a/starboard/android/shared/application_android.h b/starboard/android/shared/application_android.h
index 634cc29..6cdb792 100644
--- a/starboard/android/shared/application_android.h
+++ b/starboard/android/shared/application_android.h
@@ -21,11 +21,13 @@
 #include <unordered_map>
 #include <vector>
 
+#include "game-activity/GameActivity.h"
 #include "starboard/android/shared/input_events_generator.h"
 #ifdef STARBOARD_INPUT_EVENTS_FILTER
 #include "starboard/android/shared/internal/input_events_filter.h"
 #endif
 #include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/atomic.h"
 #include "starboard/common/condition_variable.h"
 #include "starboard/common/mutex.h"
 #include "starboard/common/scoped_ptr.h"
@@ -34,7 +36,6 @@
 #include "starboard/shared/starboard/application.h"
 #include "starboard/shared/starboard/queue_application.h"
 #include "starboard/types.h"
-#include "third_party/android_game_activity/include/game-activity/GameActivity.h"
 
 namespace starboard {
 namespace android {
@@ -75,8 +76,13 @@
   bool OnSearchRequested();
   void HandleDeepLink(const char* link_url);
   void SendTTSChangedEvent() {
+#if SB_API_VERSION >= 13
     Inject(new Event(kSbEventTypeAccessibilityTextToSpeechSettingsChanged,
                      nullptr, nullptr));
+#else
+    Inject(new Event(kSbEventTypeAccessiblityTextToSpeechSettingsChanged,
+                     nullptr, nullptr));
+#endif
   }
 
   void SendAndroidCommand(AndroidCommand::CommandType type, void* data);
@@ -124,7 +130,7 @@
   void OnSuspend() override;
 
   // --- QueueApplication overrides ---
-  bool MayHaveSystemEvents() override { return true; }
+  bool MayHaveSystemEvents() override { return handle_system_events_; }
   Event* WaitForSystemEventWithTimeout(SbTime time) override;
   void WakeSystemEventWait() override;
 
@@ -138,17 +144,28 @@
   int keyboard_inject_readfd_;
   int keyboard_inject_writefd_;
 
+  // In certain situations, the Starboard thread should not try to process new
+  // system events (e.g. while one is being processed).
+  bool handle_system_events_ = true;
+
   // Synchronization for commands that change availability of Android resources
   // such as the input and/or native_window_.
   Mutex android_command_mutex_;
   ConditionVariable android_command_condition_;
 
+  // Track queued "stop" commands to avoid starting the app when Android has
+  // already requested it be stopped.
+  SbAtomic32 android_stop_count_ = 0;
+
   // The last Activity lifecycle state command received.
   AndroidCommand::CommandType activity_state_;
 
   // The single open window, if any.
   SbWindow window_;
 
+  // |input_events_generator_| is accessed from multiple threads, so use a mutex
+  // to safely access it.
+  Mutex input_mutex_;
   scoped_ptr<InputEventsGenerator> input_events_generator_;
 
 #ifdef STARBOARD_INPUT_EVENTS_FILTER
diff --git a/starboard/android/shared/audio_decoder.cc b/starboard/android/shared/audio_decoder.cc
index 2ad0bff..fb81100 100644
--- a/starboard/android/shared/audio_decoder.cc
+++ b/starboard/android/shared/audio_decoder.cc
@@ -24,7 +24,8 @@
 // Can be locally set to |1| for verbose audio decoding.  Verbose audio
 // decoding will log the following transitions that take place for each audio
 // unit:
-//   T1: Our client passes an |InputBuffer| of audio data into us.
+//   T1: Our client passes multiple packets (i.e. InputBuffers) of audio data
+//   into us.
 //   T2: We receive a corresponding media codec output buffer back from our
 //       |MediaCodecBridge|.
 //   T3: Our client reads a corresponding |DecodedAudio| out of us.
@@ -96,16 +97,18 @@
   media_decoder_->Initialize(error_cb_);
 }
 
-void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void AudioDecoder::Decode(const InputBuffers& input_buffers,
                           const ConsumedCB& consumed_cb) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
   SB_DCHECK(output_cb_);
   SB_DCHECK(media_decoder_);
 
-  VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+  for (const auto& input_buffer : input_buffers) {
+    VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+  }
 
-  media_decoder_->WriteInputBuffer(input_buffer);
+  media_decoder_->WriteInputBuffers(input_buffers);
 
   ScopedLock lock(decoded_audios_mutex_);
   if (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <=
diff --git a/starboard/android/shared/audio_decoder.h b/starboard/android/shared/audio_decoder.h
index 8a909fe..f9968b4 100644
--- a/starboard/android/shared/audio_decoder.h
+++ b/starboard/android/shared/audio_decoder.h
@@ -48,7 +48,7 @@
   ~AudioDecoder() override;
 
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/android/shared/audio_decoder_passthrough.h b/starboard/android/shared/audio_decoder_passthrough.h
index 59b2473..4b5ca80 100644
--- a/starboard/android/shared/audio_decoder_passthrough.h
+++ b/starboard/android/shared/audio_decoder_passthrough.h
@@ -51,10 +51,10 @@
     output_cb_ = output_cb;
   }
 
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override {
     SB_DCHECK(thread_checker_.CalledOnValidThread());
-    SB_DCHECK(input_buffer);
+    SB_DCHECK(!input_buffers.empty());
     SB_DCHECK(consumed_cb);
     SB_DCHECK(output_cb_);
 
@@ -66,15 +66,18 @@
     //       We should revisit this once |DecodedAudio| is used by passthrough
     //       mode on more platforms.
     const int kChannels = 1;
-    scoped_refptr<DecodedAudio> decoded_audio =
-        new DecodedAudio(kChannels, kSbMediaAudioSampleTypeInt16Deprecated,
-                         kSbMediaAudioFrameStorageTypePlanar,
-                         input_buffer->timestamp(), input_buffer->size());
-    memcpy(decoded_audio->buffer(), input_buffer->data(), input_buffer->size());
-    decoded_audios_.push(decoded_audio);
+    for (const auto& input_buffer : input_buffers) {
+      scoped_refptr<DecodedAudio> decoded_audio =
+          new DecodedAudio(kChannels, kSbMediaAudioSampleTypeInt16Deprecated,
+                           kSbMediaAudioFrameStorageTypePlanar,
+                           input_buffer->timestamp(), input_buffer->size());
+      memcpy(decoded_audio->buffer(), input_buffer->data(),
+             input_buffer->size());
+      decoded_audios_.push(decoded_audio);
+      output_cb_();
+    }
 
     consumed_cb();
-    output_cb_();
   }
 
   void WriteEndOfStream() override {
diff --git a/starboard/android/shared/audio_renderer_passthrough.cc b/starboard/android/shared/audio_renderer_passthrough.cc
index b65ca06..34f5bed 100644
--- a/starboard/android/shared/audio_renderer_passthrough.cc
+++ b/starboard/android/shared/audio_renderer_passthrough.cc
@@ -123,10 +123,9 @@
       std::bind(&AudioRendererPassthrough::OnDecoderOutput, this), error_cb);
 }
 
-void AudioRendererPassthrough::WriteSample(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void AudioRendererPassthrough::WriteSamples(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
   SB_DCHECK(can_accept_more_data_.load());
 
   if (!audio_track_thread_) {
@@ -138,14 +137,14 @@
 
   if (frames_per_input_buffer_ == 0) {
     frames_per_input_buffer_ = ParseAc3SyncframeAudioSampleCount(
-        input_buffer->data(), input_buffer->size());
+        input_buffers.front()->data(), input_buffers.front()->size());
     SB_LOG(INFO) << "Got frames per input buffer " << frames_per_input_buffer_;
   }
 
   can_accept_more_data_.store(false);
 
   decoder_->Decode(
-      input_buffer,
+      input_buffers,
       std::bind(&AudioRendererPassthrough::OnDecoderConsumed, this));
 }
 
@@ -535,14 +534,18 @@
                  "has likely changed. Restarting playback.";
           error_cb_(kSbPlayerErrorCapabilityChanged,
                     "Audio device capability changed");
-          audio_track_bridge_->PauseAndFlush();
-          return;
+        } else {
+          // `kSbPlayerErrorDecode` is used for general SbPlayer error, there is
+          // no error code corresponding to audio sink.
+          error_cb_(
+              kSbPlayerErrorDecode,
+              FormatString("Error while writing frames: %d", samples_written));
+          SB_LOG(INFO) << "Encountered kSbPlayerErrorDecode while writing "
+                          "frames, error: "
+                       << samples_written;
         }
-        // `kSbPlayerErrorDecode` is used for general SbPlayer error, there is
-        // no error code corresponding to audio sink.
-        error_cb_(
-            kSbPlayerErrorDecode,
-            FormatString("Error while writing frames: %d", samples_written));
+        audio_track_bridge_->PauseAndFlush();
+        return;
       }
       decoded_audio_writing_offset_ += samples_written;
 
diff --git a/starboard/android/shared/audio_renderer_passthrough.h b/starboard/android/shared/audio_renderer_passthrough.h
index d1d2d05..9e421e0 100644
--- a/starboard/android/shared/audio_renderer_passthrough.h
+++ b/starboard/android/shared/audio_renderer_passthrough.h
@@ -60,7 +60,7 @@
   void Initialize(const ErrorCB& error_cb,
                   const PrerolledCB& prerolled_cb,
                   const EndedCB& ended_cb) override;
-  void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteSamples(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
 
   void SetVolume(double volume) override;
diff --git a/starboard/android/shared/audio_sink_min_required_frames_tester.cc b/starboard/android/shared/audio_sink_min_required_frames_tester.cc
index 92ca9e4..b8139a2 100644
--- a/starboard/android/shared/audio_sink_min_required_frames_tester.cc
+++ b/starboard/android/shared/audio_sink_min_required_frames_tester.cc
@@ -132,7 +132,9 @@
     }
 
     // Get start threshold before release the audio sink.
-    int start_threshold = audio_sink_->GetStartThresholdInFrames();
+    int start_threshold = audio_sink_->IsAudioTrackValid()
+                              ? audio_sink_->GetStartThresholdInFrames()
+                              : 0;
 
     // |min_required_frames_| is shared between two threads. Release audio sink
     // to end audio sink thread before access |min_required_frames_| on this
diff --git a/starboard/android/shared/cobalt/configuration.py b/starboard/android/shared/cobalt/configuration.py
index b41afbc..f740bd6 100644
--- a/starboard/android/shared/cobalt/configuration.py
+++ b/starboard/android/shared/cobalt/configuration.py
@@ -64,10 +64,8 @@
             'color_emojis_should_render_properly'),
 
           # Android 12 changed the look of emojis.
-          # TODO(b/258830349) split this test into emoji & non-emoji parts so
-          # that the emoji portion can be disabled specifically.
           ('CSS3FontsLayoutTests/Layout.Test/'
-            '5_2_use_system_fallback_if_no_matching_family_is_found'),
+            '5_2_use_system_fallback_if_no_matching_family_is_found_emoji'),
       ],
       'crypto_unittests': ['P224.*'],
       'renderer_test': [
diff --git a/starboard/android/shared/configuration.cc b/starboard/android/shared/configuration.cc
index c0f5b39..5bd6f55 100644
--- a/starboard/android/shared/configuration.cc
+++ b/starboard/android/shared/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/android/shared/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 namespace starboard {
 namespace android {
diff --git a/starboard/android/shared/crash_handler.cc b/starboard/android/shared/crash_handler.cc
new file mode 100644
index 0000000..29c4f04
--- /dev/null
+++ b/starboard/android/shared/crash_handler.cc
@@ -0,0 +1,53 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/extension/crash_handler.h"
+#include "starboard/android/shared/crash_handler.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+using starboard::android::shared::JniEnvExt;
+
+bool OverrideCrashpadAnnotations(CrashpadAnnotations* crashpad_annotations) {
+  return false;  // Deprecated
+}
+
+bool SetString(const char* key, const char* value) {
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_key(env->NewStringStandardUTFOrAbort(key));
+  ScopedLocalJavaRef<jstring> j_value(env->NewStringStandardUTFOrAbort(value));
+  env->CallStarboardVoidMethodOrAbort("setCrashContext",
+                                      "(Ljava/lang/String;Ljava/lang/String;)V",
+                                      j_key.Get(), j_value.Get());
+  return true;
+}
+
+const CobaltExtensionCrashHandlerApi kCrashHandlerApi = {
+    kCobaltExtensionCrashHandlerName,
+    2,
+    &OverrideCrashpadAnnotations,
+    &SetString,
+};
+
+const void* GetCrashHandlerApi() {
+  return &kCrashHandlerApi;
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/starboard/android/shared/crash_handler.h b/starboard/android/shared/crash_handler.h
new file mode 100644
index 0000000..fda4859
--- /dev/null
+++ b/starboard/android/shared/crash_handler.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_CRASH_HANDLER_H_
+#define STARBOARD_ANDROID_SHARED_CRASH_HANDLER_H_
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+const void* GetCrashHandlerApi();
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_CRASH_HANDLER_H_
diff --git a/starboard/android/shared/graphics.cc b/starboard/android/shared/graphics.cc
index 5ceed59..30301c5 100644
--- a/starboard/android/shared/graphics.cc
+++ b/starboard/android/shared/graphics.cc
@@ -16,8 +16,9 @@
 
 #include "starboard/common/log.h"
 
-#include "cobalt/extension/graphics.h"
 #include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/extension/graphics.h"
 
 namespace starboard {
 namespace android {
@@ -40,12 +41,48 @@
   return supports_spherical_videos;
 }
 
+bool DefaultShouldClearFrameOnShutdown(float* clear_color_red,
+                                       float* clear_color_green,
+                                       float* clear_color_blue,
+                                       float* clear_color_alpha) {
+  *clear_color_red = 0.0f;
+  *clear_color_green = 0.0f;
+  *clear_color_blue = 0.0f;
+  *clear_color_alpha = 1.0f;
+  return true;
+}
+
+bool DefaultGetMapToMeshColorAdjustments(
+    CobaltExtensionGraphicsMapToMeshColorAdjustment* color_adjustment) {
+  return false;
+}
+
+bool DefaultGetRenderRootTransform(float* m00,
+                                   float* m01,
+                                   float* m02,
+                                   float* m10,
+                                   float* m11,
+                                   float* m12,
+                                   float* m20,
+                                   float* m21,
+                                   float* m22) {
+  return false;
+}
+
+void ReportFullyDrawn() {
+  JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("reportFullyDrawn", "()V");
+}
+
 const CobaltExtensionGraphicsApi kGraphicsApi = {
     kCobaltExtensionGraphicsName,
-    3,
+    6,
     &GetMaximumFrameIntervalInMilliseconds,
     &GetMinimumFrameIntervalInMilliseconds,
     &IsMapToMeshEnabled,
+    &DefaultShouldClearFrameOnShutdown,
+    &DefaultGetMapToMeshColorAdjustments,
+    &DefaultGetRenderRootTransform,
+    &ReportFullyDrawn,
 };
 
 }  // namespace
diff --git a/starboard/android/shared/input_events_generator.cc b/starboard/android/shared/input_events_generator.cc
index 17805e9..81f153e 100644
--- a/starboard/android/shared/input_events_generator.cc
+++ b/starboard/android/shared/input_events_generator.cc
@@ -100,6 +100,10 @@
       input_device, "getMotionRange",
       "(I)Landroid/view/InputDevice$MotionRange;", axis));
 
+  if (motion_range.Get() == NULL) {
+    return 0.0f;
+  }
+
   float flat =
       env->CallFloatMethodOrAbort(motion_range.Get(), "getFlat", "()F");
 
diff --git a/starboard/android/shared/input_events_generator.h b/starboard/android/shared/input_events_generator.h
index e39a72f..ec2ff9c 100644
--- a/starboard/android/shared/input_events_generator.h
+++ b/starboard/android/shared/input_events_generator.h
@@ -19,7 +19,7 @@
 #include <memory>
 #include <vector>
 
-#include "third_party/android_game_activity/include/game-activity/GameActivity.h"
+#include "game-activity/GameActivity.h"
 
 #include "starboard/input.h"
 #include "starboard/shared/starboard/application.h"
diff --git a/starboard/android/shared/media_decoder.cc b/starboard/android/shared/media_decoder.cc
index 66bd437..90beda3 100644
--- a/starboard/android/shared/media_decoder.cc
+++ b/starboard/android/shared/media_decoder.cc
@@ -178,11 +178,9 @@
   }
 }
 
-void MediaDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void MediaDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(thread_checker_.CalledOnValidThread());
-  SB_DCHECK(input_buffer);
-
+  SB_DCHECK(!input_buffers.empty());
   if (stream_ended_.load()) {
     SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
     return;
@@ -199,9 +197,12 @@
   }
 
   ScopedLock scoped_lock(mutex_);
-  pending_tasks_.push_back(Event(input_buffer));
-  number_of_pending_tasks_.increment();
-  if (pending_tasks_.size() == 1) {
+  bool need_signal = pending_tasks_.empty();
+  for (const auto& input_buffer : input_buffers) {
+    pending_tasks_.push_back(Event(input_buffer));
+    number_of_pending_tasks_.increment();
+  }
+  if (need_signal) {
     condition_variable_.Signal();
   }
 }
diff --git a/starboard/android/shared/media_decoder.h b/starboard/android/shared/media_decoder.h
index dbe73fc..913aff6 100644
--- a/starboard/android/shared/media_decoder.h
+++ b/starboard/android/shared/media_decoder.h
@@ -47,6 +47,7 @@
  public:
   typedef ::starboard::shared::starboard::player::filter::ErrorCB ErrorCB;
   typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+  typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
   typedef std::function<void(SbTime)> FrameRenderedCB;
 
   // This class should be implemented by the users of MediaDecoder to receive
@@ -94,7 +95,7 @@
   ~MediaDecoder();
 
   void Initialize(const ErrorCB& error_cb);
-  void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+  void WriteInputBuffers(const InputBuffers& input_buffers);
   void WriteEndOfStream();
 
   void SetPlaybackRate(double playback_rate);
diff --git a/starboard/android/shared/platform_service.cc b/starboard/android/shared/platform_service.cc
index 0670bc8..56608c6 100644
--- a/starboard/android/shared/platform_service.cc
+++ b/starboard/android/shared/platform_service.cc
@@ -14,11 +14,13 @@
 
 #include "starboard/android/shared/platform_service.h"
 
-#include "cobalt/extension/platform_service.h"
+#include <memory>
+
 #include "starboard/android/shared/jni_env_ext.h"
 #include "starboard/android/shared/jni_utils.h"
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/platform_service.h"
 
 typedef struct CobaltExtensionPlatformServicePrivate {
   void* context;
@@ -30,6 +32,10 @@
     if (name) {
       delete name;
     }
+    if (cobalt_service) {
+      starboard::android::shared::JniEnvExt::Get()->DeleteGlobalRef(
+          cobalt_service);
+    }
   }
 } CobaltExtensionPlatformServicePrivate;
 
@@ -39,8 +45,8 @@
 
 namespace {
 
-using starboard::android::shared::ScopedLocalJavaRef;
 using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
 
 bool Has(const char* name) {
   JniEnvExt* env = JniEnvExt::Get();
@@ -72,7 +78,7 @@
     delete static_cast<CobaltExtensionPlatformServicePrivate*>(service);
     return kCobaltExtensionPlatformServiceInvalid;
   }
-  service->cobalt_service = cobalt_service;
+  service->cobalt_service = env->ConvertLocalRefToGlobalRef(cobalt_service);
   return service;
 }
 
@@ -99,20 +105,20 @@
   ScopedLocalJavaRef<jbyteArray> data_byte_array;
   data_byte_array.Reset(
       env->NewByteArrayFromRaw(reinterpret_cast<const jbyte*>(data), length));
-  jobject j_response_from_client =
+  ScopedLocalJavaRef<jobject> j_response_from_client(
       static_cast<jbyteArray>(env->CallObjectMethodOrAbort(
           service->cobalt_service, "receiveFromClient",
           "([B)Ldev/cobalt/coat/CobaltService$ResponseToClient;",
-          data_byte_array.Get()));
+          data_byte_array.Get())));
   if (!j_response_from_client) {
     *invalid_state = true;
     *output_length = 0;
     return 0;
   }
-  *invalid_state =
-      env->GetBooleanFieldOrAbort(j_response_from_client, "invalidState", "Z");
+  *invalid_state = env->GetBooleanFieldOrAbort(j_response_from_client.Get(),
+                                               "invalidState", "Z");
   ScopedLocalJavaRef<jbyteArray> j_out_data_array(static_cast<jbyteArray>(
-      env->GetObjectFieldOrAbort(j_response_from_client, "data", "[B")));
+      env->GetObjectFieldOrAbort(j_response_from_client.Get(), "data", "[B")));
   *output_length = env->GetArrayLength(j_out_data_array.Get());
   char* output = new char[*output_length];
   env->GetByteArrayRegion(j_out_data_array.Get(), 0, *output_length,
@@ -144,10 +150,11 @@
   }
 
   jsize length = env->GetArrayLength(j_data);
-  char* data = new char[length];
-  env->GetByteArrayRegion(j_data, 0, length, reinterpret_cast<jbyte*>(data));
+  std::unique_ptr<char[]> data(new char[length]);
+  env->GetByteArrayRegion(j_data, 0, length,
+                          reinterpret_cast<jbyte*>(data.get()));
 
-  service->receive_callback(service->context, data, length);
+  service->receive_callback(service->context, data.get(), length);
 }
 
 const void* GetPlatformServiceApi() {
diff --git a/starboard/android/shared/system_get_extensions.cc b/starboard/android/shared/system_get_extensions.cc
index 9d817f1..531f932 100644
--- a/starboard/android/shared/system_get_extensions.cc
+++ b/starboard/android/shared/system_get_extensions.cc
@@ -14,18 +14,34 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/graphics.h"
-#include "cobalt/extension/media_session.h"
-#include "cobalt/extension/platform_service.h"
 #include "starboard/android/shared/android_media_session_client.h"
 #include "starboard/android/shared/configuration.h"
+#include "starboard/android/shared/crash_handler.h"
 #include "starboard/android/shared/graphics.h"
 #include "starboard/android/shared/platform_service.h"
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
+#if SB_IS(EVERGREEN_COMPATIBLE)
+#include "starboard/elf_loader/evergreen_config.h"  // nogncheck
+#endif
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/crash_handler.h"
+#include "starboard/extension/graphics.h"
+#include "starboard/extension/media_session.h"
+#include "starboard/extension/platform_service.h"
 
 const void* SbSystemGetExtension(const char* name) {
+#if SB_IS(EVERGREEN_COMPATIBLE)
+  const starboard::elf_loader::EvergreenConfig* evergreen_config =
+      starboard::elf_loader::EvergreenConfig::GetInstance();
+  if (evergreen_config != NULL &&
+      evergreen_config->custom_get_extension_ != NULL) {
+    const void* ext = evergreen_config->custom_get_extension_(name);
+    if (ext != NULL) {
+      return ext;
+    }
+  }
+#endif
   if (strcmp(name, kCobaltExtensionPlatformServiceName) == 0) {
     return starboard::android::shared::GetPlatformServiceApi();
   }
@@ -38,5 +54,8 @@
   if (strcmp(name, kCobaltExtensionGraphicsName) == 0) {
     return starboard::android::shared::GetGraphicsApi();
   }
+  if (strcmp(name, kCobaltExtensionCrashHandlerName) == 0) {
+    return starboard::android::shared::GetCrashHandlerApi();
+  }
   return NULL;
 }
diff --git a/starboard/android/shared/test_filters.py b/starboard/android/shared/test_filters.py
index 8d5d0c0..fca2878 100644
--- a/starboard/android/shared/test_filters.py
+++ b/starboard/android/shared/test_filters.py
@@ -19,6 +19,10 @@
 # pylint: disable=line-too-long
 _FILTERED_TESTS = {
     'player_filter_tests': [
+        # Invalid input may lead to unexpected behaviors.
+        'AudioDecoderTests/AudioDecoderTest.MultipleInvalidInput/*',
+        'AudioDecoderTests/AudioDecoderTest.MultipleValidInputsAfterInvalidInput*',
+
         # GetMaxNumberOfCachedFrames() on Android is device dependent,
         # and Android doesn't provide an API to get it. So, this function
         # doesn't make sense on Android. But HoldFramesUntilFull tests depend
@@ -39,7 +43,8 @@
         'PlayerComponentsTests/PlayerComponentsTest.EOSWithoutInput/*',
 
         # The e/eac3 audio time reporting during pause will be revisitied.
-        'PlayerComponentsTests/PlayerComponentsTest.Pause/15',
+        'PlayerComponentsTests/PlayerComponentsTest.Pause/*ac3*',
+        'PlayerComponentsTests/PlayerComponentsTest.Pause/*ec3*',
     ],
     'nplb': [
         # This test is failing because localhost is not defined for IPv6 in
diff --git a/starboard/android/shared/video_decoder.cc b/starboard/android/shared/video_decoder.cc
index 5078989..e84cf2e 100644
--- a/starboard/android/shared/video_decoder.cc
+++ b/starboard/android/shared/video_decoder.cc
@@ -337,20 +337,20 @@
   return kInitialPrerollTimeout;
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
-  SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeVideo);
+  SB_DCHECK(!input_buffers.empty());
+  SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo);
   SB_DCHECK(decoder_status_cb_);
 
   if (input_buffer_written_ == 0) {
     SB_DCHECK(video_fps_ == 0);
-    first_buffer_timestamp_ = input_buffer->timestamp();
+    first_buffer_timestamp_ = input_buffers.front()->timestamp();
 
     // If color metadata is present and is not an identity mapping, then
     // teardown the codec so it can be reinitalized with the new metadata.
-    auto& color_metadata = input_buffer->video_sample_info().color_metadata;
+    const auto& color_metadata =
+        input_buffers.front()->video_sample_info().color_metadata;
     if (!IsIdentity(color_metadata)) {
       SB_DCHECK(!color_metadata_) << "Unexpected residual color metadata.";
       SB_LOG(INFO) << "Reinitializing codec with HDR color metadata.";
@@ -378,14 +378,15 @@
     }
   }
 
-  ++input_buffer_written_;
+  input_buffer_written_ += input_buffers.size();
 
   if (video_codec_ == kSbMediaVideoCodecAv1 && video_fps_ == 0) {
     SB_DCHECK(!media_decoder_);
 
+    pending_input_buffers_.insert(pending_input_buffers_.end(),
+                                  input_buffers.begin(), input_buffers.end());
     if (pending_input_buffers_.size() <
         kFpsGuesstimateRequiredInputBufferCount) {
-      pending_input_buffers_.push_back(input_buffer);
       decoder_status_cb_(kNeedMoreInput, NULL);
       return;
     }
@@ -398,9 +399,10 @@
       ReportError(kSbPlayerErrorDecode, error_message);
       return;
     }
+    return;
   }
 
-  WriteInputBufferInternal(input_buffer);
+  WriteInputBuffersInternal(input_buffers);
 }
 
 void VideoDecoder::WriteEndOfStream() {
@@ -581,9 +583,9 @@
     } else {
       SB_DCHECK(pending_input_buffers_.empty());
     }
-    while (!pending_input_buffers_.empty()) {
-      WriteInputBufferInternal(pending_input_buffers_[0]);
-      pending_input_buffers_.pop_front();
+    if (!pending_input_buffers_.empty()) {
+      WriteInputBuffersInternal(pending_input_buffers_);
+      pending_input_buffers_.clear();
     }
     return true;
   }
@@ -646,8 +648,10 @@
   sink_->Render();
 }
 
-void VideoDecoder::WriteInputBufferInternal(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffersInternal(
+    const InputBuffers& input_buffers) {
+  SB_DCHECK(!input_buffers.empty());
+
   // There's a race condition when suspending the app. If surface view is
   // destroyed before video decoder stopped, |media_decoder_| could be null
   // here. And error_cb_() could be handled asynchronously. It's possible
@@ -657,7 +661,7 @@
     SB_LOG(INFO) << "Trying to write input buffer when media_decoder_ is null.";
     return;
   }
-  media_decoder_->WriteInputBuffer(input_buffer);
+  media_decoder_->WriteInputBuffers(input_buffers);
   if (media_decoder_->GetNumberOfPendingTasks() < kMaxPendingWorkSize) {
     decoder_status_cb_(kNeedMoreInput, NULL);
   } else if (tunnel_mode_audio_session_id_ != -1) {
@@ -669,7 +673,11 @@
   }
 
   if (tunnel_mode_audio_session_id_ != -1) {
-    video_frame_tracker_->OnInputBuffer(input_buffer->timestamp());
+    SbTime max_timestamp = input_buffers[0]->timestamp();
+    for (const auto& input_buffer : input_buffers) {
+      video_frame_tracker_->OnInputBuffer(input_buffer->timestamp());
+      max_timestamp = std::max(max_timestamp, input_buffer->timestamp());
+    }
 
     if (tunnel_mode_prerolling_.load()) {
       // TODO: Refine preroll logic in tunnel mode.
@@ -677,17 +685,17 @@
       if (first_buffer_timestamp_ == 0) {
         // Initial playback.
         enough_buffers_written_to_media_codec =
-            (input_buffer_written_ - pending_input_buffers_.size() -
+            (input_buffer_written_ -
              media_decoder_->GetNumberOfPendingTasks()) >
             kInitialPrerollFrameCount;
       } else {
         // Seeking.  Note that this branch can be eliminated once seeking in
         // tunnel mode is always aligned to the next video key frame.
         enough_buffers_written_to_media_codec =
-            (input_buffer_written_ - pending_input_buffers_.size() -
+            (input_buffer_written_ -
              media_decoder_->GetNumberOfPendingTasks()) >
                 kSeekingPrerollPendingWorkSizeInTunnelMode &&
-            input_buffer->timestamp() >= video_frame_tracker_->seek_to_time();
+            max_timestamp >= video_frame_tracker_->seek_to_time();
       }
 
       bool cache_full =
@@ -698,7 +706,7 @@
       if (prerolled && tunnel_mode_prerolling_.exchange(false)) {
         SB_LOG(INFO)
             << "Tunnel mode preroll finished on enqueuing input buffer "
-            << input_buffer->timestamp() << ", for seek time "
+            << max_timestamp << ", for seek time "
             << video_frame_tracker_->seek_to_time();
         decoder_status_cb_(
             kNeedMoreInput,
diff --git a/starboard/android/shared/video_decoder.h b/starboard/android/shared/video_decoder.h
index 7859aa2..19a8b82 100644
--- a/starboard/android/shared/video_decoder.h
+++ b/starboard/android/shared/video_decoder.h
@@ -16,8 +16,8 @@
 #define STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
 
 #include <atomic>
-#include <deque>
 #include <string>
+#include <vector>
 
 #include "starboard/android/shared/drm_system.h"
 #include "starboard/android/shared/media_codec_bridge.h"
@@ -87,8 +87,7 @@
   // buffer.
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
   SbDecodeTarget GetCurrentDecodeTarget() override;
@@ -105,7 +104,7 @@
   bool InitializeCodec(std::string* error_message);
   void TeardownCodec();
 
-  void WriteInputBufferInternal(const scoped_refptr<InputBuffer>& input_buffer);
+  void WriteInputBuffersInternal(const InputBuffers& input_buffers);
   void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
                            const DequeueOutputResult& output) override;
   void OnEndOfStreamWritten(MediaCodecBridge* media_codec_bridge);
@@ -196,7 +195,7 @@
   Mutex surface_destroy_mutex_;
   ConditionVariable surface_condition_variable_;
 
-  std::deque<const scoped_refptr<InputBuffer>> pending_input_buffers_;
+  std::vector<scoped_refptr<InputBuffer>> pending_input_buffers_;
   int video_fps_ = 0;
 };
 
diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn
index 774be60..0d3b11c 100644
--- a/starboard/build/config/BUILDCONFIG.gn
+++ b/starboard/build/config/BUILDCONFIG.gn
@@ -105,6 +105,7 @@
 # sub-config of an existing one, most commonly the main "compiler" one.
 
 default_compiler_configs = [
+  "//build/config/coverage:default_coverage",
   "//build/config/compiler:default_include_dirs",
   "//build/config/compiler:no_exceptions",
   "//starboard/build/config:base",
diff --git a/starboard/build/config/base_configuration.gni b/starboard/build/config/base_configuration.gni
index 25d3ba3..eb7f8d8 100644
--- a/starboard/build/config/base_configuration.gni
+++ b/starboard/build/config/base_configuration.gni
@@ -51,6 +51,10 @@
   # Whether to adopt Evergreen Lite on the Evergreen compatible platform.
   sb_evergreen_compatible_enable_lite = false
 
+  # Whether to generate the whole package containing both Loader app and Cobalt
+  # core on the Evergreen compatible platform.
+  sb_evergreen_compatible_package = false
+
   # The target type for test targets. Allows changing the target type
   # on platforms where the native code may require an additional packaging step
   # (ex. Android).
@@ -135,12 +139,6 @@
   # Set to true to enable H5vccAccountManager.
   enable_account_manager = false
 
-  # Set to true to enable H5vccSSO (Single Sign On).
-  enable_sso = false
-
-  # Set to true to enable filtering of HTTP headers before sending.
-  enable_xhr_header_filtering = false
-
   # TODO(b/173248397): Migrate to CobaltExtensions or PlatformServices.
   # List of platform-specific targets that get compiled into cobalt.
   cobalt_platform_dependencies = []
diff --git a/starboard/build/config/bundle_content.gni b/starboard/build/config/bundle_content.gni
index 2008c95..f7f2a5d 100644
--- a/starboard/build/config/bundle_content.gni
+++ b/starboard/build/config/bundle_content.gni
@@ -16,7 +16,7 @@
   bundle_name = invoker.bundle_name
   bundle_deps = invoker.bundle_deps
 
-  bundle_content_list_file = "$root_gen_dir/${target_name}_files.txt"
+  bundle_content_rsp_file = "$target_gen_dir/${target_name}_list.rsp"
   file_format = "list lines"
 
   # If the platform bundles content the list of content files can be collected
@@ -37,7 +37,7 @@
     }
 
     output_conversion = file_format
-    outputs = [ bundle_content_list_file ]
+    outputs = [ bundle_content_rsp_file ]
   }
 
   action(target_name) {
@@ -45,16 +45,14 @@
 
     deps = [ ":list_$target_name" ]
 
-    inputs = [ bundle_content_list_file ]
+    sources = [ bundle_content_rsp_file ]
 
     bundle_content_dir =
         "$sb_install_output_dir/$bundle_name/$sb_install_content_subdir"
 
-    # TODO(b/220024845): We don't have the list of output files. The files
-    # are listed in `bundle_content_list_file` but can't be accessed in GN
-    # as `read_file` can't reliably access files generated by
-    # `generated_file` targets.
-    outputs = [ bundle_content_dir ]
+    outputs = [ "$target_gen_dir/${target_name}.stamp" ]
+
+    depfile = "$target_out_dir/$target_name.d"
 
     script = "//starboard/build/copy_install_content.py"
     args = [
@@ -63,7 +61,11 @@
       "--base_dir",
       rebase_path(sb_static_contents_output_data_dir, root_build_dir),
       "--files_list",
-      rebase_path(bundle_content_list_file, root_build_dir),
+      rebase_path(bundle_content_rsp_file, root_build_dir),
+      "--output",
+      rebase_path(outputs[0], root_build_dir),
+      "--depfile",
+      rebase_path(depfile, root_build_dir),
     ]
   }
 }
diff --git a/starboard/build/copy_install_content.py b/starboard/build/copy_install_content.py
index 00fa015..b5dcdb0 100644
--- a/starboard/build/copy_install_content.py
+++ b/starboard/build/copy_install_content.py
@@ -19,17 +19,31 @@
 
 The folder structure of the input files is maintained in the output relative
 to the 'base_dir' parameter.
+
+If the parameters `output` and `depfile` are supplied the list of copied files
+will be written to `depfile` and an `output` dummy file will be created.
 """
 
 import argparse
 import os
+import pathlib
 import shutil
+import sys
 
 
 class InvalidArgumentException(Exception):
   pass
 
 
+def validate_args(options):
+  if not os.path.exists(options.files_list):
+    raise InvalidArgumentException(f'{options.files_list} doesn\'t exist')
+
+  # If either `depfile` and `output` is present the other one must also be.
+  if bool(options.depfile) != bool(options.output):
+    raise InvalidArgumentException('output and depfile must both be supplied')
+
+
 def copy_files(files_to_copy, base_dir, output_dir):
   if not os.path.exists(output_dir):
     os.makedirs(output_dir)
@@ -67,7 +81,14 @@
       shutil.copytree(filename, output_filename)
 
 
-if __name__ == '__main__':
+def write_outputs(output, depfile, files):
+  with open(depfile, 'w') as f:
+    f.write('{}: \\\n  {}\n'.format(output, ' \\\n  '.join(sorted(files))))
+  # Touch the output file to tell ninja that the script ran successfully.
+  pathlib.Path(output).touch()
+
+
+def main():
   parser = argparse.ArgumentParser()
   parser.add_argument(
       '--output_dir', dest='output_dir', required=True, help='output directory')
@@ -81,11 +102,26 @@
       dest='files_list',
       required=True,
       help='path to file containing list of input files')
+  parser.add_argument('--output', dest='output', help='dummy output file')
+  parser.add_argument(
+      '--depfile',
+      dest='depfile',
+      help='depfile to write the list of touched files to')
   options = parser.parse_args()
 
+  validate_args(options)
+
   # Load file names from the file containing the list of file names.
   # The file name list must be passed in a file to due to command line limits.
   with open(options.files_list) as input_file:
     file_names = [line.strip() for line in input_file]
 
   copy_files(file_names, options.base_dir, options.output_dir)
+
+  if options.output and options.depfile:
+    # If depfile and output are present write the list of files to the depfile.
+    write_outputs(options.output, options.depfile, file_names)
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/starboard/client_porting/cwrappers/pow_wrapper.cc b/starboard/client_porting/cwrappers/pow_wrapper.cc
index a5724f6..58544c4 100644
--- a/starboard/client_porting/cwrappers/pow_wrapper.cc
+++ b/starboard/client_porting/cwrappers/pow_wrapper.cc
@@ -14,11 +14,13 @@
 
 #include <cmath>
 
-#include "cobalt/extension/cwrappers.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/cwrappers.h"
 #include "starboard/once.h"
 #include "starboard/system.h"
 
+#include "starboard/client_porting/cwrappers/pow_wrapper.h"
+
 static double (*g_pow_wrapper)(double, double) = &pow;
 static SbOnceControl g_pow_wrapper_once = SB_ONCE_INITIALIZER;
 
@@ -38,5 +40,4 @@
   SbOnce(&g_pow_wrapper_once, InitPowWrapper);
   return g_pow_wrapper(base, exponent);
 }
-
 }
diff --git a/starboard/contrib/linux/stadia/system_get_extensions.cc b/starboard/contrib/linux/stadia/system_get_extensions.cc
index f5db101..42a78c7 100644
--- a/starboard/contrib/linux/stadia/system_get_extensions.cc
+++ b/starboard/contrib/linux/stadia/system_get_extensions.cc
@@ -11,10 +11,10 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/crash_handler.h"
 #include "starboard/common/string.h"
 #include "starboard/contrib/stadia/get_platform_service_api.h"
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/crash_handler.h"
 #include "starboard/shared/starboard/crash_handler.h"
 #include "starboard/system.h"
 #if SB_IS(EVERGREEN_COMPATIBLE)
diff --git a/starboard/contrib/stadia/get_platform_service_api.cc b/starboard/contrib/stadia/get_platform_service_api.cc
index cb73154..cb532bd 100644
--- a/starboard/contrib/stadia/get_platform_service_api.cc
+++ b/starboard/contrib/stadia/get_platform_service_api.cc
@@ -19,12 +19,12 @@
 #include <memory>
 #include <vector>
 
-#include "cobalt/extension/platform_service.h"
 #include "starboard/common/log.h"
 #include "starboard/common/mutex.h"
 #include "starboard/common/string.h"
 #include "starboard/contrib/stadia/stadia_interface.h"
 #include "starboard/event.h"
+#include "starboard/extension/platform_service.h"
 #include "starboard/memory.h"
 #include "starboard/window.h"
 
@@ -67,16 +67,13 @@
   auto std_callback = std::make_unique<
       std::function<void(const std::vector<uint8_t>& message)>>(
       [receive_callback, context](const std::vector<uint8_t>& message) -> void {
-
         receive_callback(context, message.data(), message.size());
-
       });
 
   StadiaPlugin* plugin = g_stadia_interface->StadiaPluginOpen(
       name_c_str,
 
       [](const uint8_t* const message, size_t length, void* user_data) -> void {
-
         auto callback = static_cast<
             const std::function<void(const std::vector<uint8_t>& message)>*>(
             user_data);
@@ -132,7 +129,10 @@
 const CobaltExtensionPlatformServiceApi kPlatformServiceApi = {
     kCobaltExtensionPlatformServiceName,
     // API version that's implemented.
-    1, &HasPlatformService, &OpenPlatformService, &ClosePlatformService,
+    1,
+    &HasPlatformService,
+    &OpenPlatformService,
+    &ClosePlatformService,
     &SendToPlatformService,
 };
 }  // namespace
diff --git a/starboard/doc/crash_handlers.md b/starboard/doc/crash_handlers.md
index 0863bf1..c11516b 100644
--- a/starboard/doc/crash_handlers.md
+++ b/starboard/doc/crash_handlers.md
@@ -16,7 +16,7 @@
 ```
 #include "starboard/system.h"
 
-#include "cobalt/extension/crash_handler.h"
+#include "starboard/extension/crash_handler.h"
 #include "starboard/shared/starboard/crash_handler.h"
 
 ...
diff --git a/starboard/elf_loader/exported_symbols.h b/starboard/elf_loader/exported_symbols.h
index d385e1d..d2ee152 100644
--- a/starboard/elf_loader/exported_symbols.h
+++ b/starboard/elf_loader/exported_symbols.h
@@ -18,10 +18,6 @@
 #include <map>
 #include <string>
 
-#include "starboard/elf_loader/elf_hash_table.h"
-#include "starboard/elf_loader/gnu_hash_table.h"
-#include "starboard/file.h"
-
 namespace starboard {
 namespace elf_loader {
 
diff --git a/starboard/elf_loader/program_table.h b/starboard/elf_loader/program_table.h
index 08f4231..8a96c0c 100644
--- a/starboard/elf_loader/program_table.h
+++ b/starboard/elf_loader/program_table.h
@@ -17,9 +17,9 @@
 
 #include <vector>
 
-#include "cobalt/extension/memory_mapped_file.h"
 #include "starboard/elf_loader/elf.h"
 #include "starboard/elf_loader/file.h"
+#include "starboard/extension/memory_mapped_file.h"
 
 namespace starboard {
 namespace elf_loader {
diff --git a/starboard/evergreen/shared/launcher.py b/starboard/evergreen/shared/launcher.py
index e6f331d..d22f416 100644
--- a/starboard/evergreen/shared/launcher.py
+++ b/starboard/evergreen/shared/launcher.py
@@ -19,7 +19,6 @@
 
 from starboard.tools import abstract_launcher
 from starboard.tools import paths
-from starboard.tools import port_symlink
 
 _BASE_STAGING_DIRECTORY = 'evergreen_staging'
 _CRASHPAD_TARGET = 'crashpad_handler'
@@ -80,9 +79,9 @@
     # The relationship of loader platforms and configurations to evergreen
     # platforms and configurations is many-to-many. We need a separate directory
     # for each of them, i.e. linux-x64x11_debug__evergreen-x64_gold.
-    self.combined_config = '{}_{}__{}_{}'.format(self.loader_platform,
-                                                 self.loader_config, platform,
-                                                 config)
+    loader_build_name = f'{self.loader_platform}_{self.loader_config}'
+    target_build_name = f'{platform}_{config}'
+    self.combined_config = f'{loader_build_name}__{target_build_name}'
 
     self.staging_directory = os.path.join(
         os.path.dirname(self.out_directory), _BASE_STAGING_DIRECTORY,
@@ -94,8 +93,9 @@
 
     # Ensure the path, relative to the content of the ELF Loader, to the
     # Evergreen target and its content are passed as command line switches.
-    library_path_param = '--evergreen_library=app/{}/lib/lib{}'.format(
-        self.target_name, self.target_name)
+    library_path_value = os.path.join('app', self.target_name, 'lib',
+                                      f'lib{self.target_name}')
+    library_path_param = f'--evergreen_library={library_path_value}'
     if self.use_compressed_library:
       if self.target_name != 'cobalt':
         raise ValueError(
@@ -106,7 +106,7 @@
 
     target_command_line_params = [
         library_path_param,
-        '--evergreen_content=app/{}/content'.format(self.target_name)
+        f'--evergreen_content=app/{self.target_name}/content'
     ]
 
     if self.target_command_line_params:
@@ -172,16 +172,13 @@
       shutil.rmtree(self.staging_directory)
     os.makedirs(self.staging_directory)
 
-    if os.path.exists(os.path.join(self.loader_out_directory, 'install')):
-      # TODO: Make the Linux launcher run from the install_directory
-      if 'linux' in self.loader_platform:
-        self._StageTargetsAndContentsGnLinux()
-      else:
-        self._StageTargetsAndContentsGnRaspi()
+    # TODO(b/267568637): Make the Linux launcher run from the install_directory.
+    if 'linux' in self.loader_platform:
+      self._StageTargetsAndContentsLinux()
     else:
-      self._StageTargetsAndContentsGyp()
+      self._StageTargetsAndContentsRaspi()
 
-  def _StageTargetsAndContentsGnLinux(self):
+  def _StageTargetsAndContentsLinux(self):
     """Stage targets and their contents for GN builds for Linux platforms."""
     content_subdir = os.path.join('usr', 'share', 'cobalt')
 
@@ -211,14 +208,14 @@
     target_content_dst = os.path.join(target_staging_dir, 'content')
     shutil.copytree(target_content_src, target_content_dst)
 
-    shlib_name = 'lib{}'.format(self.target_name)
+    shlib_name = f'lib{self.target_name}'
     shlib_name += '.lz4' if self.use_compressed_library else '.so'
     target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
     target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
     os.makedirs(os.path.join(target_staging_dir, 'lib'))
     shutil.copy(target_binary_src, target_binary_dst)
 
-  def _StageTargetsAndContentsGnRaspi(self):
+  def _StageTargetsAndContentsRaspi(self):
     """Stage targets and their contents for GN builds for Raspi platforms."""
     # TODO(b/218889313): `content` is hardcoded on raspi and must be in the same
     # directory as the binaries.
@@ -265,50 +262,13 @@
     target_content_dst = os.path.join(target_staging_dir, 'content')
     shutil.copytree(target_content_src, target_content_dst)
 
-    shlib_name = 'lib{}'.format(self.target_name)
+    shlib_name = f'lib{self.target_name}'
     shlib_name += '.lz4' if self.use_compressed_library else '.so'
     target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
     target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
     os.makedirs(os.path.join(target_staging_dir, 'lib'))
     shutil.copy(target_binary_src, target_binary_dst)
 
-  def _StageTargetsAndContentsGyp(self):
-    """Stage targets and their contents for GYP builds."""
-    # <outpath>/deploy/elf_loader_sandbox
-    staging_directory_loader = os.path.join(self.staging_directory, 'deploy',
-                                            self.loader_target)
-
-    # <outpath>/deploy/elf_loader_sandbox/content/app/nplb/
-    staging_directory_evergreen = os.path.join(staging_directory_loader,
-                                               'content', 'app',
-                                               self.target_name)
-
-    # Make a hard copy of the ELF Loader's install_directory in the location
-    # specified by |staging_directory_loader|. A symbolic link here would cause
-    # future symbolic links to fall through to the original out-directories.
-    shutil.copytree(
-        os.path.join(self.loader_out_directory, 'deploy', self.loader_target),
-        staging_directory_loader)
-    shutil.copy(
-        os.path.join(self.loader_out_directory, 'deploy', _CRASHPAD_TARGET,
-                     _CRASHPAD_TARGET), staging_directory_loader)
-
-    port_symlink.MakeSymLink(
-        os.path.join(self.out_directory, 'deploy', self.target_name),
-        staging_directory_evergreen)
-
-    # TODO: Make the Linux launcher run from the install_directory, no longer
-    #       create these symlinks, and remove the NOTE from the docstring.
-    port_symlink.MakeSymLink(
-        os.path.join(staging_directory_loader, self.loader_target),
-        os.path.join(self.staging_directory, self.loader_target))
-    port_symlink.MakeSymLink(
-        os.path.join(staging_directory_loader, _CRASHPAD_TARGET),
-        os.path.join(self.staging_directory, _CRASHPAD_TARGET))
-    port_symlink.MakeSymLink(
-        os.path.join(staging_directory_loader, 'content'),
-        os.path.join(self.staging_directory, 'content'))
-
   def SupportsSuspendResume(self):
     return self.launcher.SupportsSuspendResume()
 
diff --git a/starboard/evergreen/testing/linux-x64x11/clean_up.sh b/starboard/evergreen/testing/linux-x64x11/clean_up.sh
new file mode 100755
index 0000000..ea93a9d
--- /dev/null
+++ b/starboard/evergreen/testing/linux-x64x11/clean_up.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function clean_up() {
+  clear_storage
+}
diff --git a/starboard/evergreen/testing/linux-x64x11/clear_storage.sh b/starboard/evergreen/testing/linux-x64x11/clear_storage.sh
new file mode 100755
index 0000000..f9a3574
--- /dev/null
+++ b/starboard/evergreen/testing/linux-x64x11/clear_storage.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function clear_storage() {
+  echo " Clearing Cobalt storage"
+  eval "find ${STORAGE_DIR}/ -mindepth 1 -maxdepth 1 ! -name 'icu' -exec rm -rf {} +" 1> /dev/null
+}
diff --git a/starboard/evergreen/testing/linux/create_file.sh b/starboard/evergreen/testing/linux-x64x11/create_file.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/create_file.sh
rename to starboard/evergreen/testing/linux-x64x11/create_file.sh
diff --git a/starboard/evergreen/testing/linux/delete_file.sh b/starboard/evergreen/testing/linux-x64x11/delete_file.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/delete_file.sh
rename to starboard/evergreen/testing/linux-x64x11/delete_file.sh
diff --git a/starboard/evergreen/testing/linux-x64x11/deploy_cobalt.sh b/starboard/evergreen/testing/linux-x64x11/deploy_cobalt.sh
new file mode 100755
index 0000000..1c3c73e
--- /dev/null
+++ b/starboard/evergreen/testing/linux-x64x11/deploy_cobalt.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function deploy_cobalt() {
+  if [[ -z "${OUT}" ]]; then
+    log "info" "Please set the environment variable 'OUT'"
+    exit 1
+  fi
+
+  staging_dir="${OUT}"
+
+  echo " Checking '${staging_dir}'"
+
+  PATHS=("${staging_dir}/loader_app"                                                \
+         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
+         "${staging_dir}/content/app/cobalt/content/")
+
+  for file in "${PATHS[@]}"; do
+    if [[ ! -e "${file}" ]]; then
+      echo " Failed to find '${file}'"
+      exit 1
+    fi
+  done
+
+  echo " Required files were found within '${staging_dir}'"
+
+  echo " Deploying Cobalt on local device"
+
+  echo " Generating HTML test directory"
+  eval "mkdir -p ${staging_dir}/content/app/cobalt/content/web/tests/" 1> /dev/null
+
+  echo " Copying HTML test files to HTML test directory"
+  eval "cp ${1}/../tests/*.html ${staging_dir}/content/app/cobalt/content/web/tests/" 1> /dev/null
+
+  clear_storage
+
+  echo " Successfully deployed!"
+}
diff --git a/starboard/evergreen/testing/linux/run_command.sh b/starboard/evergreen/testing/linux-x64x11/run_command.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/run_command.sh
rename to starboard/evergreen/testing/linux-x64x11/run_command.sh
diff --git a/starboard/evergreen/testing/linux/setup.sh b/starboard/evergreen/testing/linux-x64x11/setup.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/setup.sh
rename to starboard/evergreen/testing/linux-x64x11/setup.sh
diff --git a/starboard/evergreen/testing/linux/start_cobalt.sh b/starboard/evergreen/testing/linux-x64x11/start_cobalt.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/start_cobalt.sh
rename to starboard/evergreen/testing/linux-x64x11/start_cobalt.sh
diff --git a/starboard/evergreen/testing/linux-x64x11/stop_cobalt.sh b/starboard/evergreen/testing/linux-x64x11/stop_cobalt.sh
new file mode 100755
index 0000000..b3a4d90
--- /dev/null
+++ b/starboard/evergreen/testing/linux-x64x11/stop_cobalt.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function stop_cobalt() {
+  echo " Stopping Cobalt"
+  eval "kill -9 $(pidof "${OUT}/loader_app")" 1> /dev/null
+  sleep 1
+}
diff --git a/starboard/evergreen/testing/linux/stop_process.sh b/starboard/evergreen/testing/linux-x64x11/stop_process.sh
similarity index 100%
rename from starboard/evergreen/testing/linux/stop_process.sh
rename to starboard/evergreen/testing/linux-x64x11/stop_process.sh
diff --git a/starboard/evergreen/testing/linux/clean_up.sh b/starboard/evergreen/testing/linux/clean_up.sh
deleted file mode 100755
index d454e2f..0000000
--- a/starboard/evergreen/testing/linux/clean_up.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function clean_up() {
-  clear_storage
-}
-
diff --git a/starboard/evergreen/testing/linux/clear_storage.sh b/starboard/evergreen/testing/linux/clear_storage.sh
deleted file mode 100755
index 6f14bde..0000000
--- a/starboard/evergreen/testing/linux/clear_storage.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function clear_storage() {
-  echo " Clearing Cobalt storage"
-  eval "find ${STORAGE_DIR}/ -mindepth 1 -maxdepth 1 ! -name 'icu' -exec rm -rf {} +" 1> /dev/null
-}
-
diff --git a/starboard/evergreen/testing/linux/deploy_cobalt.sh b/starboard/evergreen/testing/linux/deploy_cobalt.sh
deleted file mode 100755
index a461fae..0000000
--- a/starboard/evergreen/testing/linux/deploy_cobalt.sh
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function deploy_cobalt() {
-  if [[ -z "${OUT}" ]]; then
-    log "info" "Please set the environment variable 'OUT'"
-    exit 1
-  fi
-
-  declare staging_dir=""
-  if [[ -e "${OUT}/deploy/loader_app" ]]; then
-    # Expected after launcher is run for a GYP build.
-    staging_dir="${OUT}/deploy/loader_app"
-  else
-    # Expected after launcher is run for a GN build.
-    staging_dir="${OUT}"
-  fi
-
-  echo " Checking '${staging_dir}'"
-
-  PATHS=("${staging_dir}/loader_app"                                                \
-         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
-         "${staging_dir}/content/app/cobalt/content/")
-
-  for file in "${PATHS[@]}"; do
-    if [[ ! -e "${file}" ]]; then
-      echo " Failed to find '${file}'"
-      exit 1
-    fi
-  done
-
-  echo " Required files were found within '${staging_dir}'"
-
-  echo " Deploying Cobalt on local device"
-
-  echo " Generating HTML test directory"
-  eval "mkdir -p ${staging_dir}/content/app/cobalt/content/web/tests/" 1> /dev/null
-
-  echo " Copying HTML test files to HTML test directory"
-  eval "cp ${1}/../tests/*.html ${staging_dir}/content/app/cobalt/content/web/tests/" 1> /dev/null
-
-  clear_storage
-
-  echo " Successfully deployed!"
-}
diff --git a/starboard/evergreen/testing/linux/stop_cobalt.sh b/starboard/evergreen/testing/linux/stop_cobalt.sh
deleted file mode 100755
index 0028883..0000000
--- a/starboard/evergreen/testing/linux/stop_cobalt.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function stop_cobalt() {
-  echo " Stopping Cobalt"
-  eval "kill -9 $(pidof "${OUT}/loader_app")" 1> /dev/null
-  sleep 1
-}
-
diff --git a/starboard/evergreen/testing/raspi/clean_up.sh b/starboard/evergreen/testing/raspi-2/clean_up.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/clean_up.sh
rename to starboard/evergreen/testing/raspi-2/clean_up.sh
diff --git a/starboard/evergreen/testing/raspi/clear_storage.sh b/starboard/evergreen/testing/raspi-2/clear_storage.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/clear_storage.sh
rename to starboard/evergreen/testing/raspi-2/clear_storage.sh
diff --git a/starboard/evergreen/testing/raspi/create_file.sh b/starboard/evergreen/testing/raspi-2/create_file.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/create_file.sh
rename to starboard/evergreen/testing/raspi-2/create_file.sh
diff --git a/starboard/evergreen/testing/raspi/delete_file.sh b/starboard/evergreen/testing/raspi-2/delete_file.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/delete_file.sh
rename to starboard/evergreen/testing/raspi-2/delete_file.sh
diff --git a/starboard/evergreen/testing/raspi-2/deploy_cobalt.sh b/starboard/evergreen/testing/raspi-2/deploy_cobalt.sh
new file mode 100755
index 0000000..930b417
--- /dev/null
+++ b/starboard/evergreen/testing/raspi-2/deploy_cobalt.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function deploy_cobalt() {
+  if [[ -z "${OUT}" ]]; then
+    log "info" "Please set the environment variable 'OUT'"
+    exit 1
+  fi
+
+  staging_dir="${OUT}/install/loader_app"
+
+  echo " Checking '${staging_dir}'"
+
+  PATHS=("${staging_dir}/loader_app"                                                \
+         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
+         "${staging_dir}/content/app/cobalt/content/")
+
+  for file in "${PATHS[@]}"; do
+    if [[ ! -e "${file}" ]]; then
+      echo " Failed to find '${file}'"
+      exit 1
+    fi
+  done
+
+  echo " Required files were found within '${OUT}'"
+
+  echo " Deploying to the Raspberry Pi 2 at ${RASPI_ADDR}"
+
+  echo " Regenerating Cobalt-on-Evergreen directory"
+  eval "${SSH} \"rm -rf /home/pi/coeg/\""
+  eval "${SSH} \"mkdir /home/pi/coeg/\""
+
+  echo " Copying loader_app to Cobalt-on-Evergreen directory"
+  eval "${SCP} \"${staging_dir}/loader_app pi@${RASPI_ADDR}:/home/pi/coeg/\""
+
+  echo " Copying crashpad_handler to Cobalt-on-Evergreen directory"
+  eval "${SCP} \"${staging_dir}/crashpad_handler pi@${RASPI_ADDR}:/home/pi/coeg/\""
+
+  echo " Regenerating system image directory"
+  eval "${SSH} \"mkdir -p /home/pi/coeg/content/app/cobalt/lib\""
+
+  echo " Copying cobalt to system image directory"
+  eval "${SCP} \"${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION} pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/lib/\""
+
+  echo " Copying content to system image directory"
+  eval "${SCP} \"-r ${staging_dir}/content/app/cobalt/content/ pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/\""
+
+  echo " Copying fonts to system content directory"
+  eval "${SCP} \"-r ${staging_dir}/content/fonts/ pi@${RASPI_ADDR}:/home/pi/coeg/content/\""
+
+  echo " Generating HTML test directory"
+  eval "${SSH} \"mkdir -p /home/pi/coeg/content/app/cobalt/content/web/tests/\""
+
+  echo " Copying HTML test files to HTML test directory"
+  eval "${SCP} \"${1}/../tests/empty.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
+  eval "${SCP} \"${1}/../tests/test.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
+  eval "${SCP} \"${1}/../tests/tseries.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
+
+  echo " Successfully deployed!"
+}
diff --git a/starboard/evergreen/testing/raspi/run_command.sh b/starboard/evergreen/testing/raspi-2/run_command.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/run_command.sh
rename to starboard/evergreen/testing/raspi-2/run_command.sh
diff --git a/starboard/evergreen/testing/raspi/secure_communication.sh b/starboard/evergreen/testing/raspi-2/secure_communication.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/secure_communication.sh
rename to starboard/evergreen/testing/raspi-2/secure_communication.sh
diff --git a/starboard/evergreen/testing/raspi/setup.sh b/starboard/evergreen/testing/raspi-2/setup.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/setup.sh
rename to starboard/evergreen/testing/raspi-2/setup.sh
diff --git a/starboard/evergreen/testing/raspi/start_cobalt.sh b/starboard/evergreen/testing/raspi-2/start_cobalt.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/start_cobalt.sh
rename to starboard/evergreen/testing/raspi-2/start_cobalt.sh
diff --git a/starboard/evergreen/testing/raspi-2/stop_cobalt.sh b/starboard/evergreen/testing/raspi-2/stop_cobalt.sh
new file mode 100755
index 0000000..0e80571
--- /dev/null
+++ b/starboard/evergreen/testing/raspi-2/stop_cobalt.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function stop_cobalt() {
+  echo " Stopping Cobalt"
+  eval "${SSH}\"pidof /home/pi/coeg/loader_app | xargs kill -9\"" 1> /dev/null
+  sleep 1
+}
diff --git a/starboard/evergreen/testing/raspi/stop_process.sh b/starboard/evergreen/testing/raspi-2/stop_process.sh
similarity index 100%
rename from starboard/evergreen/testing/raspi/stop_process.sh
rename to starboard/evergreen/testing/raspi-2/stop_process.sh
diff --git a/starboard/evergreen/testing/raspi/deploy_cobalt.sh b/starboard/evergreen/testing/raspi/deploy_cobalt.sh
deleted file mode 100755
index 9012f3c..0000000
--- a/starboard/evergreen/testing/raspi/deploy_cobalt.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function deploy_cobalt() {
-  if [[ -z "${OUT}" ]]; then
-    log "info" "Please set the environment variable 'OUT'"
-    exit 1
-  fi
-
-  declare staging_dir=""
-  if [[ -e "${OUT}/deploy/loader_app" ]]; then
-    # Expected after launcher is run for a GYP build.
-    staging_dir="${OUT}/deploy/loader_app"
-  else
-    # Expected after launcher is run for a GN build.
-    staging_dir="${OUT}/install/loader_app"
-  fi
-
-  echo " Checking '${staging_dir}'"
-
-  PATHS=("${staging_dir}/loader_app"                                                \
-         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
-         "${staging_dir}/content/app/cobalt/content/")
-
-  for file in "${PATHS[@]}"; do
-    if [[ ! -e "${file}" ]]; then
-      echo " Failed to find '${file}'"
-      exit 1
-    fi
-  done
-
-  echo " Required files were found within '${OUT}'"
-
-  echo " Deploying to the Raspberry Pi 2 at ${RASPI_ADDR}"
-
-  echo " Regenerating Cobalt-on-Evergreen directory"
-  eval "${SSH} \"rm -rf /home/pi/coeg/\""
-  eval "${SSH} \"mkdir /home/pi/coeg/\""
-
-  echo " Copying loader_app to Cobalt-on-Evergreen directory"
-  eval "${SCP} \"${staging_dir}/loader_app pi@${RASPI_ADDR}:/home/pi/coeg/\""
-
-  echo " Copying crashpad_handler to Cobalt-on-Evergreen directory"
-  eval "${SCP} \"${staging_dir}/crashpad_handler pi@${RASPI_ADDR}:/home/pi/coeg/\""
-
-  echo " Regenerating system image directory"
-  eval "${SSH} \"mkdir -p /home/pi/coeg/content/app/cobalt/lib\""
-
-  echo " Copying cobalt to system image directory"
-  eval "${SCP} \"${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION} pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/lib/\""
-
-  echo " Copying content to system image directory"
-  eval "${SCP} \"-r ${staging_dir}/content/app/cobalt/content/ pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/\""
-
-  echo " Copying fonts to system content directory"
-  eval "${SCP} \"-r ${staging_dir}/content/fonts/ pi@${RASPI_ADDR}:/home/pi/coeg/content/\""
-
-  echo " Generating HTML test directory"
-  eval "${SSH} \"mkdir -p /home/pi/coeg/content/app/cobalt/content/web/tests/\""
-
-  echo " Copying HTML test files to HTML test directory"
-  eval "${SCP} \"${1}/../tests/empty.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
-  eval "${SCP} \"${1}/../tests/test.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
-  eval "${SCP} \"${1}/../tests/tseries.html pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/content/web/tests/\""
-
-  echo " Successfully deployed!"
-}
diff --git a/starboard/evergreen/testing/raspi/stop_cobalt.sh b/starboard/evergreen/testing/raspi/stop_cobalt.sh
deleted file mode 100755
index ffd5bac..0000000
--- a/starboard/evergreen/testing/raspi/stop_cobalt.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-function stop_cobalt() {
-  echo " Stopping Cobalt"
-  eval "${SSH}\"pidof /home/pi/coeg/loader_app | xargs kill -9\"" 1> /dev/null
-  sleep 1
-}
-
diff --git a/starboard/evergreen/testing/setup.sh b/starboard/evergreen/testing/setup.sh
index 393ea6d..e97e067 100755
--- a/starboard/evergreen/testing/setup.sh
+++ b/starboard/evergreen/testing/setup.sh
@@ -27,16 +27,15 @@
 log "info" " [==========] Preparing to test with USE_COMPRESSED_SYSTEM_IMAGE=${USE_COMPRESSED_SYSTEM_IMAGE}."
 
 if [[ -z ${1} ]]; then
-  log "error" "A platform must be provided"
+  log "error" "A loader platform must be provided"
   exit 1
 fi
 
-PLATFORMS=("linux" "raspi")
-if [[ ! "${PLATFORMS[@]}" =~ "${1}" ]] && [[ ! -d "${DIR}/${1}" ]]; then
-  log "error" "The platform provided must be one of the following: ${PLATFORMS[*]}"
+if [[ "${1}" != "linux-x64x11" ]] && [[ "${1}" != "raspi-2" ]]; then
+  log "error" "The loader platform provided must be either linux-x64x11 or raspi-2"
   exit 1
 fi
-PLATFORM="${1}"
+LOADER_PLATFORM="${1}"
 
 # List of all required scripts.
 SCRIPTS=("${DIR}/shared/app_key.sh"           \
@@ -48,17 +47,17 @@
 
          # Each of the following scripts must be provided for the targeted
          # platform. The script 'setup.sh' must be source'd first.
-         "${DIR}/${PLATFORM}/setup.sh"               \
+         "${DIR}/${LOADER_PLATFORM}/setup.sh"               \
 
-         "${DIR}/${PLATFORM}/clean_up.sh"            \
-         "${DIR}/${PLATFORM}/clear_storage.sh"       \
-         "${DIR}/${PLATFORM}/create_file.sh"         \
-         "${DIR}/${PLATFORM}/delete_file.sh"         \
-         "${DIR}/${PLATFORM}/deploy_cobalt.sh"       \
-         "${DIR}/${PLATFORM}/run_command.sh"         \
-         "${DIR}/${PLATFORM}/start_cobalt.sh"        \
-         "${DIR}/${PLATFORM}/stop_cobalt.sh"         \
-         "${DIR}/${PLATFORM}/stop_process.sh")
+         "${DIR}/${LOADER_PLATFORM}/clean_up.sh"            \
+         "${DIR}/${LOADER_PLATFORM}/clear_storage.sh"       \
+         "${DIR}/${LOADER_PLATFORM}/create_file.sh"         \
+         "${DIR}/${LOADER_PLATFORM}/delete_file.sh"         \
+         "${DIR}/${LOADER_PLATFORM}/deploy_cobalt.sh"       \
+         "${DIR}/${LOADER_PLATFORM}/run_command.sh"         \
+         "${DIR}/${LOADER_PLATFORM}/start_cobalt.sh"        \
+         "${DIR}/${LOADER_PLATFORM}/stop_cobalt.sh"         \
+         "${DIR}/${LOADER_PLATFORM}/stop_process.sh")
 
 for script in "${SCRIPTS[@]}"; do
   if [[ ! -f "${script}" ]]; then
@@ -66,5 +65,5 @@
     exit 1
   fi
 
-  source $script "${DIR}/${PLATFORM}"
+  source $script "${DIR}/${LOADER_PLATFORM}"
 done
diff --git a/starboard/evergreen/testing/tests/use_mmap_file_test.sh b/starboard/evergreen/testing/tests/use_mmap_file_test.sh
index 66b53f1..9905bc1 100644
--- a/starboard/evergreen/testing/tests/use_mmap_file_test.sh
+++ b/starboard/evergreen/testing/tests/use_mmap_file_test.sh
@@ -23,8 +23,8 @@
 TEST_FILE="test.html"
 
 function run_test() {
-  if [[ "${PLATFORM}" == "raspi" ]]; then
-    echo " MemoryMappedFile extension not implemented for raspi, skipping"
+  if [[ "${LOADER_PLATFORM}" == "raspi-2" ]]; then
+    echo " MemoryMappedFile extension not implemented for raspi-2, skipping"
     return 2
   fi
 
diff --git a/starboard/examples/hello_world/BUILD.gn b/starboard/examples/hello_world/BUILD.gn
new file mode 100644
index 0000000..36642f6
--- /dev/null
+++ b/starboard/examples/hello_world/BUILD.gn
@@ -0,0 +1,18 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+target(final_executable_type, "starboard_hello_world_example") {
+  deps = [ "//starboard" ]
+  sources = [ "main.cc" ]
+}
diff --git a/starboard/examples/hello_world/README.md b/starboard/examples/hello_world/README.md
new file mode 100644
index 0000000..9b1a3df
--- /dev/null
+++ b/starboard/examples/hello_world/README.md
@@ -0,0 +1,3 @@
+# Hello World Example
+
+This is hello world example using Starboard.
diff --git a/starboard/examples/hello_world/main.cc b/starboard/examples/hello_world/main.cc
new file mode 100644
index 0000000..3cdadc0
--- /dev/null
+++ b/starboard/examples/hello_world/main.cc
@@ -0,0 +1,27 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/event.h"
+#include "starboard/log.h"
+
+void SbEventHandle(const SbEvent* event) {
+  switch (event->type) {
+    case kSbEventTypeStart:
+      SbLogRawFormatF("hello, world!\n");
+      break;
+    default:
+      SbLogRawFormatF("event->type=%d\n", event->type);
+      break;
+  }
+}
diff --git a/cobalt/extension/BUILD.gn b/starboard/extension/BUILD.gn
similarity index 100%
rename from cobalt/extension/BUILD.gn
rename to starboard/extension/BUILD.gn
diff --git a/starboard/extension/configuration.h b/starboard/extension/configuration.h
new file mode 100644
index 0000000..bbd2da3
--- /dev/null
+++ b/starboard/extension/configuration.h
@@ -0,0 +1,227 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_CONFIGURATION_H_
+#define STARBOARD_EXTENSION_CONFIGURATION_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionConfigurationName "dev.cobalt.extension.Configuration"
+
+typedef struct CobaltExtensionConfigurationApi {
+  // Name should be the string |kCobaltExtensionConfigurationName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // The functions below configure Cobalt. All correspond to some GYP variable,
+  // but the implementation of this functions will take precedence over the GYP
+  // variable.
+
+  // This variable defines what Cobalt's preferred strategy should be for
+  // handling internally triggered application exit requests (e.g. the user
+  // chooses to back out of the application).
+  //   'stop'    -- The application should call SbSystemRequestStop() on exit,
+  //                resulting in a complete shutdown of the application.
+  //   'suspend' -- The application should call SbSystemRequestSuspend() on
+  //                exit, resulting in the application being "minimized".
+  //   'noexit'  -- The application should never allow the user to trigger an
+  //                exit, this will be managed by the system.
+  const char* (*CobaltUserOnExitStrategy)();
+
+  // If set to |true|, will enable support for rendering only the regions of
+  // the display that are modified due to animations, instead of re-rendering
+  // the entire scene each frame.  This feature can reduce startup time where
+  // usually there is a small loading spinner animating on the screen.  On GLES
+  // renderers, Cobalt will attempt to implement this support by using
+  // eglSurfaceAttrib(..., EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED), otherwise
+  // the dirty region will be silently disabled. Note that some GLES driver
+  // implementations may internally allocate an extra full screen surface to
+  // support this feature, and many have been noticed to not properly support
+  // this functionality (but they report that they do), and for these reasons
+  // this value is defaulted to |false|.
+  bool (*CobaltRenderDirtyRegionOnly)();
+
+  // Cobalt will call eglSwapInterval() and specify this value before calling
+  // eglSwapBuffers() each frame.
+  int (*CobaltEglSwapInterval)();
+
+  // The URL of default build time splash screen - see
+  // cobalt/doc/splash_screen.md for information about this.
+  const char* (*CobaltFallbackSplashScreenUrl)();
+
+  // If set to |true|, enables Quic.
+  bool (*CobaltEnableQuic)();
+
+  // Cache parameters
+
+  // The following set of parameters define how much memory is reserved for
+  // different Cobalt caches.  These caches affect CPU *and* GPU memory usage.
+  //
+  // The sum of the following caches effectively describes the maximum GPU
+  // texture memory usage (though it doesn't consider video textures and
+  // display color buffers):
+  //   - CobaltSkiaCacheSizeInBytes (GLES2 rasterizer only)
+  //   - CobaltImageCacheSizeInBytes
+  //   - CobaltSkiaGlyphAtlasWidth * CobaltSkiaGlyphAtlasHeight
+  //
+  // The other caches affect CPU memory usage.
+
+  // Determines the capacity of the skia cache.  The Skia cache is maintained
+  // within Skia and is used to cache the results of complicated effects such
+  // as shadows, so that Skia draw calls that are used repeatedly across
+  // frames can be cached into surfaces.  This setting is only relevant when
+  // using the hardware-accelerated Skia rasterizer.
+  int (*CobaltSkiaCacheSizeInBytes)();
+
+  // Determines the amount of GPU memory the offscreen target atlases will
+  // use. This is specific to the direct-GLES rasterizer and caches any render
+  // tree nodes which require skia for rendering. Two atlases will be allocated
+  // from this memory or multiple atlases of the frame size if the limit
+  // allows. It is recommended that enough memory be reserved for two RGBA
+  // atlases about a quarter of the frame size.
+  int (*CobaltOffscreenTargetCacheSizeInBytes)();
+
+  // Determines the capacity of the encoded image cache, which manages encoded
+  // images downloaded from a web page. These images are cached within CPU
+  // memory.  This not only reduces network traffic to download the encoded
+  // images, but also allows the downloaded images to be held during suspend.
+  // Note that there is also a cache for the decoded images whose capacity is
+  // specified in |CobaltImageCacheSizeInBytes|.  The decoded images are often
+  // cached in the GPU memory and will be released during suspend.
+  //
+  // If a system meets the following requirements:
+  // 1. Has a fast image decoder.
+  // 2. Has enough CPU memory, or has a unified memory architecture that allows
+  //    sharing of CPU and GPU memory.
+  // Then it may consider implementing |CobaltEncodedImageCacheSizeInBytes| to
+  // return a much bigger value, and set the return value of
+  // |CobaltImageCacheSizeInBytes| to a much smaller value. This allows the app
+  // to cache significantly more images.
+  //
+  // Setting this to 0 can disable the cache completely.
+  int (*CobaltEncodedImageCacheSizeInBytes)();
+
+  // Determines the capacity of the image cache, which manages image surfaces
+  // downloaded from a web page.  While it depends on the platform, often (and
+  // ideally) these images are cached within GPU memory.
+  // Set to -1 to automatically calculate the value at runtime, based on
+  // features like windows dimensions and the value of
+  // SbSystemGetTotalGPUMemory().
+  int (*CobaltImageCacheSizeInBytes)();
+
+  // Determines the capacity of the local font cache, which manages all fonts
+  // loaded from local files. Newly encountered sections of font files are
+  // lazily loaded into the cache, enabling subsequent requests to the same
+  // file sections to be handled via direct memory access. Once the limit is
+  // reached, further requests are handled via file stream.
+  // Setting the value to 0 disables memory caching and causes all font file
+  // accesses to be done using file streams.
+  int (*CobaltLocalTypefaceCacheSizeInBytes)();
+
+  // Determines the capacity of the remote font cache, which manages all
+  // fonts downloaded from a web page.
+  int (*CobaltRemoteTypefaceCacheSizeInBytes)();
+
+  // Determines the capacity of the mesh cache. Each mesh is held compressed
+  // in main memory, to be inflated into a GPU buffer when needed for
+  // projection.
+  int (*CobaltMeshCacheSizeInBytes)();
+
+  // Deprecated
+  int (*CobaltSoftwareSurfaceCacheSizeInBytes)();
+
+  // Modifying this function's return value to be non-1.0f will result in the
+  // image cache capacity being cleared and then temporarily reduced for the
+  // duration that a video is playing.  This can be useful for some platforms
+  // if they are particularly constrained for (GPU) memory during video
+  // playback.  When playing a video, the image cache is reduced to:
+  // CobaltImageCacheSizeInBytes() *
+  //     CobaltImageCacheCapacityMultiplierWhenPlayingVideo().
+  float (*CobaltImageCacheCapacityMultiplierWhenPlayingVideo)();
+
+  // Determines the size in pixels of the glyph atlas where rendered glyphs are
+  // cached. The resulting memory usage is 2 bytes of GPU memory per pixel.
+  // When a value is used that is too small, thrashing may occur that will
+  // result in visible stutter. Such thrashing is more likely to occur when CJK
+  // language glyphs are rendered and when the size of the glyphs in pixels is
+  // larger, such as for higher resolution displays.
+  // The negative default values indicates to the engine that these settings
+  // should be automatically set.
+  int (*CobaltSkiaGlyphAtlasWidth)();
+  int (*CobaltSkiaGlyphAtlasHeight)();
+
+  // This configuration has been deprecated and is only kept for
+  // backward-compatibility. It has no effect on V8.
+  int (*CobaltJsGarbageCollectionThresholdInBytes)();
+
+  // When specified this value will reduce the cpu memory consumption by
+  // the specified amount. -1 disables the value.
+  int (*CobaltReduceCpuMemoryBy)();
+
+  // When specified this value will reduce the gpu memory consumption by
+  // the specified amount. -1 disables the value.
+  int (*CobaltReduceGpuMemoryBy)();
+
+  // Can be set to enable zealous garbage collection. Zealous garbage
+  // collection will cause garbage collection to occur much more frequently
+  // than normal, for the purpose of finding or reproducing bugs.
+  bool (*CobaltGcZeal)();
+
+  // Defines what kind of rasterizer will be used.  This can be adjusted to
+  // force a stub graphics implementation.
+  // It can be one of the following options:
+  //   'direct-gles' -- Uses a light wrapper over OpenGL ES to handle most
+  //                    draw elements. This will fall back to the skia hardware
+  //                    rasterizer for some render tree node types, but is
+  //                    generally faster on the CPU and GPU. This can handle
+  //                    360 rendering.
+  //   'hardware'    -- As much hardware acceleration of graphics commands as
+  //                    possible. This uses skia to wrap OpenGL ES commands.
+  //                    Required for 360 rendering.
+  //   'stub'        -- Stub graphics rasterization.  A rasterizer object will
+  //                    still be available and valid, but it will do nothing.
+  const char* (*CobaltRasterizerType)();
+
+  // Controls whether or not just in time code should be used.
+  // See "cobalt/doc/performance_tuning.md" for more information on when this
+  // should be used.
+  bool (*CobaltEnableJit)();
+
+  // The fields below this point were added in version 2 or later.
+
+  // A mapping of splash screen topics to fallback URLs.
+  const char* (*CobaltFallbackSplashScreenTopics)();
+
+  // The fields below this point were added in version 3 or later.
+
+  // Determines whether compiled Javascript caching code is enabled.
+  bool (*CobaltCanStoreCompiledJavascript)();
+} CobaltExtensionConfigurationApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_CONFIGURATION_H_
diff --git a/starboard/extension/crash_handler.h b/starboard/extension/crash_handler.h
new file mode 100644
index 0000000..92a7d35
--- /dev/null
+++ b/starboard/extension/crash_handler.h
@@ -0,0 +1,54 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_CRASH_HANDLER_H_
+#define STARBOARD_EXTENSION_CRASH_HANDLER_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+#include "third_party/crashpad/wrapper/annotations.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionCrashHandlerName "dev.cobalt.extension.CrashHandler"
+
+typedef struct CobaltExtensionCrashHandlerApi {
+  // Name should be the string |kCobaltExtensionCrashHandlerName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Deprecated in version 2 and later.
+  bool (*OverrideCrashpadAnnotations)(
+      CrashpadAnnotations* crashpad_annotations);
+
+  // The fields below this point were added in version 2 or later.
+
+  // Sets a (key, value) pair for the handler to include when annotating a
+  // crash. Returns true on success and false on failure.
+  bool (*SetString)(const char* key, const char* value);
+} CobaltExtensionCrashHandlerApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_CRASH_HANDLER_H_
diff --git a/starboard/extension/cwrappers.h b/starboard/extension/cwrappers.h
new file mode 100644
index 0000000..f3bddf3
--- /dev/null
+++ b/starboard/extension/cwrappers.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_CWRAPPERS_H_
+#define STARBOARD_EXTENSION_CWRAPPERS_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionCWrappersName "dev.cobalt.extension.CWrappers"
+
+typedef struct CobaltExtensionCWrappersApi {
+  // Name should be the string kCobaltExtensionCWrapperName.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  double (*PowWrapper)(double base, double exponent);
+} CobaltExtensionCWrappersApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_CWRAPPERS_H_
diff --git a/starboard/extension/demuxer.h b/starboard/extension/demuxer.h
new file mode 100644
index 0000000..6107e64
--- /dev/null
+++ b/starboard/extension/demuxer.h
@@ -0,0 +1,409 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Contains extension code allowing partners to provide their own demuxer.
+// CobaltExtensionDemuxerApi is the main API.
+
+#ifndef STARBOARD_EXTENSION_DEMUXER_H_
+#define STARBOARD_EXTENSION_DEMUXER_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "starboard/time.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionDemuxerApi "dev.cobalt.extension.Demuxer"
+
+// This must stay in sync with ::media::PipelineStatus. Missing values are
+// either irrelevant to the demuxer or are deprecated values of PipelineStatus.
+typedef enum CobaltExtensionDemuxerStatus {
+  kCobaltExtensionDemuxerOk = 0,
+  kCobaltExtensionDemuxerErrorNetwork = 2,
+  kCobaltExtensionDemuxerErrorAbort = 5,
+  kCobaltExtensionDemuxerErrorInitializationFailed = 6,
+  kCobaltExtensionDemuxerErrorRead = 9,
+  kCobaltExtensionDemuxerErrorInvalidState = 11,
+  kCobaltExtensionDemuxerErrorCouldNotOpen = 12,
+  kCobaltExtensionDemuxerErrorCouldNotParse = 13,
+  kCobaltExtensionDemuxerErrorNoSupportedStreams = 14
+} CobaltExtensionDemuxerStatus;
+
+// Type of side data associated with a buffer.
+typedef enum CobaltExtensionDemuxerSideDataType {
+  kCobaltExtensionDemuxerUnknownSideDataType = 0,
+  kCobaltExtensionDemuxerMatroskaBlockAdditional = 1,
+} CobaltExtensionDemuxerSideDataType;
+
+// This must stay in sync with ::media::AudioCodec.
+typedef enum CobaltExtensionDemuxerAudioCodec {
+  kCobaltExtensionDemuxerCodecUnknownAudio = 0,
+  kCobaltExtensionDemuxerCodecAAC = 1,
+  kCobaltExtensionDemuxerCodecMP3 = 2,
+  kCobaltExtensionDemuxerCodecPCM = 3,
+  kCobaltExtensionDemuxerCodecVorbis = 4,
+  kCobaltExtensionDemuxerCodecFLAC = 5,
+  kCobaltExtensionDemuxerCodecAMR_NB = 6,
+  kCobaltExtensionDemuxerCodecAMR_WB = 7,
+  kCobaltExtensionDemuxerCodecPCM_MULAW = 8,
+  kCobaltExtensionDemuxerCodecGSM_MS = 9,
+  kCobaltExtensionDemuxerCodecPCM_S16BE = 10,
+  kCobaltExtensionDemuxerCodecPCM_S24BE = 11,
+  kCobaltExtensionDemuxerCodecOpus = 12,
+  kCobaltExtensionDemuxerCodecEAC3 = 13,
+  kCobaltExtensionDemuxerCodecPCM_ALAW = 14,
+  kCobaltExtensionDemuxerCodecALAC = 15,
+  kCobaltExtensionDemuxerCodecAC3 = 16
+} CobaltExtensionDemuxerAudioCodec;
+
+// This must stay in sync with ::media::VideoCodec.
+typedef enum CobaltExtensionDemuxerVideoCodec {
+  kCobaltExtensionDemuxerCodecUnknownVideo = 0,
+  kCobaltExtensionDemuxerCodecH264,
+  kCobaltExtensionDemuxerCodecVC1,
+  kCobaltExtensionDemuxerCodecMPEG2,
+  kCobaltExtensionDemuxerCodecMPEG4,
+  kCobaltExtensionDemuxerCodecTheora,
+  kCobaltExtensionDemuxerCodecVP8,
+  kCobaltExtensionDemuxerCodecVP9,
+  kCobaltExtensionDemuxerCodecHEVC,
+  kCobaltExtensionDemuxerCodecDolbyVision,
+  kCobaltExtensionDemuxerCodecAV1,
+} CobaltExtensionDemuxerVideoCodec;
+
+// This must stay in sync with ::media::SampleFormat.
+typedef enum CobaltExtensionDemuxerSampleFormat {
+  kCobaltExtensionDemuxerSampleFormatUnknown = 0,
+  kCobaltExtensionDemuxerSampleFormatU8,   // Unsigned 8-bit w/ bias of 128.
+  kCobaltExtensionDemuxerSampleFormatS16,  // Signed 16-bit.
+  kCobaltExtensionDemuxerSampleFormatS32,  // Signed 32-bit.
+  kCobaltExtensionDemuxerSampleFormatF32,  // Float 32-bit.
+  kCobaltExtensionDemuxerSampleFormatPlanarS16,  // Signed 16-bit planar.
+  kCobaltExtensionDemuxerSampleFormatPlanarF32,  // Float 32-bit planar.
+  kCobaltExtensionDemuxerSampleFormatPlanarS32,  // Signed 32-bit planar.
+  kCobaltExtensionDemuxerSampleFormatS24,        // Signed 24-bit.
+} CobaltExtensionDemuxerSampleFormat;
+
+// This must stay in sync with ::media::ChannelLayout.
+typedef enum CobaltExtensionDemuxerChannelLayout {
+  kCobaltExtensionDemuxerChannelLayoutNone = 0,
+  kCobaltExtensionDemuxerChannelLayoutUnsupported = 1,
+  kCobaltExtensionDemuxerChannelLayoutMono = 2,
+  kCobaltExtensionDemuxerChannelLayoutStereo = 3,
+  kCobaltExtensionDemuxerChannelLayout2_1 = 4,
+  kCobaltExtensionDemuxerChannelLayoutSurround = 5,
+  kCobaltExtensionDemuxerChannelLayout4_0 = 6,
+  kCobaltExtensionDemuxerChannelLayout2_2 = 7,
+  kCobaltExtensionDemuxerChannelLayoutQuad = 8,
+  kCobaltExtensionDemuxerChannelLayout5_0 = 9,
+  kCobaltExtensionDemuxerChannelLayout5_1 = 10,
+  kCobaltExtensionDemuxerChannelLayout5_0Back = 11,
+  kCobaltExtensionDemuxerChannelLayout5_1Back = 12,
+  kCobaltExtensionDemuxerChannelLayout7_0 = 13,
+  kCobaltExtensionDemuxerChannelLayout7_1 = 14,
+  kCobaltExtensionDemuxerChannelLayout7_1Wide = 15,
+  kCobaltExtensionDemuxerChannelLayoutStereoDownmix = 16,
+  kCobaltExtensionDemuxerChannelLayout2point1 = 17,
+  kCobaltExtensionDemuxerChannelLayout3_1 = 18,
+  kCobaltExtensionDemuxerChannelLayout4_1 = 19,
+  kCobaltExtensionDemuxerChannelLayout6_0 = 20,
+  kCobaltExtensionDemuxerChannelLayout6_0Front = 21,
+  kCobaltExtensionDemuxerChannelLayoutHexagonal = 22,
+  kCobaltExtensionDemuxerChannelLayout6_1 = 23,
+  kCobaltExtensionDemuxerChannelLayout6_1Back = 24,
+  kCobaltExtensionDemuxerChannelLayout6_1Front = 25,
+  kCobaltExtensionDemuxerChannelLayout7_0Front = 26,
+  kCobaltExtensionDemuxerChannelLayout7_1WideBack = 27,
+  kCobaltExtensionDemuxerChannelLayoutOctagonal = 28,
+  kCobaltExtensionDemuxerChannelLayoutDiscrete = 29,
+  kCobaltExtensionDemuxerChannelLayoutStereoAndKeyboardMic = 30,
+  kCobaltExtensionDemuxerChannelLayout4_1QuadSide = 31,
+  kCobaltExtensionDemuxerChannelLayoutBitstream = 32
+} CobaltExtensionDemuxerChannelLayout;
+
+// This must stay in sync with ::media::VideoCodecProfile.
+typedef enum CobaltExtensionDemuxerVideoCodecProfile {
+  kCobaltExtensionDemuxerVideoCodecProfileUnknown = -1,
+  kCobaltExtensionDemuxerH264ProfileMin = 0,
+  kCobaltExtensionDemuxerH264ProfileBaseline =
+      kCobaltExtensionDemuxerH264ProfileMin,
+  kCobaltExtensionDemuxerH264ProfileMain = 1,
+  kCobaltExtensionDemuxerH264ProfileExtended = 2,
+  kCobaltExtensionDemuxerH264ProfileHigh = 3,
+  kCobaltExtensionDemuxerH264ProfileHigh10Profile = 4,
+  kCobaltExtensionDemuxerH264ProfileHigh422Profile = 5,
+  kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile = 6,
+  kCobaltExtensionDemuxerH264ProfileScalableBaseline = 7,
+  kCobaltExtensionDemuxerH264ProfileScalableHigh = 8,
+  kCobaltExtensionDemuxerH264ProfileStereoHigh = 9,
+  kCobaltExtensionDemuxerH264ProfileMultiviewHigh = 10,
+  kCobaltExtensionDemuxerH264ProfileMax =
+      kCobaltExtensionDemuxerH264ProfileMultiviewHigh,
+  kCobaltExtensionDemuxerVp8ProfileMin = 11,
+  kCobaltExtensionDemuxerVp8ProfileAny = kCobaltExtensionDemuxerVp8ProfileMin,
+  kCobaltExtensionDemuxerVp8ProfileMax = kCobaltExtensionDemuxerVp8ProfileAny,
+  kCobaltExtensionDemuxerVp9ProfileMin = 12,
+  kCobaltExtensionDemuxerVp9ProfileProfile0 =
+      kCobaltExtensionDemuxerVp9ProfileMin,
+  kCobaltExtensionDemuxerVp9ProfileProfile1 = 13,
+  kCobaltExtensionDemuxerVp9ProfileProfile2 = 14,
+  kCobaltExtensionDemuxerVp9ProfileProfile3 = 15,
+  kCobaltExtensionDemuxerVp9ProfileMax =
+      kCobaltExtensionDemuxerVp9ProfileProfile3,
+  kCobaltExtensionDemuxerHevcProfileMin = 16,
+  kCobaltExtensionDemuxerHevcProfileMain =
+      kCobaltExtensionDemuxerHevcProfileMin,
+  kCobaltExtensionDemuxerHevcProfileMain10 = 17,
+  kCobaltExtensionDemuxerHevcProfileMainStillPicture = 18,
+  kCobaltExtensionDemuxerHevcProfileMax =
+      kCobaltExtensionDemuxerHevcProfileMainStillPicture,
+  kCobaltExtensionDemuxerDolbyVisionProfile0 = 19,
+  kCobaltExtensionDemuxerDolbyVisionProfile4 = 20,
+  kCobaltExtensionDemuxerDolbyVisionProfile5 = 21,
+  kCobaltExtensionDemuxerDolbyVisionProfile7 = 22,
+  kCobaltExtensionDemuxerTheoraProfileMin = 23,
+  kCobaltExtensionDemuxerTheoraProfileAny =
+      kCobaltExtensionDemuxerTheoraProfileMin,
+  kCobaltExtensionDemuxerTheoraProfileMax =
+      kCobaltExtensionDemuxerTheoraProfileAny,
+  kCobaltExtensionDemuxerAv1ProfileMin = 24,
+  kCobaltExtensionDemuxerAv1ProfileProfileMain =
+      kCobaltExtensionDemuxerAv1ProfileMin,
+  kCobaltExtensionDemuxerAv1ProfileProfileHigh = 25,
+  kCobaltExtensionDemuxerAv1ProfileProfilePro = 26,
+  kCobaltExtensionDemuxerAv1ProfileMax =
+      kCobaltExtensionDemuxerAv1ProfileProfilePro,
+  kCobaltExtensionDemuxerDolbyVisionProfile8 = 27,
+  kCobaltExtensionDemuxerDolbyVisionProfile9 = 28,
+} CobaltExtensionDemuxerVideoCodecProfile;
+
+// This must be kept in sync with gfx::ColorSpace::RangeID.
+typedef enum CobaltExtensionDemuxerColorSpaceRangeId {
+  kCobaltExtensionDemuxerColorSpaceRangeIdInvalid = 0,
+  kCobaltExtensionDemuxerColorSpaceRangeIdLimited = 1,
+  kCobaltExtensionDemuxerColorSpaceRangeIdFull = 2,
+  kCobaltExtensionDemuxerColorSpaceRangeIdDerived = 3
+} CobaltExtensionDemuxerColorSpaceRangeId;
+
+// This must be kept in sync with media::VideoDecoderConfig::AlphaMode.
+typedef enum CobaltExtensionDemuxerAlphaMode {
+  kCobaltExtensionDemuxerHasAlpha,
+  kCobaltExtensionDemuxerIsOpaque
+} CobaltExtensionDemuxerAlphaMode;
+
+// This must be kept in sync with ::media::DemuxerStream::Type.
+typedef enum CobaltExtensionDemuxerStreamType {
+  kCobaltExtensionDemuxerStreamTypeUnknown,
+  kCobaltExtensionDemuxerStreamTypeAudio,
+  kCobaltExtensionDemuxerStreamTypeVideo,
+  kCobaltExtensionDemuxerStreamTypeText
+} CobaltExtensionDemuxerStreamType;
+
+// This must be kept in sync with media::EncryptionScheme.
+typedef enum CobaltExtensionDemuxerEncryptionScheme {
+  kCobaltExtensionDemuxerEncryptionSchemeUnencrypted,
+  kCobaltExtensionDemuxerEncryptionSchemeCenc,
+  kCobaltExtensionDemuxerEncryptionSchemeCbcs,
+} CobaltExtensionDemuxerEncryptionScheme;
+
+typedef struct CobaltExtensionDemuxerAudioDecoderConfig {
+  CobaltExtensionDemuxerAudioCodec codec;
+  CobaltExtensionDemuxerSampleFormat sample_format;
+  CobaltExtensionDemuxerChannelLayout channel_layout;
+  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
+  int samples_per_second;
+
+  uint8_t* extra_data;  // Not owned by this struct.
+  int64_t extra_data_size;
+} CobaltExtensionDemuxerAudioDecoderConfig;
+
+typedef struct CobaltExtensionDemuxerVideoDecoderConfig {
+  CobaltExtensionDemuxerVideoCodec codec;
+  CobaltExtensionDemuxerVideoCodecProfile profile;
+
+  // These fields represent the color space.
+  int color_space_primaries;
+  int color_space_transfer;
+  int color_space_matrix;
+  CobaltExtensionDemuxerColorSpaceRangeId color_space_range_id;
+
+  CobaltExtensionDemuxerAlphaMode alpha_mode;
+
+  // These fields represent the coded size.
+  int coded_width;
+  int coded_height;
+
+  // These fields represent the visible rectangle.
+  int visible_rect_x;
+  int visible_rect_y;
+  int visible_rect_width;
+  int visible_rect_height;
+
+  // These fields represent the natural size.
+  int natural_width;
+  int natural_height;
+
+  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
+
+  uint8_t* extra_data;  // Not owned by this struct.
+  int64_t extra_data_size;
+} CobaltExtensionDemuxerVideoDecoderConfig;
+
+typedef struct CobaltExtensionDemuxerSideData {
+  uint8_t* data;  // Not owned by this struct.
+  // Number of bytes in |data|.
+  int64_t data_size;
+  // Specifies the format of |data|.
+  CobaltExtensionDemuxerSideDataType type;
+} CobaltExtensionDemuxerSideData;
+
+typedef struct CobaltExtensionDemuxerBuffer {
+  // The media data for this buffer. Ownership is not transferred via this
+  // struct.
+  uint8_t* data;
+  // Number of bytes in |data|.
+  int64_t data_size;
+  // An array of side data elements containing any side data for this buffer.
+  // Ownership is not transferred via this struct.
+  CobaltExtensionDemuxerSideData* side_data;
+  // Number of elements in |side_data|.
+  int64_t side_data_elements;
+  // Playback time in microseconds.
+  SbTime pts;
+  // Duration of this buffer in microseconds.
+  SbTime duration;
+  // True if this buffer contains a keyframe.
+  bool is_keyframe;
+  // Signifies the end of the stream. If this is true, the other fields will be
+  // ignored.
+  bool end_of_stream;
+} CobaltExtensionDemuxerBuffer;
+
+// Note: |buffer| is the input to this function, not the output. Cobalt
+// implements this function to read media data provided by the implementer of
+// CobaltExtensionDemuxer.
+typedef void (*CobaltExtensionDemuxerReadCB)(
+    CobaltExtensionDemuxerBuffer* buffer,
+    void* user_data);
+
+// A fully synchronous demuxer API. Threading concerns are handled by the code
+// that uses this API.
+// When calling the defined functions, the |user_data| argument must be the
+// void* user_data field stored in this struct.
+typedef struct CobaltExtensionDemuxer {
+  // Initialize must only be called once for a demuxer; subsequent calls can
+  // fail.
+  CobaltExtensionDemuxerStatus (*Initialize)(void* user_data);
+
+  CobaltExtensionDemuxerStatus (*Seek)(SbTime seek_time, void* user_data);
+
+  // Returns the starting time for the media file; it is always positive.
+  SbTime (*GetStartTime)(void* user_data);
+
+  // Returns the time -- in microseconds since Windows epoch -- represented by
+  // presentation timestamp 0. If the timestamps are not associated with a time,
+  // returns 0.
+  SbTime (*GetTimelineOffset)(void* user_data);
+
+  // Calls |read_cb| with a buffer of type |type| and the user data provided by
+  // |read_cb_user_data|. |read_cb| is a synchronous function, so the data
+  // passed to it can safely be freed after |read_cb| returns. |read_cb| must be
+  // called exactly once, and it must be called before Read returns.
+  //
+  // An error can be handled in one of two ways:
+  // 1. Pass a null buffer to read_cb. This will cause the pipeline to handle
+  //    the situation as an error. Alternatively,
+  // 2. Pass an "end of stream" buffer to read_cb. This will cause the relevant
+  //    stream to end normally.
+  void (*Read)(CobaltExtensionDemuxerStreamType type,
+               CobaltExtensionDemuxerReadCB read_cb,
+               void* read_cb_user_data,
+               void* user_data);
+
+  // Returns true and populates |audio_config| if an audio stream is present;
+  // returns false otherwise. |config| must not be null.
+  bool (*GetAudioConfig)(CobaltExtensionDemuxerAudioDecoderConfig* config,
+                         void* user_data);
+
+  // Returns true and populates |video_config| if a video stream is present;
+  // returns false otherwise. |config| must not be null.
+  bool (*GetVideoConfig)(CobaltExtensionDemuxerVideoDecoderConfig* config,
+                         void* user_data);
+
+  // Returns the duration, in microseconds.
+  SbTime (*GetDuration)(void* user_data);
+
+  // Will be passed to all functions.
+  void* user_data;
+} CobaltExtensionDemuxer;
+
+typedef struct CobaltExtensionDemuxerDataSource {
+  // Reads up to |bytes_requested|, writing the data into |data| and returning
+  // the number of bytes read. |data| must be able to store at least
+  // |bytes_requested| bytes. Calling BlockingRead advances the read position.
+  int (*BlockingRead)(uint8_t* data, int bytes_requested, void* user_data);
+
+  // Seeks to |position| (specified in bytes) in the data source.
+  void (*SeekTo)(int position, void* user_data);
+
+  // Returns the offset into the data source, in bytes.
+  int64_t (*GetPosition)(void* user_data);
+
+  // Returns the size of the data source, in bytes.
+  int64_t (*GetSize)(void* user_data);
+
+  // Whether this represents a streaming data source.
+  bool is_streaming;
+
+  // Will be passed to all functions.
+  void* user_data;
+} CobaltExtensionDemuxerDataSource;
+
+typedef struct CobaltExtensionDemuxerApi {
+  // Name should be the string |kCobaltExtensionDemuxerApi|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Creates a demuxer for the content provided by |data_source|. Ownership of
+  // |data_source| is not transferred to this function.
+  //
+  // Ownership of the returned demuxer is transferred to the caller, but it must
+  // be deleted via DestroyDemuxer (below). The caller must not manually delete
+  // the demuxer.
+  CobaltExtensionDemuxer* (*CreateDemuxer)(
+      CobaltExtensionDemuxerDataSource* data_source,
+      CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
+      int64_t supported_audio_codecs_size,
+      CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
+      int64_t supported_video_codecs_size);
+
+  // Destroys |demuxer|. After calling this, |demuxer| must not be dereferenced
+  // or deleted by the caller.
+  void (*DestroyDemuxer)(CobaltExtensionDemuxer* demuxer);
+} CobaltExtensionDemuxerApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_DEMUXER_H_
diff --git a/starboard/extension/extension_test.cc b/starboard/extension/extension_test.cc
new file mode 100644
index 0000000..ec716ad
--- /dev/null
+++ b/starboard/extension/extension_test.cc
@@ -0,0 +1,386 @@
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cmath>
+
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/crash_handler.h"
+#include "starboard/extension/cwrappers.h"
+#include "starboard/extension/font.h"
+#include "starboard/extension/free_space.h"
+#include "starboard/extension/graphics.h"
+#include "starboard/extension/installation_manager.h"
+#include "starboard/extension/javascript_cache.h"
+#include "starboard/extension/media_session.h"
+#include "starboard/extension/memory_mapped_file.h"
+#include "starboard/extension/platform_service.h"
+#include "starboard/extension/updater_notification.h"
+#include "starboard/extension/url_fetcher_observer.h"
+#include "starboard/system.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace extension {
+
+TEST(ExtensionTest, PlatformService) {
+  typedef CobaltExtensionPlatformServiceApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionPlatformServiceName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 3u);
+  EXPECT_NE(extension_api->Has, nullptr);
+  EXPECT_NE(extension_api->Open, nullptr);
+  EXPECT_NE(extension_api->Close, nullptr);
+  EXPECT_NE(extension_api->Send, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, Graphics) {
+  typedef CobaltExtensionGraphicsApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionGraphicsName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 6u);
+
+  EXPECT_NE(extension_api->GetMaximumFrameIntervalInMilliseconds, nullptr);
+  float maximum_frame_interval =
+      extension_api->GetMaximumFrameIntervalInMilliseconds();
+  EXPECT_FALSE(std::isnan(maximum_frame_interval));
+
+  if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->GetMinimumFrameIntervalInMilliseconds, nullptr);
+    float minimum_frame_interval =
+        extension_api->GetMinimumFrameIntervalInMilliseconds();
+    EXPECT_GT(minimum_frame_interval, 0);
+  }
+
+  if (extension_api->version >= 3) {
+    EXPECT_NE(extension_api->IsMapToMeshEnabled, nullptr);
+  }
+
+  if (extension_api->version >= 4) {
+    EXPECT_NE(extension_api->ShouldClearFrameOnShutdown, nullptr);
+    float clear_color_r, clear_color_g, clear_color_b, clear_color_a;
+    if (extension_api->ShouldClearFrameOnShutdown(
+            &clear_color_r, &clear_color_g, &clear_color_b, &clear_color_a)) {
+      EXPECT_GE(clear_color_r, 0.0f);
+      EXPECT_LE(clear_color_r, 1.0f);
+      EXPECT_GE(clear_color_g, 0.0f);
+      EXPECT_LE(clear_color_g, 1.0f);
+      EXPECT_GE(clear_color_b, 0.0f);
+      EXPECT_LE(clear_color_b, 1.0f);
+      EXPECT_GE(clear_color_a, 0.0f);
+      EXPECT_LE(clear_color_a, 1.0f);
+    }
+  }
+
+  if (extension_api->version >= 5) {
+    EXPECT_NE(extension_api->GetMapToMeshColorAdjustments, nullptr);
+    EXPECT_NE(extension_api->GetRenderRootTransform, nullptr);
+  }
+
+  if (extension_api->version >= 6) {
+    EXPECT_NE(extension_api->ReportFullyDrawn, nullptr);
+  }
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, InstallationManager) {
+  typedef CobaltExtensionInstallationManagerApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionInstallationManagerName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 3u);
+  EXPECT_NE(extension_api->GetCurrentInstallationIndex, nullptr);
+  EXPECT_NE(extension_api->MarkInstallationSuccessful, nullptr);
+  EXPECT_NE(extension_api->RequestRollForwardToInstallation, nullptr);
+  EXPECT_NE(extension_api->GetInstallationPath, nullptr);
+  EXPECT_NE(extension_api->SelectNewInstallationIndex, nullptr);
+  EXPECT_NE(extension_api->GetAppKey, nullptr);
+  EXPECT_NE(extension_api->GetMaxNumberInstallations, nullptr);
+  EXPECT_NE(extension_api->ResetInstallation, nullptr);
+  EXPECT_NE(extension_api->Reset, nullptr);
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, Configuration) {
+  typedef CobaltExtensionConfigurationApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionConfigurationName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 3u);
+  EXPECT_NE(extension_api->CobaltUserOnExitStrategy, nullptr);
+  EXPECT_NE(extension_api->CobaltRenderDirtyRegionOnly, nullptr);
+  EXPECT_NE(extension_api->CobaltEglSwapInterval, nullptr);
+  EXPECT_NE(extension_api->CobaltFallbackSplashScreenUrl, nullptr);
+  EXPECT_NE(extension_api->CobaltEnableQuic, nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltOffscreenTargetCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltEncodedImageCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltImageCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltLocalTypefaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltRemoteTypefaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltMeshCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltSoftwareSurfaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltImageCacheCapacityMultiplierWhenPlayingVideo,
+            nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasWidth, nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasHeight, nullptr);
+  EXPECT_NE(extension_api->CobaltJsGarbageCollectionThresholdInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltReduceCpuMemoryBy, nullptr);
+  EXPECT_NE(extension_api->CobaltReduceGpuMemoryBy, nullptr);
+  EXPECT_NE(extension_api->CobaltGcZeal, nullptr);
+  if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->CobaltFallbackSplashScreenTopics, nullptr);
+  }
+
+  if (extension_api->version >= 3) {
+    EXPECT_NE(extension_api->CobaltCanStoreCompiledJavascript, nullptr);
+  }
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, MediaSession) {
+  typedef CobaltExtensionMediaSessionApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionMediaSessionName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->OnMediaSessionStateChanged, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, CrashHandler) {
+  typedef CobaltExtensionCrashHandlerApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionCrashHandlerName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 2u);
+  EXPECT_NE(extension_api->OverrideCrashpadAnnotations, nullptr);
+
+  if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->SetString, nullptr);
+  }
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, CWrappers) {
+  typedef CobaltExtensionCWrappersApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionCWrappersName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->PowWrapper, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, Font) {
+  typedef CobaltExtensionFontApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionFontName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->GetPathFallbackFontDirectory, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, JavaScriptCache) {
+  typedef CobaltExtensionJavaScriptCacheApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionJavaScriptCacheName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->GetCachedScript, nullptr);
+  EXPECT_NE(extension_api->ReleaseCachedScriptData, nullptr);
+  EXPECT_NE(extension_api->StoreCachedScript, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, UrlFetcherObserver) {
+  typedef CobaltExtensionUrlFetcherObserverApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionUrlFetcherObserverName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->FetcherCreated, nullptr);
+  EXPECT_NE(extension_api->FetcherDestroyed, nullptr);
+  EXPECT_NE(extension_api->StartURLRequest, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, UpdaterNotification) {
+  typedef CobaltExtensionUpdaterNotificationApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionUpdaterNotificationName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->UpdaterState, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, MemoryMappedFile) {
+  typedef CobaltExtensionMemoryMappedFileApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionMemoryMappedFileName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->MemoryMapFile, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
+TEST(ExtensionTest, FreeSpace) {
+  typedef CobaltExtensionFreeSpaceApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionFreeSpaceName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->MeasureFreeSpace, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+}  // namespace extension
+}  // namespace cobalt
diff --git a/starboard/extension/font.h b/starboard/extension/font.h
new file mode 100644
index 0000000..d181c48
--- /dev/null
+++ b/starboard/extension/font.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_FONT_H_
+#define STARBOARD_EXTENSION_FONT_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionFontName "dev.cobalt.extension.Font"
+
+typedef struct CobaltExtensionFontApi {
+  // Name should be the string |kCobaltExtensionFontName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Provide additional font directory for fonts not available
+  // as system or Cobalt fonts. This is useful for adding local fallback fonts.
+  bool (*GetPathFallbackFontDirectory)(char* path, int path_size);
+} CobaltExtensionFontApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_FONT_H_
diff --git a/starboard/extension/free_space.h b/starboard/extension/free_space.h
new file mode 100644
index 0000000..ea2684c
--- /dev/null
+++ b/starboard/extension/free_space.h
@@ -0,0 +1,48 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_FREE_SPACE_H_
+#define STARBOARD_EXTENSION_FREE_SPACE_H_
+
+#include <stdint.h>
+
+#include "starboard/system.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionFreeSpaceName "dev.cobalt.extension.FreeSpace"
+
+typedef struct CobaltExtensionFreeSpaceApi {
+  // Name should be the string |kCobaltExtensionFreeSpaceName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Returns the free space in bytes for the provided |system_path_id|.
+  // If there is no implementation for the that |system_path_id| or
+  // if there was an error -1 is returned.
+  int64_t (*MeasureFreeSpace)(SbSystemPathId system_path_id);
+} CobaltExtensionFreeSpaceApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_FREE_SPACE_H_
diff --git a/starboard/extension/graphics.h b/starboard/extension/graphics.h
new file mode 100644
index 0000000..a901d51
--- /dev/null
+++ b/starboard/extension/graphics.h
@@ -0,0 +1,128 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_GRAPHICS_H_
+#define STARBOARD_EXTENSION_GRAPHICS_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionGraphicsName "dev.cobalt.extension.Graphics"
+
+// This structure allows post-processing of output colors for 360 videos.
+// Given "rgba" is the color that the pixel shader calculates, this struct
+// allows additional processing in the form of:
+//   final color = rgba0_scale +
+//                 rgba1_scale * rgba +
+//                 rgba2_scale * rgba * rgba +
+//                 rgba3_scale * rgba * rgba * rgba;
+// The final_color is then clamped to the range of [0,1] for each element.
+typedef struct CobaltExtensionGraphicsMapToMeshColorAdjustment {
+  float rgba0_scale[4];  // multiplier for rgba^0
+  float rgba1_scale[4];  // multiplier for rgba^1
+  float rgba2_scale[4];  // multiplier for rgba^2
+  float rgba3_scale[4];  // multiplier for rgba^3
+} CobaltExtensionGraphicsMapToMeshColorAdjustment;
+
+typedef struct CobaltExtensionGraphicsApi {
+  // Name should be the string kCobaltExtensionGraphicsName.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Get the maximum time between rendered frames. This value can be dynamic
+  // and is queried periodically. This can be used to force the rasterizer to
+  // present a new frame even if nothing has changed visually. Due to the
+  // imprecision of thread scheduling, it may be necessary to specify a lower
+  // interval time to ensure frames aren't skipped when the throttling logic
+  // is executed a little too early. Return a negative number if frames should
+  // only be presented when something changes (i.e. there is no maximum frame
+  // interval).
+  // NOTE: The gyp variable 'cobalt_minimum_frame_time_in_milliseconds' takes
+  // precedence over this. For example, if the minimum frame time is 8ms and
+  // the maximum frame interval is 0ms, then the renderer will target 125 fps.
+  float (*GetMaximumFrameIntervalInMilliseconds)();
+
+  // The fields below this point were added in version 2 or later.
+
+  // Allow throttling of the frame rate. This is expressed in terms of
+  // milliseconds and can be a floating point number. Keep in mind that
+  // swapping frames may take some additional processing time, so it may be
+  // better to specify a lower delay. For example, '33' instead of '33.33'
+  // for 30 Hz refresh.
+  float (*GetMinimumFrameIntervalInMilliseconds)();
+
+  // The fields below this point were added in version 3 or later.
+
+  // Get whether the renderer should support 360 degree video or not.
+  bool (*IsMapToMeshEnabled)();
+
+  // The fields below this point were added in version 4 or later.
+
+  // Specify whether the framebuffer should be cleared when the graphics
+  // system is shutdown and color to use for clearing. The graphics system
+  // is shutdown on suspend or exit. The clear color values should be in the
+  // range of [0,1]; color values are only used if this function returns true.
+  //
+  // The default behavior is to clear to opaque black on shutdown unless this
+  // API specifies otherwise.
+  bool (*ShouldClearFrameOnShutdown)(float* clear_color_red,
+                                     float* clear_color_green,
+                                     float* clear_color_blue,
+                                     float* clear_color_alpha);
+
+  // The fields below this point were added in version 5 or later.
+
+  // Use the provided color adjustments for 360 videos if the function returns
+  // true. See declaration of CobaltExtensionGraphicsMapToMeshColorAdjustment
+  // for details.
+  bool (*GetMapToMeshColorAdjustments)(
+      CobaltExtensionGraphicsMapToMeshColorAdjustment* adjustment);
+
+  // This function can be used to insert a custom transform at the root of
+  // rendering. This allows custom scaling, rotating, etc. of the frame. This
+  // only impacts rendering of the frame -- the web app will not know about this
+  // transform, so it may not layout elements appropriately. This function
+  // should return true if a custom transform should be used.
+  bool (*GetRenderRootTransform)(float* m00,
+                                 float* m01,
+                                 float* m02,
+                                 float* m10,
+                                 float* m11,
+                                 float* m12,
+                                 float* m20,
+                                 float* m21,
+                                 float* m22);
+
+  // The fields below this point were added in version 6 or later.
+
+  // This function is called when the web app reports that it should be drawn.
+  void (*ReportFullyDrawn)();
+
+} CobaltExtensionGraphicsApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_GRAPHICS_H_
diff --git a/starboard/extension/installation_manager.h b/starboard/extension/installation_manager.h
new file mode 100644
index 0000000..09611cf
--- /dev/null
+++ b/starboard/extension/installation_manager.h
@@ -0,0 +1,63 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_INSTALLATION_MANAGER_H_
+#define STARBOARD_EXTENSION_INSTALLATION_MANAGER_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+
+#define IM_EXT_MAX_APP_KEY_LENGTH 1024
+#define IM_EXT_INVALID_INDEX -1
+#define IM_EXT_ERROR -1
+#define IM_EXT_SUCCESS 0
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionInstallationManagerName \
+  "dev.cobalt.extension.InstallationManager"
+
+typedef struct CobaltExtensionInstallationManagerApi {
+  // Name should be the string kCobaltExtensionInstallationManagerName.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // Installation Manager API wrapper.
+  // For more details, check:
+  //  starboard/loader_app/installation_manager.h
+
+  int (*GetCurrentInstallationIndex)();
+  int (*MarkInstallationSuccessful)(int installation_index);
+  int (*RequestRollForwardToInstallation)(int installation_index);
+  int (*GetInstallationPath)(int installation_index,
+                             char* path,
+                             int path_length);
+  int (*SelectNewInstallationIndex)();
+  int (*GetAppKey)(char* app_key, int app_key_length);
+  int (*GetMaxNumberInstallations)();
+  int (*ResetInstallation)(int installation_index);
+  int (*Reset)();
+} CobaltExtensionInstallationManagerApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_INSTALLATION_MANAGER_H_
diff --git a/starboard/extension/javascript_cache.h b/starboard/extension/javascript_cache.h
new file mode 100644
index 0000000..730c3f3
--- /dev/null
+++ b/starboard/extension/javascript_cache.h
@@ -0,0 +1,65 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_JAVASCRIPT_CACHE_H_
+#define STARBOARD_EXTENSION_JAVASCRIPT_CACHE_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionJavaScriptCacheName \
+  "dev.cobalt.extension.JavaScriptCache"
+
+// The implementation must be thread-safe as the extension would
+// be called from different threads. Also all storage management
+// is delegated to the platform.
+typedef struct CobaltExtensionJavaScriptCacheApi {
+  // Name should be the string |kCobaltExtensionJavaScriptCacheName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Retrieves the cached data for a script using |key|. The |source_length|
+  // provides the actual size of the script source in bytes.  The cached script
+  // bytes will be returned in |cache_data_out|. After the |cache_data| is
+  // processed the memory should be released by calling
+  // |ReleaseScriptCacheData|.
+  bool (*GetCachedScript)(uint32_t key,
+                          int source_length,
+                          const uint8_t** cache_data_out,
+                          int* cache_data_length);
+
+  // Releases the memory allocated for the |cache_data| by |GetCachedScript|.
+  void (*ReleaseCachedScriptData)(const uint8_t* cache_data);
+
+  // Stores the cached data for |key|.
+  bool (*StoreCachedScript)(uint32_t key,
+                            int source_length,
+                            const uint8_t* cache_data,
+                            int cache_data_length);
+} CobaltExtensionJavaScriptCacheApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_JAVASCRIPT_CACHE_H_
diff --git a/starboard/extension/media_session.h b/starboard/extension/media_session.h
new file mode 100644
index 0000000..5dd9375
--- /dev/null
+++ b/starboard/extension/media_session.h
@@ -0,0 +1,139 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_MEDIA_SESSION_H_
+#define STARBOARD_EXTENSION_MEDIA_SESSION_H_
+
+#include "starboard/configuration.h"
+#include "starboard/time.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionMediaSessionName "dev.cobalt.extension.MediaSession"
+
+typedef enum CobaltExtensionMediaSessionPlaybackState {
+  kCobaltExtensionMediaSessionNone = 0,
+  kCobaltExtensionMediaSessionPaused = 1,
+  kCobaltExtensionMediaSessionPlaying = 2
+} CobaltExtensionMediaSessionPlaybackState;
+
+typedef enum CobaltExtensionMediaSessionAction {
+  kCobaltExtensionMediaSessionActionPlay,
+  kCobaltExtensionMediaSessionActionPause,
+  kCobaltExtensionMediaSessionActionSeekbackward,
+  kCobaltExtensionMediaSessionActionSeekforward,
+  kCobaltExtensionMediaSessionActionPrevioustrack,
+  kCobaltExtensionMediaSessionActionNexttrack,
+  kCobaltExtensionMediaSessionActionSkipad,
+  kCobaltExtensionMediaSessionActionStop,
+  kCobaltExtensionMediaSessionActionSeekto,
+
+  // Not part of spec, but used in Cobalt implementation.
+  kCobaltExtensionMediaSessionActionNumActions,
+} CobaltExtensionMediaSessionAction;
+
+typedef struct CobaltExtensionMediaImage {
+  // These fields are null-terminated strings copied over from IDL.
+  const char* size;
+  const char* src;
+  const char* type;
+} CobaltExtensionMediaImage;
+
+typedef struct CobaltExtensionMediaMetadata {
+  // These fields are null-terminated strings copied over from IDL.
+  const char* album;
+  const char* artist;
+  const char* title;
+
+  CobaltExtensionMediaImage* artwork;
+  size_t artwork_count;
+} CobaltExtensionMediaMetadata;
+
+typedef struct CobaltExtensionMediaSessionActionDetails {
+  CobaltExtensionMediaSessionAction action;
+
+  // Seek time/offset are non-negative. Negative value signifies "unset".
+  double seek_offset;
+  double seek_time;
+
+  bool fast_seek;
+} CobaltExtensionMediaSessionActionDetails;
+
+typedef void (*CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback)(
+    CobaltExtensionMediaSessionPlaybackState state,
+    void* callback_context);
+typedef void (*CobaltExtensionMediaSessionInvokeActionCallback)(
+    CobaltExtensionMediaSessionActionDetails details,
+    void* callback_context);
+
+// This struct and all its members should only be used for piping data to each
+// platform's implementation of OnMediaSessionStateChanged and they are only
+// valid within the scope of that function. Any data inside must be copied if it
+// will be referenced later.
+typedef struct CobaltExtensionMediaSessionState {
+  SbTimeMonotonic duration;
+  CobaltExtensionMediaSessionPlaybackState actual_playback_state;
+  bool available_actions[kCobaltExtensionMediaSessionActionNumActions];
+  CobaltExtensionMediaMetadata* metadata;
+  double actual_playback_rate;
+  SbTimeMonotonic current_playback_position;
+  bool has_position_state;
+} CobaltExtensionMediaSessionState;
+
+typedef struct CobaltExtensionMediaSessionApi {
+  // Name should be the string kCobaltExtensionMediaSessionName.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // MediaSession API Wrapper.
+
+  void (*OnMediaSessionStateChanged)(
+      CobaltExtensionMediaSessionState session_state);
+
+  // Register MediaSessionClient callbacks when the platform create a new
+  // MediaSessionClient.
+  void (*RegisterMediaSessionCallbacks)(
+      void* callback_context,
+      CobaltExtensionMediaSessionInvokeActionCallback invoke_action_callback,
+      CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback
+          update_platform_playback_state_callback);
+
+  // Destroy platform's MediaSessionClient after the Cobalt's
+  // MediaSessionClient has been destroyed.
+  void (*DestroyMediaSessionClientCallback)();
+
+  // Starboard method for updating playback state.
+  void (*UpdateActiveSessionPlatformPlaybackState)(
+      CobaltExtensionMediaSessionPlaybackState state);
+} CobaltExtensionMediaSessionApi;
+
+inline void CobaltExtensionMediaSessionActionDetailsInit(
+    CobaltExtensionMediaSessionActionDetails* details,
+    CobaltExtensionMediaSessionAction action) {
+  details->action = action;
+  details->seek_offset = -1.0;
+  details->seek_time = -1.0;
+  details->fast_seek = false;
+}
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_MEDIA_SESSION_H_
diff --git a/starboard/extension/memory_mapped_file.h b/starboard/extension/memory_mapped_file.h
new file mode 100644
index 0000000..de5e2dd
--- /dev/null
+++ b/starboard/extension/memory_mapped_file.h
@@ -0,0 +1,58 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_MEMORY_MAPPED_FILE_H_
+#define STARBOARD_EXTENSION_MEMORY_MAPPED_FILE_H_
+
+#include <stdint.h>
+
+#include "starboard/memory.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionMemoryMappedFileName \
+  "dev.cobalt.extension.MemoryMappedFile"
+
+typedef struct CobaltExtensionMemoryMappedFileApi {
+  // Name should be the string |kCobaltExtensionMemoryMappedFileName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Memory maps a file at the specified |address| starting at |file_offset|
+  // and  mapping |size| bytes. The |address| argument can be NULL in which
+  // case new memory buffer will be allocated. If a non NULL |address| is
+  // passed the memory should be resreved in advance through |SbMemoryMap|.
+  // To release the memory call |SbMemoryUnmap|.
+  // The |file_offset| must be a multiple of |kSbMemoryPageSize|.
+  // Returns NULL or error.
+  void* (*MemoryMapFile)(void* address,
+                         const char* path,
+                         SbMemoryMapFlags flags,
+                         int64_t file_offset,
+                         int64_t size);
+
+} CobaltExtensionMemoryMappedFileApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_MEMORY_MAPPED_FILE_H_
diff --git a/starboard/extension/on_screen_keyboard.h b/starboard/extension/on_screen_keyboard.h
new file mode 100644
index 0000000..63c101a
--- /dev/null
+++ b/starboard/extension/on_screen_keyboard.h
@@ -0,0 +1,51 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_ON_SCREEN_KEYBOARD_H_
+#define STARBOARD_EXTENSION_ON_SCREEN_KEYBOARD_H_
+
+#include "starboard/system.h"
+#include "starboard/window.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionOnScreenKeyboardName \
+  "dev.cobalt.extension.OnScreenKeyboard"
+
+typedef struct CobaltExtensionOnScreenKeyboardApi {
+  // Name should be the string
+  // |kCobaltExtensionOnScreenKeyboardName|. This helps to validate that
+  // the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // This function overrides the background color of on-screen keyboard in RGB
+  // color space, where r, g, b are between 0 and 255.
+  void (*SetBackgroundColor)(SbWindow window, uint8_t r, uint8_t g, uint8_t b);
+
+  // This function overrides the light theme of on-screen keyboard.
+  void (*SetLightTheme)(SbWindow window, bool light_theme);
+} CobaltExtensionOnScreenKeyboardApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_ON_SCREEN_KEYBOARD_H_
diff --git a/starboard/extension/platform_service.h b/starboard/extension/platform_service.h
new file mode 100644
index 0000000..aec2f4c
--- /dev/null
+++ b/starboard/extension/platform_service.h
@@ -0,0 +1,94 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_PLATFORM_SERVICE_H_
+#define STARBOARD_EXTENSION_PLATFORM_SERVICE_H_
+
+#include <stdint.h>
+
+#include "starboard/configuration.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct CobaltExtensionPlatformServicePrivate
+    CobaltExtensionPlatformServicePrivate;
+typedef CobaltExtensionPlatformServicePrivate* CobaltExtensionPlatformService;
+
+// Well-defined value for an invalid |Service|.
+#define kCobaltExtensionPlatformServiceInvalid \
+  ((CobaltExtensionPlatformService)0)
+
+#define kCobaltExtensionPlatformServiceName \
+  "dev.cobalt.extension.PlatformService"
+
+// Checks whether a |CobaltExtensionPlatformService| is valid.
+static SB_C_INLINE bool CobaltExtensionPlatformServiceIsValid(
+    CobaltExtensionPlatformService service) {
+  return service != kCobaltExtensionPlatformServiceInvalid;
+}
+
+// When a client receives a message from a service, the service will be passed
+// in as the |context| here, with |data|, which has length |length|.
+typedef void (*ReceiveMessageCallback)(void* context,
+                                       const void* data,
+                                       uint64_t length);
+
+typedef struct CobaltExtensionPlatformServiceApi {
+  // Name should be the string kCobaltExtensionPlatformServiceName.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Return whether the platform has service indicated by |name|.
+  bool (*Has)(const char* name);
+
+  // Open and return a service by name.
+  //
+  // |context|: pointer to context object for callback.
+  // |name|: name of the service.
+  // |receive_callback|: callback to run when Cobalt should receive data.
+  CobaltExtensionPlatformService (*Open)(
+      void* context,
+      const char* name,
+      ReceiveMessageCallback receive_callback);
+
+  // Close the service passed in as |service|.
+  void (*Close)(CobaltExtensionPlatformService service);
+
+  // Send |data| of length |length| to |service|. If there is a synchronous
+  // response, it will be returned via void* and |output_length| will be set
+  // to its length. The returned void* will be owned by the caller, and must
+  // be deallocated via SbMemoryDeallocate() by the caller when appropriate.
+  // If there is no synchronous response, NULL will be returned and
+  // |output_length| will be 0. The |invalid_state| will be set to true if the
+  // service is not currently able to accept data, and otherwise will be set to
+  // false.
+  void* (*Send)(CobaltExtensionPlatformService service,
+                void* data,
+                uint64_t length,
+                uint64_t* output_length,
+                bool* invalid_state);
+} CobaltExtensionPlatformServiceApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_PLATFORM_SERVICE_H_
diff --git a/starboard/extension/updater_notification.h b/starboard/extension/updater_notification.h
new file mode 100644
index 0000000..8d9521f
--- /dev/null
+++ b/starboard/extension/updater_notification.h
@@ -0,0 +1,68 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_UPDATER_NOTIFICATION_H_
+#define STARBOARD_EXTENSION_UPDATER_NOTIFICATION_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionUpdaterNotificationName \
+  "dev.cobalt.extension.UpdaterNotification"
+
+typedef enum CobaltExtensionUpdaterNotificationState {
+  kCobaltExtensionUpdaterNotificationStateNone = 0,
+  kCobaltExtensionUpdaterNotificationStateChecking = 1,
+  kCobaltExtensionUpdaterNotificationStateUpdateAvailable = 2,
+  kCobaltExtensionUpdaterNotificationStateDownloading = 3,
+  kCobaltExtensionUpdaterNotificationStateDownloaded = 4,
+  kCobaltExtensionUpdaterNotificationStateInstalling = 5,
+#if SB_API_VERSION > 13
+  kCobaltExtensionUpdaterNotificationStateUpdated = 6,
+  kCobaltExtensionUpdaterNotificationStateUpToDate = 7,
+  kCobaltExtensionUpdaterNotificationStateUpdateFailed = 8,
+#else
+  kCobaltExtensionUpdaterNotificationStatekUpdated = 6,
+  kCobaltExtensionUpdaterNotificationStatekUpToDate = 7,
+  kCobaltExtensionUpdaterNotificationStatekUpdateFailed = 8,
+#endif
+} CobaltExtensionUpdaterNotificationState;
+
+typedef struct CobaltExtensionUpdaterNotificationApi {
+  // Name should be the string |kCobaltExtensionUpdaterNotificationName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Notify the Starboard implementation of the updater state.
+  // The Starboard platform can check if the device is low on storage
+  // and prompt the user to free some storage. The implementation
+  // should keep track of the frequency of showing the prompt to the
+  // user and try to minimize the number of user notifications.
+  void (*UpdaterState)(CobaltExtensionUpdaterNotificationState state,
+                       const char* current_evergreen_version);
+} CobaltExtensionUpdaterNotificationApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_UPDATER_NOTIFICATION_H_
diff --git a/starboard/extension/url_fetcher_observer.h b/starboard/extension/url_fetcher_observer.h
new file mode 100644
index 0000000..a5b8411
--- /dev/null
+++ b/starboard/extension/url_fetcher_observer.h
@@ -0,0 +1,54 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_URL_FETCHER_OBSERVER_H_
+#define STARBOARD_EXTENSION_URL_FETCHER_OBSERVER_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define URL_FETCHER_OBSERVER_MAX_URL_SIZE 128
+#define URL_FETCHER_COMMAND_LINE_SWITCH "url_fetcher_observer"
+
+#define kCobaltExtensionUrlFetcherObserverName \
+  "dev.cobalt.extension.UrlFetcherObserver"
+
+typedef struct CobaltExtensionUrlFetcherObserverApi {
+  // Name should be the string |kCobaltExtensionUrlFetcherObserverName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // The UrlFetcher for the specified |url| was created.
+  void (*FetcherCreated)(const char* url);
+
+  // The UrlFetcher for the specified |url| was destroyed.
+  void (*FetcherDestroyed)(const char* url);
+
+  // The URL request started for the specified |url|.
+  void (*StartURLRequest)(const char* url);
+} CobaltExtensionUrlFetcherObserverApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_URL_FETCHER_OBSERVER_H_
diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn
index 8a087b6..ec8ce53 100644
--- a/starboard/linux/shared/BUILD.gn
+++ b/starboard/linux/shared/BUILD.gn
@@ -27,6 +27,7 @@
 
   deps = [
     "//third_party/libevent",
+    "//third_party/modp_b64",
     "//third_party/opus",
   ]
 
@@ -83,6 +84,7 @@
     "//starboard/shared/alsa/alsa_util.cc",
     "//starboard/shared/alsa/alsa_util.h",
     "//starboard/shared/deviceauth/deviceauth_internal.cc",
+    "//starboard/shared/deviceauth/deviceauth_internal.h",
     "//starboard/shared/egl/system_egl.cc",
     "//starboard/shared/gcc/atomic_gcc_public.h",
     "//starboard/shared/gles/system_gles2.cc",
@@ -137,6 +139,10 @@
     "//starboard/shared/libevent/socket_waiter_wait.cc",
     "//starboard/shared/libevent/socket_waiter_wait_timed.cc",
     "//starboard/shared/libevent/socket_waiter_wake_up.cc",
+    "//starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc",
+    "//starboard/shared/libfdkaac/fdk_aac_audio_decoder.h",
+    "//starboard/shared/libfdkaac/libfdkaac_library_loader.cc",
+    "//starboard/shared/libfdkaac/libfdkaac_library_loader.h",
     "//starboard/shared/libvpx/vpx_video_decoder.cc",
     "//starboard/shared/libvpx/vpx_video_decoder.h",
     "//starboard/shared/linux/byte_swap.cc",
diff --git a/starboard/linux/shared/configuration.cc b/starboard/linux/shared/configuration.cc
index 0293a00..31d8ac2 100644
--- a/starboard/linux/shared/configuration.cc
+++ b/starboard/linux/shared/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/linux/shared/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 // Omit namespace linux due to symbol name conflict.
 namespace starboard {
diff --git a/starboard/linux/shared/launcher.py b/starboard/linux/shared/launcher.py
index 0e90bc9..f0b0751 100644
--- a/starboard/linux/shared/launcher.py
+++ b/starboard/linux/shared/launcher.py
@@ -28,6 +28,9 @@
 
 STATUS_CHANGE_TIMEOUT = 15
 
+# This file is still executed with Python2 in CI.
+# pylint:disable=consider-using-f-string,super-with-arguments
+
 
 def GetProcessStatus(pid):
   """Returns process running status given its pid, or empty string if not found.
@@ -46,6 +49,8 @@
   def __init__(self, platform, target_name, config, device_id, **kwargs):
     super(Launcher, self).__init__(platform, target_name, config, device_id,
                                    **kwargs)
+    # Starts should be generally quick on Linux, default is 2 minutes
+    self.startup_timeout_seconds = 15
     if self.device_id:
       self.device_ip = self.device_id
     else:
diff --git a/starboard/linux/shared/player_components_factory.cc b/starboard/linux/shared/player_components_factory.cc
index 4dcc0ca..7440b40 100644
--- a/starboard/linux/shared/player_components_factory.cc
+++ b/starboard/linux/shared/player_components_factory.cc
@@ -22,6 +22,8 @@
 #include "starboard/shared/ffmpeg/ffmpeg_video_decoder.h"
 #include "starboard/shared/libdav1d/dav1d_video_decoder.h"
 #include "starboard/shared/libde265/de265_video_decoder.h"
+#include "starboard/shared/libfdkaac/fdk_aac_audio_decoder.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
 #include "starboard/shared/libvpx/vpx_video_decoder.h"
 #include "starboard/shared/openh264/openh264_library_loader.h"
 #include "starboard/shared/openh264/openh264_video_decoder.h"
@@ -65,6 +67,8 @@
 
       typedef ::starboard::shared::ffmpeg::AudioDecoder FfmpegAudioDecoder;
       typedef ::starboard::shared::opus::OpusAudioDecoder OpusAudioDecoder;
+      typedef ::starboard::shared::libfdkaac::FdkAacAudioDecoder
+          FdkAacAudioDecoder;
 
       auto decoder_creator = [](const SbMediaAudioSampleInfo& audio_sample_info,
                                 SbDrmSystem drm_system) {
@@ -74,11 +78,18 @@
           if (audio_decoder_impl->is_valid()) {
             return audio_decoder_impl.PassAs<AudioDecoder>();
           }
+        } else if (audio_sample_info.codec == kSbMediaAudioCodecAac &&
+                   audio_sample_info.number_of_channels <=
+                       FdkAacAudioDecoder::kMaxChannels &&
+                   libfdkaac::LibfdkaacHandle::GetHandle()->IsLoaded()) {
+          SB_LOG(INFO) << "Playing audio using FdkAacAudioDecoder.";
+          return scoped_ptr<AudioDecoder>(new FdkAacAudioDecoder());
         } else {
           scoped_ptr<FfmpegAudioDecoder> audio_decoder_impl(
               FfmpegAudioDecoder::Create(audio_sample_info.codec,
                                          audio_sample_info));
           if (audio_decoder_impl && audio_decoder_impl->is_valid()) {
+            SB_LOG(INFO) << "Playing audio using FfmpegAudioDecoder";
             return audio_decoder_impl.PassAs<AudioDecoder>();
           }
         }
diff --git a/starboard/linux/shared/soft_mic_platform_service.cc b/starboard/linux/shared/soft_mic_platform_service.cc
index 15fe440..dbaa0d2 100644
--- a/starboard/linux/shared/soft_mic_platform_service.cc
+++ b/starboard/linux/shared/soft_mic_platform_service.cc
@@ -17,10 +17,10 @@
 #include <memory>
 #include <string>
 
-#include "cobalt/extension/platform_service.h"
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
 #include "starboard/configuration.h"
+#include "starboard/extension/platform_service.h"
 #include "starboard/shared/starboard/application.h"
 #if SB_IS(EVERGREEN_COMPATIBLE)
 #include "starboard/elf_loader/evergreen_config.h"
diff --git a/starboard/linux/shared/system_get_extensions.cc b/starboard/linux/shared/system_get_extensions.cc
index fba44bd..eafacf0 100644
--- a/starboard/linux/shared/system_get_extensions.cc
+++ b/starboard/linux/shared/system_get_extensions.cc
@@ -14,13 +14,13 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/crash_handler.h"
-#include "cobalt/extension/demuxer.h"
-#include "cobalt/extension/free_space.h"
-#include "cobalt/extension/memory_mapped_file.h"
-#include "cobalt/extension/platform_service.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/crash_handler.h"
+#include "starboard/extension/demuxer.h"
+#include "starboard/extension/free_space.h"
+#include "starboard/extension/memory_mapped_file.h"
+#include "starboard/extension/platform_service.h"
 #include "starboard/linux/shared/soft_mic_platform_service.h"
 #include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
 #include "starboard/shared/posix/free_space.h"
diff --git a/starboard/linux/x64x11/skia/configuration.cc b/starboard/linux/x64x11/skia/configuration.cc
index 01aacdc..5c6f80a 100644
--- a/starboard/linux/x64x11/skia/configuration.cc
+++ b/starboard/linux/x64x11/skia/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/linux/x64x11/skia/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 // Omit namespace linux due to symbol name conflict.
 namespace starboard {
diff --git a/starboard/linux/x64x11/skia/system_get_extensions.cc b/starboard/linux/x64x11/skia/system_get_extensions.cc
index 1bbc6f5..07b8147 100644
--- a/starboard/linux/x64x11/skia/system_get_extensions.cc
+++ b/starboard/linux/x64x11/skia/system_get_extensions.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
 #include "starboard/linux/x64x11/skia/configuration.h"
 
 const void* SbSystemGetExtension(const char* name) {
diff --git a/starboard/linux/x64x11/system_get_property_impl.cc b/starboard/linux/x64x11/system_get_property_impl.cc
index aa33def..7442064 100644
--- a/starboard/linux/x64x11/system_get_property_impl.cc
+++ b/starboard/linux/x64x11/system_get_property_impl.cc
@@ -14,6 +14,8 @@
 
 #include "starboard/linux/x64x11/system_get_property_impl.h"
 
+#include <string>
+
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
 #include "starboard/linux/x64x11/system_properties.h"
@@ -58,31 +60,53 @@
     return false;
   }
 
+  std::string env_value;
   switch (property_id) {
     case kSbSystemPropertyBrandName:
-      return CopyStringAndTestIfSuccess(out_value, value_length, kBrandName);
+      env_value = GetEnvironment("COBALT_TESTING_BRAND_NAME");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kBrandName : env_value.c_str());
     case kSbSystemPropertyCertificationScope:
       if (kCertificationScope[0] == '\0')
         return false;
       return CopyStringAndTestIfSuccess(out_value, value_length,
                                         kCertificationScope);
     case kSbSystemPropertyChipsetModelNumber:
-      return CopyStringAndTestIfSuccess(out_value, value_length,
-                                        kChipsetModelNumber);
+      env_value = GetEnvironment("COBALT_TESTING_CHIPSET_MODEL_NUMBER");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kChipsetModelNumber : env_value.c_str());
     case kSbSystemPropertyFirmwareVersion:
-      return CopyStringAndTestIfSuccess(out_value, value_length,
-                                        kFirmwareVersion);
+      env_value = GetEnvironment("COBALT_TESTING_FIRMWARE_VERSION");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kFirmwareVersion : env_value.c_str());
     case kSbSystemPropertyFriendlyName:
-      return CopyStringAndTestIfSuccess(out_value, value_length, kFriendlyName);
+      env_value = GetEnvironment("COBALT_TESTING_FRIENDLY_NAME");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kFriendlyName : env_value.c_str());
     case kSbSystemPropertyModelName:
-      return CopyStringAndTestIfSuccess(out_value, value_length, kModelName);
+      env_value = GetEnvironment("COBALT_TESTING_MODEL_NAME");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kModelName : env_value.c_str());
     case kSbSystemPropertyModelYear:
-      return CopyStringAndTestIfSuccess(out_value, value_length, kModelYear);
+      env_value = GetEnvironment("COBALT_TESTING_MODEL_YEAR");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kModelYear : env_value.c_str());
     case kSbSystemPropertyPlatformName:
-      return CopyStringAndTestIfSuccess(out_value, value_length, kPlatformName);
+      env_value = GetEnvironment("COBALT_TESTING_PLATFORM_NAME");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kPlatformName : env_value.c_str());
     case kSbSystemPropertySystemIntegratorName:
-      return CopyStringAndTestIfSuccess(out_value, value_length,
-                                        kSystemIntegratorName);
+      env_value = GetEnvironment("COBALT_TESTING_SYSTEM_INTEGRATOR_NAME");
+      return CopyStringAndTestIfSuccess(
+          out_value, value_length,
+          env_value.empty() ? kSystemIntegratorName : env_value.c_str());
     case kSbSystemPropertySpeechApiKey:
     case kSbSystemPropertyUserAgentAuxField:
       return false;
diff --git a/starboard/loader_app/BUILD.gn b/starboard/loader_app/BUILD.gn
index 0f80d8a..71c898c 100644
--- a/starboard/loader_app/BUILD.gn
+++ b/starboard/loader_app/BUILD.gn
@@ -38,6 +38,33 @@
   }
 }
 
+if (sb_is_evergreen_compatible && sb_evergreen_compatible_package) {
+  copy("copy_loader_app_content") {
+    install_content = true
+    if (target_cpu == "arm" && arm_float_abi == "softfp") {
+      sources = [ "$root_out_dir/../evergreen-$target_cpu-${arm_float_abi}_$build_type/content" ]
+    } else if (target_cpu == "arm64") {
+      sources = [ "$root_out_dir/../evergreen-$target_cpu_$build_type/content" ]
+    } else {
+      sources = []
+    }
+    outputs = [ "$sb_static_contents_output_data_dir/app/cobalt/content" ]
+  }
+  copy("copy_loader_app_lib") {
+    install_content = true
+    if (target_cpu == "arm" && arm_float_abi == "softfp") {
+      sources = [ "$root_out_dir/../evergreen-$target_cpu-${arm_float_abi}_$build_type/libcobalt.so" ]
+    } else if (target_cpu == "arm64") {
+      sources =
+          [ "$root_out_dir/../evergreen-$target_cpu_$build_type/libcobalt.so" ]
+    } else {
+      sources = []
+    }
+    outputs =
+        [ "$sb_static_contents_output_data_dir/app/cobalt/lib/libcobalt.so" ]
+  }
+}
+
 target(final_executable_type, "loader_app") {
   if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
       target_cpu == "arm64") {
@@ -56,13 +83,24 @@
       "//cobalt/content/fonts:copy_font_data",
       "//starboard/elf_loader",
     ]
+    if (sb_is_evergreen_compatible && sb_evergreen_compatible_package) {
+      data_deps += [
+        ":copy_loader_app_content",
+        ":copy_loader_app_lib",
+      ]
+      deps += [
+        ":copy_loader_app_content",
+        ":copy_loader_app_lib",
+      ]
+    }
   }
 }
 
 if (sb_is_evergreen_compatible) {
+  # TODO: b/261635039 enable this target on Android
   target(final_executable_type, "loader_app_sys") {
-    if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
-        target_cpu == "arm64") {
+    if ((target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
+         target_cpu == "arm64") && target_os != "android") {
       sources = _common_loader_app_sources
 
       starboard_syms_path =
diff --git a/starboard/loader_app/system_get_extension_shim.cc b/starboard/loader_app/system_get_extension_shim.cc
index 2389c20..6365854 100644
--- a/starboard/loader_app/system_get_extension_shim.cc
+++ b/starboard/loader_app/system_get_extension_shim.cc
@@ -17,8 +17,8 @@
 #include <cstring>
 #include <string>
 
-#include "cobalt/extension/installation_manager.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/installation_manager.h"
 #include "starboard/loader_app/installation_manager.h"
 #include "starboard/string.h"
 #include "starboard/system.h"
diff --git a/starboard/nplb/media_can_play_mime_and_key_system_test.cc b/starboard/nplb/media_can_play_mime_and_key_system_test.cc
index 16f8c28..e3746e5 100644
--- a/starboard/nplb/media_can_play_mime_and_key_system_test.cc
+++ b/starboard/nplb/media_can_play_mime_and_key_system_test.cc
@@ -757,10 +757,10 @@
 //       boost the queries of repeated mime and key system. Note that if there's
 //       any capability change, the platform need to explicitly clear the
 //       caches, otherwise they may return outdated results.
-TEST(SbMediaCanPlayMimeAndKeySystem, ValidatePerformance) {
+TEST(SbMediaCanPlayMimeAndKeySystem, FLAKY_ValidatePerformance) {
   auto test_sequential_function_calls =
       [](const SbMediaCanPlayMimeAndKeySystemParam* mime_params,
-         int num_function_calls, SbTimeMonotonic max_time_delta,
+         int num_function_calls, SbTimeMonotonic max_time_delta_per_call,
          const char* query_type) {
         const SbTimeMonotonic time_start = SbTimeGetMonotonicNow();
         for (int i = 0; i < num_function_calls; ++i) {
@@ -777,33 +777,32 @@
                      << "us total across " << num_function_calls << " calls.";
         SB_LOG(INFO) << "  Measured duration " << time_per_call
                      << "us average per call.";
-        EXPECT_LE(time_delta, max_time_delta);
+        EXPECT_LE(time_delta, max_time_delta_per_call * num_function_calls);
       };
 
   // Warmup the cache.
+  test_sequential_function_calls(kWarmupQueryParams,
+                                 SB_ARRAY_SIZE_INT(kWarmupQueryParams),
+                                 100 * kSbTimeMillisecond, "Warmup queries");
+
+  // First round of the queries.
   test_sequential_function_calls(
-      kWarmupQueryParams, SB_ARRAY_SIZE_INT(kWarmupQueryParams),
-      5 * kSbTimeMillisecond /* 9 calls */, "Warmup queries");
-  // First round of the queires.
+      kSdrQueryParams, SB_ARRAY_SIZE_INT(kSdrQueryParams), 500, "SDR queries");
   test_sequential_function_calls(
-      kSdrQueryParams, SB_ARRAY_SIZE_INT(kSdrQueryParams),
-      10 * kSbTimeMillisecond /* 38 calls */, "SDR queries");
+      kHdrQueryParams, SB_ARRAY_SIZE_INT(kHdrQueryParams), 500, "HDR queries");
   test_sequential_function_calls(
-      kHdrQueryParams, SB_ARRAY_SIZE_INT(kHdrQueryParams),
-      10 * kSbTimeMillisecond /* 82 calls */, "HDR queries");
-  test_sequential_function_calls(
-      kDrmQueryParams, SB_ARRAY_SIZE_INT(kDrmQueryParams),
-      10 * kSbTimeMillisecond /* 81 calls */, "DRM queries");
+      kDrmQueryParams, SB_ARRAY_SIZE_INT(kDrmQueryParams), 500, "DRM queries");
+
   // Second round of the queries.
-  test_sequential_function_calls(
-      kSdrQueryParams, SB_ARRAY_SIZE_INT(kSdrQueryParams),
-      5 * kSbTimeMillisecond /* 38 calls */, "Cached SDR queries");
-  test_sequential_function_calls(
-      kHdrQueryParams, SB_ARRAY_SIZE_INT(kHdrQueryParams),
-      5 * kSbTimeMillisecond /* 82 calls */, "Cached HDR queries");
-  test_sequential_function_calls(
-      kDrmQueryParams, SB_ARRAY_SIZE_INT(kDrmQueryParams),
-      5 * kSbTimeMillisecond /* 81 calls */, "Cached DRM queries");
+  test_sequential_function_calls(kSdrQueryParams,
+                                 SB_ARRAY_SIZE_INT(kSdrQueryParams), 100,
+                                 "Cached SDR queries");
+  test_sequential_function_calls(kHdrQueryParams,
+                                 SB_ARRAY_SIZE_INT(kHdrQueryParams), 100,
+                                 "Cached HDR queries");
+  test_sequential_function_calls(kDrmQueryParams,
+                                 SB_ARRAY_SIZE_INT(kDrmQueryParams), 100,
+                                 "Cached DRM queries");
 }
 
 }  // namespace
diff --git a/starboard/nplb/nplb_evergreen_compat_tests/crashpad_config_test.cc b/starboard/nplb/nplb_evergreen_compat_tests/crashpad_config_test.cc
index ca4ee47..2b07856 100644
--- a/starboard/nplb/nplb_evergreen_compat_tests/crashpad_config_test.cc
+++ b/starboard/nplb/nplb_evergreen_compat_tests/crashpad_config_test.cc
@@ -15,7 +15,7 @@
 #include <string>
 #include <vector>
 
-#include "cobalt/extension/crash_handler.h"
+#include "starboard/extension/crash_handler.h"
 #include "starboard/nplb/nplb_evergreen_compat_tests/checks.h"
 #include "starboard/system.h"
 #include "testing/gtest/include/gtest/gtest.h"
diff --git a/starboard/nplb/player_write_sample_test.cc b/starboard/nplb/player_write_sample_test.cc
index 1eca87f..65b3b19 100644
--- a/starboard/nplb/player_write_sample_test.cc
+++ b/starboard/nplb/player_write_sample_test.cc
@@ -113,9 +113,10 @@
       const SbTime timeout = kDefaultWaitForPlayerStateTimeout);
 
   // Player and Decoder methods for driving input and output.
-  void WriteSingleInput(size_t index);
+  void WriteSingleBatch(int start_index, int samples_to_write);
   void WriteEndOfStream();
-  void WriteMultipleInputs(size_t start_index, size_t num_inputs_to_write);
+  void WriteMultipleBatches(size_t start_index,
+                            size_t number_of_write_sample_calls);
   void DrainOutputs();
 
   int GetNumBuffers() const;
@@ -389,12 +390,26 @@
       << "Did not received expected state.";
 }
 
-void SbPlayerWriteSampleTest::WriteSingleInput(size_t index) {
+void SbPlayerWriteSampleTest::WriteSingleBatch(int start_index,
+                                               int samples_to_write) {
+  SB_DCHECK(samples_to_write > 0);
+  SB_DCHECK(start_index >= 0);
   ASSERT_FALSE(destroy_player_called_);
-  ASSERT_LT(index, GetNumBuffers());
-  SbPlayerSampleInfo sample_info =
-      dmp_reader_->GetPlayerSampleInfo(test_media_type_, index);
-  SbPlayerWriteSample2(player_, test_media_type_, &sample_info, 1);
+
+  int max_batch_size =
+      SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+  SB_DCHECK(max_batch_size > 0);
+  samples_to_write = std::min(samples_to_write, max_batch_size);
+
+  SB_DCHECK(start_index + samples_to_write <= GetNumBuffers());
+  // Prepare a batch writing.
+  std::vector<SbPlayerSampleInfo> sample_infos;
+  for (int i = 0; i < samples_to_write; ++i) {
+    sample_infos.push_back(
+        dmp_reader_->GetPlayerSampleInfo(test_media_type_, start_index++));
+  }
+  SbPlayerWriteSample2(player_, test_media_type_, sample_infos.data(),
+                       samples_to_write);
 }
 
 void SbPlayerWriteSampleTest::WriteEndOfStream() {
@@ -404,20 +419,29 @@
   SbPlayerWriteEndOfStream(player_, test_media_type_);
 }
 
-void SbPlayerWriteSampleTest::WriteMultipleInputs(size_t start_index,
-                                                  size_t num_inputs_to_write) {
-  SB_DCHECK(num_inputs_to_write > 0);
+void SbPlayerWriteSampleTest::WriteMultipleBatches(
+    size_t start_index,
+    size_t number_of_write_sample_calls) {
+  SB_DCHECK(number_of_write_sample_calls > 0);
   SB_DCHECK(start_index < GetNumBuffers());
+  ASSERT_FALSE(destroy_player_called_);
 
-  ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index));
-  ++start_index;
-  --num_inputs_to_write;
+  int max_batch_size =
+      SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+  SB_DCHECK(max_batch_size > 0);
 
-  while (num_inputs_to_write > 0 && start_index < GetNumBuffers()) {
+  int num_inputs_to_write = max_batch_size;
+  int sample_index = start_index;
+  for (int i = 0; i < number_of_write_sample_calls; ++i) {
+    if (sample_index + num_inputs_to_write > GetNumBuffers()) {
+      break;
+    }
+    ASSERT_NO_FATAL_FAILURE(
+        WriteSingleBatch(sample_index, num_inputs_to_write));
     ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
-    ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index));
-    ++start_index;
-    --num_inputs_to_write;
+    sample_index = sample_index + num_inputs_to_write;
+    num_inputs_to_write =
+        (num_inputs_to_write == 1) ? max_batch_size : num_inputs_to_write - 1;
   }
 }
 
@@ -504,17 +528,19 @@
   DrainOutputs();
 }
 
-TEST_P(SbPlayerWriteSampleTest, SingleInput) {
-  ASSERT_NO_FATAL_FAILURE(WriteSingleInput(0));
+// A single call to write a batch consists of multiple samples.
+TEST_P(SbPlayerWriteSampleTest, WriteSingleBatch) {
+  int max_batch_size =
+      SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+  ASSERT_NO_FATAL_FAILURE(
+      WriteSingleBatch(0, std::min<size_t>(max_batch_size, GetNumBuffers())));
   ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
   WriteEndOfStream();
   DrainOutputs();
 }
 
-TEST_P(SbPlayerWriteSampleTest, MultipleInputs) {
-  ASSERT_NO_FATAL_FAILURE(
-      WriteMultipleInputs(0, std::min<size_t>(10, GetNumBuffers())));
-  ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
+TEST_P(SbPlayerWriteSampleTest, WriteMultipleBatches) {
+  ASSERT_NO_FATAL_FAILURE(WriteMultipleBatches(0, 8));
   WriteEndOfStream();
   DrainOutputs();
 }
diff --git a/starboard/nplb/speech_synthesis_basic_test.cc b/starboard/nplb/speech_synthesis_basic_test.cc
index aadf06f..b11f3bc 100644
--- a/starboard/nplb/speech_synthesis_basic_test.cc
+++ b/starboard/nplb/speech_synthesis_basic_test.cc
@@ -28,6 +28,14 @@
   SbSpeechSynthesisCancel();
 }
 
+TEST(SbSpeechSynthesisBasicTest, RainyDayNull) {
+  SbSpeechSynthesisSpeak(NULL);
+}
+
+TEST(SbSpeechSynthesisBasicTest, RainyDayEmpty) {
+  SbSpeechSynthesisSpeak("");
+}
+
 }  // namespace
 }  // namespace nplb
 }  // namespace starboard
diff --git a/starboard/nplb/system_get_path_test.cc b/starboard/nplb/system_get_path_test.cc
index 44f4e00..e320737 100644
--- a/starboard/nplb/system_get_path_test.cc
+++ b/starboard/nplb/system_get_path_test.cc
@@ -69,9 +69,6 @@
 TEST(SbSystemGetPathTest, ReturnsRequiredPaths) {
   BasicTest(kSbSystemPathContentDirectory, true, true, __LINE__);
   BasicTest(kSbSystemPathCacheDirectory, true, true, __LINE__);
-#if SB_API_VERSION >= 14
-  BasicTest(kSbSystemPathStorageDirectory, true, true, __LINE__);
-#endif  // SB_API_VERSION >= 14
 }
 
 TEST(SbSystemGetPathTest, FailsGracefullyZeroBufferLength) {
diff --git a/starboard/queue.h b/starboard/queue.h
deleted file mode 100644
index ca22cee..0000000
--- a/starboard/queue.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef STARBOARD_QUEUE_H_
-#define STARBOARD_QUEUE_H_
-
-#error "File moved to //starboard/common/queue.h."
-
-#endif  // STARBOARD_QUEUE_H_
diff --git a/starboard/raspi/2/skia/configuration.cc b/starboard/raspi/2/skia/configuration.cc
index d1d34a5..69c9886 100644
--- a/starboard/raspi/2/skia/configuration.cc
+++ b/starboard/raspi/2/skia/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/raspi/2/skia/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 namespace starboard {
 namespace raspi {
diff --git a/starboard/raspi/2/skia/system_get_extensions.cc b/starboard/raspi/2/skia/system_get_extensions.cc
index 568d321..c80a3d1 100644
--- a/starboard/raspi/2/skia/system_get_extensions.cc
+++ b/starboard/raspi/2/skia/system_get_extensions.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
 #include "starboard/raspi/2/skia/configuration.h"
 
 const void* SbSystemGetExtension(const char* name) {
diff --git a/starboard/raspi/shared/configuration.cc b/starboard/raspi/shared/configuration.cc
index 43c015e..b1f0283 100644
--- a/starboard/raspi/shared/configuration.cc
+++ b/starboard/raspi/shared/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/raspi/shared/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 namespace starboard {
 namespace raspi {
diff --git a/starboard/raspi/shared/graphics.cc b/starboard/raspi/shared/graphics.cc
index 15f598c..02309fc 100644
--- a/starboard/raspi/shared/graphics.cc
+++ b/starboard/raspi/shared/graphics.cc
@@ -14,7 +14,7 @@
 
 #include "starboard/raspi/shared/graphics.h"
 
-#include "cobalt/extension/graphics.h"
+#include "starboard/extension/graphics.h"
 
 namespace starboard {
 namespace raspi {
@@ -35,11 +35,11 @@
 }
 
 const CobaltExtensionGraphicsApi kGraphicsApi = {
-  kCobaltExtensionGraphicsName,
-  3,
-  &GetMaximumFrameIntervalInMilliseconds,
-  &GetMinimumFrameIntervalInMilliseconds,
-  &IsMapToMeshEnabled,
+    kCobaltExtensionGraphicsName,
+    3,
+    &GetMaximumFrameIntervalInMilliseconds,
+    &GetMinimumFrameIntervalInMilliseconds,
+    &IsMapToMeshEnabled,
 };
 
 }  // namespace
diff --git a/starboard/raspi/shared/launcher.py b/starboard/raspi/shared/launcher.py
index 3f6a2ba..ba6605b 100644
--- a/starboard/raspi/shared/launcher.py
+++ b/starboard/raspi/shared/launcher.py
@@ -112,14 +112,11 @@
     # TODO(b/218889313): This should reference the bin/ subdir when that's
     # used.
     test_dir = os.path.join(self.out_directory, 'install', self.target_name)
-    # TODO(b/216356058): Delete this conditional that's just for GYP.
-    if not os.path.isdir(test_dir):
-      test_dir = os.path.join(self.out_directory, 'deploy', self.target_name)
     test_file = self.target_name
 
     test_path = os.path.join(test_dir, test_file)
     if not os.path.isfile(test_path):
-      raise ValueError('TargetPath ({}) must be a file.'.format(test_path))
+      raise ValueError(f'TargetPath ({test_path}) must be a file.')
 
     raspi_user_hostname = Launcher._RASPI_USERNAME + '@' + self.device_id
 
@@ -131,7 +128,7 @@
     # rsync command setup
     options = '-avzLhc'
     source = test_dir + '/'
-    destination = '{}:~/{}/'.format(raspi_user_hostname, raspi_test_dir)
+    destination = f'{raspi_user_hostname}:~/{raspi_test_dir}/'
     self.rsync_command = 'rsync ' + options + ' ' + source + ' ' + destination
 
     # ssh command setup
@@ -145,19 +142,18 @@
     escaped_flags = re.subn(meta_re, r'\\\1', flags)[0]
 
     # test output tags
-    self.test_complete_tag = 'TEST-{time}'.format(time=time.time())
+    self.test_complete_tag = f'TEST-{time.time()}'
     self.test_success_tag = 'succeeded'
     self.test_failure_tag = 'failed'
 
     # test command setup
     test_base_command = raspi_test_path + ' ' + escaped_flags
-    test_success_output = ' && echo {} {}'.format(self.test_complete_tag,
-                                                  self.test_success_tag)
-    test_failure_output = ' || echo {} {}'.format(self.test_complete_tag,
-                                                  self.test_failure_tag)
-    self.test_command = '{} {} {}'.format(test_base_command,
-                                          test_success_output,
-                                          test_failure_output)
+    test_success_output = (f' && echo {self.test_complete_tag}'
+                           f'{self.test_success_tag}')
+    test_failure_output = (f' || echo {self.test_complete_tag}'
+                           f'{self.test_failure_tag}')
+    self.test_command = (f'{test_base_command} {test_success_output}'
+                         f'{test_failure_output}')
 
   def _PexpectSpawnAndConnect(self, command):
     """Spawns a process with pexpect and connect to the raspi.
@@ -238,8 +234,7 @@
           raise
 
   def _Sleep(self, val):
-    self._PexpectSendLine('sleep {};echo {}'.format(val,
-                                                    Launcher._SSH_SLEEP_SIGNAL))
+    self._PexpectSendLine(f'sleep {val};echo {Launcher._SSH_SLEEP_SIGNAL}')
     self.pexpect_process.expect([Launcher._SSH_SLEEP_SIGNAL])
 
   def _CleanupPexpectProcess(self):
@@ -334,7 +329,7 @@
       # Execute debugging commands on the first run
       first_run_commands = []
       if self.test_result_xml_path:
-        first_run_commands.append('touch {}'.format(self.test_result_xml_path))
+        first_run_commands.append(f'touch {self.test_result_xml_path}')
       first_run_commands.extend(['free -mh', 'ps -ux', 'df -h'])
       if FirstRun():
         for cmd in first_run_commands:
diff --git a/starboard/raspi/shared/open_max/video_decoder.cc b/starboard/raspi/shared/open_max/video_decoder.cc
index d9ad900..e2afc38 100644
--- a/starboard/raspi/shared/open_max/video_decoder.cc
+++ b/starboard/raspi/shared/open_max/video_decoder.cc
@@ -69,18 +69,19 @@
   SB_DCHECK(SbThreadIsValid(thread_));
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
-  SB_DCHECK(input_buffer);
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
   SB_DCHECK(!eos_written_);
 
   first_input_written_ = true;
+  const auto& input_buffer = input_buffers[0];
   queue_.Put(new Event(input_buffer));
   if (!TryToDeliverOneFrame()) {
     SbThreadSleep(kSbTimeMillisecond);
-    // Call the callback with NULL frame to ensure that the host know that more
-    // data is expected.
+    // Call the callback with NULL frame to ensure that the host knows that
+    // more data is expected.
     decoder_status_cb_(kNeedMoreInput, NULL);
   }
 }
diff --git a/starboard/raspi/shared/open_max/video_decoder.h b/starboard/raspi/shared/open_max/video_decoder.h
index 7dc899e..9965a59 100644
--- a/starboard/raspi/shared/open_max/video_decoder.h
+++ b/starboard/raspi/shared/open_max/video_decoder.h
@@ -50,8 +50,7 @@
   size_t GetPrerollFrameCount() const override { return 1; }
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
-  void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer)
-      override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
   SbDecodeTarget GetCurrentDecodeTarget() override {
diff --git a/starboard/raspi/shared/system_get_extensions.cc b/starboard/raspi/shared/system_get_extensions.cc
index cf9419f..bd7854d 100644
--- a/starboard/raspi/shared/system_get_extensions.cc
+++ b/starboard/raspi/shared/system_get_extensions.cc
@@ -14,10 +14,10 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/crash_handler.h"
-#include "cobalt/extension/graphics.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/crash_handler.h"
+#include "starboard/extension/graphics.h"
 #include "starboard/shared/starboard/crash_handler.h"
 #if SB_IS(EVERGREEN_COMPATIBLE)
 #include "starboard/elf_loader/evergreen_config.h"
diff --git a/starboard/raspi/shared/test_filters.py b/starboard/raspi/shared/test_filters.py
index da172ec..9e8e146 100644
--- a/starboard/raspi/shared/test_filters.py
+++ b/starboard/raspi/shared/test_filters.py
@@ -15,6 +15,7 @@
 
 from starboard.tools.testing import test_filter
 
+# pylint: disable=line-too-long
 _FILTERED_TESTS = {
     'nplb': [
         'SbAudioSinkTest.*',
@@ -35,8 +36,8 @@
         # The implementations for the raspberry pi (0 and 2) are incomplete
         # and not meant to be a reference implementation. As such we will
         # not repair these failing tests for now.
-        'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/0',
-        'VideoDecoderTests/VideoDecoderTest.MultipleResets/0',
+        'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/beneath_the_canopy_137_avc_dmp_Punchout',
+        'VideoDecoderTests/VideoDecoderTest.MultipleResets/beneath_the_canopy_137_avc_dmp_Punchout',
         # Filter failed tests.
         'PlayerComponentsTests/PlayerComponentsTest.*',
     ],
diff --git a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
index 50cb9bd..4b21032 100644
--- a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
@@ -106,14 +106,16 @@
   error_cb_ = error_cb;
 }
 
-void AudioDecoderImpl<FFMPEG>::Decode(
-    const scoped_refptr<InputBuffer>& input_buffer,
-    const ConsumedCB& consumed_cb) {
+void AudioDecoderImpl<FFMPEG>::Decode(const InputBuffers& input_buffers,
+                                      const ConsumedCB& consumed_cb) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(output_cb_);
   SB_CHECK(codec_context_ != NULL);
 
+  const auto& input_buffer = input_buffers[0];
+
   Schedule(consumed_cb);
 
   if (stream_ended_) {
diff --git a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
index 2378319..9c4cc69 100644
--- a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
@@ -53,7 +53,7 @@
 
   // From: starboard::player::filter::AudioDecoder
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer.h b/starboard/shared/ffmpeg/ffmpeg_demuxer.h
index 1a132bc3..eeed996 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer.h
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer.h
@@ -17,7 +17,7 @@
 
 #include <memory>
 
-#include "cobalt/extension/demuxer.h"
+#include "starboard/extension/demuxer.h"
 
 namespace starboard {
 namespace shared {
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
index cbe4b09..c8113c9 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
@@ -19,7 +19,7 @@
 #include <memory>
 #include <vector>
 
-#include "cobalt/extension/demuxer.h"
+#include "starboard/extension/demuxer.h"
 #include "starboard/shared/ffmpeg/ffmpeg_common.h"
 #include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
 #include "starboard/shared/ffmpeg/ffmpeg_demuxer_impl_interface.h"
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl_interface.h b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl_interface.h
index 901edc6..27a0cfb 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl_interface.h
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl_interface.h
@@ -17,7 +17,7 @@
 
 #include <memory>
 
-#include "cobalt/extension/demuxer.h"
+#include "starboard/extension/demuxer.h"
 #include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
 
 namespace starboard {
diff --git a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
index d8605a6..3b6a9bb 100644
--- a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
@@ -148,12 +148,15 @@
   error_cb_ = error_cb;
 }
 
-void VideoDecoderImpl<FFMPEG>::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
-  SB_DCHECK(input_buffer);
+void VideoDecoderImpl<FFMPEG>::WriteInputBuffers(
+    const InputBuffers& input_buffers) {
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(queue_.Poll().type == kInvalid);
   SB_DCHECK(decoder_status_cb_);
 
+  const auto& input_buffer = input_buffers[0];
+
   if (stream_ended_) {
     SB_LOG(ERROR) << "WriteInputFrame() was called after WriteEndOfStream().";
     return;
@@ -165,7 +168,6 @@
         &VideoDecoderImpl<FFMPEG>::ThreadEntryPoint, this);
     SB_DCHECK(SbThreadIsValid(decoder_thread_));
   }
-
   queue_.Put(Event(input_buffer));
 }
 
@@ -177,7 +179,7 @@
   stream_ended_ = true;
 
   if (!SbThreadIsValid(decoder_thread_)) {
-    // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+    // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
     // don't create the decoder thread and send the EOS frame directly.
     decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
     return;
diff --git a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
index 4e3cca0..aa9faab 100644
--- a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
@@ -63,8 +63,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
diff --git a/starboard/shared/libaom/aom_video_decoder.cc b/starboard/shared/libaom/aom_video_decoder.cc
index 605ed98..2170661 100644
--- a/starboard/shared/libaom/aom_video_decoder.cc
+++ b/starboard/shared/libaom/aom_video_decoder.cc
@@ -58,10 +58,10 @@
   error_cb_ = error_cb;
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
 
   if (stream_ended_) {
@@ -74,6 +74,7 @@
     SB_DCHECK(decoder_thread_);
   }
 
+  auto input_buffer = input_buffers[0];
   decoder_thread_->job_queue()->Schedule(
       std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
 }
diff --git a/starboard/shared/libaom/aom_video_decoder.h b/starboard/shared/libaom/aom_video_decoder.h
index 5301023..d551872 100644
--- a/starboard/shared/libaom/aom_video_decoder.h
+++ b/starboard/shared/libaom/aom_video_decoder.h
@@ -48,8 +48,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
diff --git a/starboard/shared/libdav1d/dav1d_video_decoder.cc b/starboard/shared/libdav1d/dav1d_video_decoder.cc
index 7d768d5..95a1c08 100644
--- a/starboard/shared/libdav1d/dav1d_video_decoder.cc
+++ b/starboard/shared/libdav1d/dav1d_video_decoder.cc
@@ -15,6 +15,7 @@
 #include "starboard/shared/libdav1d/dav1d_video_decoder.h"
 
 #include <string>
+#include <utility>
 
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
@@ -81,14 +82,14 @@
   error_cb_ = error_cb;
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
 
   if (stream_ended_) {
-    ReportError("WriteInputBuffer() was called after WriteEndOfStream().");
+    ReportError("WriteInputBuffers() was called after WriteEndOfStream().");
     return;
   }
 
@@ -97,6 +98,7 @@
     SB_DCHECK(decoder_thread_);
   }
 
+  const auto& input_buffer = input_buffers[0];
   decoder_thread_->job_queue()->Schedule(
       std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
 }
@@ -110,7 +112,7 @@
   stream_ended_ = true;
 
   if (!decoder_thread_) {
-    // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+    // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
     // don't create the decoder thread and send the EOS frame directly.
     decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
     return;
diff --git a/starboard/shared/libdav1d/dav1d_video_decoder.h b/starboard/shared/libdav1d/dav1d_video_decoder.h
index b05b366..fb39ba1 100644
--- a/starboard/shared/libdav1d/dav1d_video_decoder.h
+++ b/starboard/shared/libdav1d/dav1d_video_decoder.h
@@ -49,8 +49,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
diff --git a/starboard/shared/libde265/de265_video_decoder.cc b/starboard/shared/libde265/de265_video_decoder.cc
index 48a9882..ee8e149 100644
--- a/starboard/shared/libde265/de265_video_decoder.cc
+++ b/starboard/shared/libde265/de265_video_decoder.cc
@@ -54,10 +54,10 @@
   error_cb_ = error_cb;
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
 
   if (stream_ended_) {
@@ -70,6 +70,7 @@
     SB_DCHECK(decoder_thread_);
   }
 
+  const auto& input_buffer = input_buffers[0];
   decoder_thread_->job_queue()->Schedule(
       std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
 }
@@ -83,7 +84,7 @@
   stream_ended_ = true;
 
   if (!decoder_thread_) {
-    // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+    // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
     // don't create the decoder thread and send the EOS frame directly.
     decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
     return;
diff --git a/starboard/shared/libde265/de265_video_decoder.h b/starboard/shared/libde265/de265_video_decoder.h
index 614b4ab..878b49d 100644
--- a/starboard/shared/libde265/de265_video_decoder.h
+++ b/starboard/shared/libde265/de265_video_decoder.h
@@ -50,8 +50,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
diff --git a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc
new file mode 100644
index 0000000..59ffd16
--- /dev/null
+++ b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc
@@ -0,0 +1,250 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/libfdkaac/fdk_aac_audio_decoder.h"
+
+#include "starboard/common/log.h"
+#include "starboard/common/string.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+FdkAacAudioDecoder::FdkAacAudioDecoder() {
+  static_assert(sizeof(INT_PCM) == sizeof(int16_t),
+                "sizeof(INT_PCM) has to be the same as sizeof(int16_t).");
+  InitializeCodec();
+}
+
+FdkAacAudioDecoder::~FdkAacAudioDecoder() {
+  TeardownCodec();
+}
+
+void FdkAacAudioDecoder::Initialize(const OutputCB& output_cb,
+                                    const ErrorCB& error_cb) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb);
+  SB_DCHECK(!output_cb_);
+  SB_DCHECK(error_cb);
+  SB_DCHECK(!error_cb_);
+
+  output_cb_ = output_cb;
+  error_cb_ = error_cb;
+}
+
+void FdkAacAudioDecoder::Decode(const InputBuffers& input_buffers,
+                                const ConsumedCB& consumed_cb) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(decoder_ != NULL);
+
+  if (stream_ended_) {
+    SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
+    return;
+  }
+  const auto& input_buffer = input_buffers[0];
+  if (!WriteToFdkDecoder(input_buffer)) {
+    return;
+  }
+
+  timestamp_queue_.push(input_buffer->timestamp());
+  Schedule(consumed_cb);
+  ReadFromFdkDecoder(kDecodeModeDoNotFlush);
+}
+
+scoped_refptr<FdkAacAudioDecoder::DecodedAudio> FdkAacAudioDecoder::Read(
+    int* samples_per_second) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(!decoded_audios_.empty());
+
+  scoped_refptr<DecodedAudio> result;
+  if (!decoded_audios_.empty()) {
+    result = decoded_audios_.front();
+    decoded_audios_.pop();
+  }
+  *samples_per_second = samples_per_second_;
+  return result;
+}
+
+void FdkAacAudioDecoder::Reset() {
+  SB_DCHECK(decoder_ != NULL);
+  SB_DCHECK(BelongsToCurrentThread());
+
+  TeardownCodec();
+  InitializeCodec();
+
+  stream_ended_ = false;
+  decoded_audios_ = std::queue<scoped_refptr<DecodedAudio>>();  // clear
+  partially_decoded_audio_ = nullptr;
+  partially_decoded_audio_data_in_bytes_ = 0;
+  timestamp_queue_ = std::queue<SbTime>();  // clear
+  // Clean up stream information and deduced results.
+  has_stream_info_ = false;
+  num_channels_ = 0;
+  samples_per_second_ = 0;
+  decoded_audio_size_in_bytes_ = 0;
+  audio_data_to_discard_in_bytes_ = 0;
+  CancelPendingJobs();
+}
+
+void FdkAacAudioDecoder::WriteEndOfStream() {
+  SB_DCHECK(decoder_ != NULL);
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb_);
+
+  while (!timestamp_queue_.empty()) {
+    if (!ReadFromFdkDecoder(kDecodeModeFlush)) {
+      return;
+    }
+  }
+  stream_ended_ = true;
+  // Put EOS into the queue.
+  decoded_audios_.push(new DecodedAudio);
+  Schedule(output_cb_);
+}
+
+void FdkAacAudioDecoder::InitializeCodec() {
+  SB_DCHECK(decoder_ == NULL);
+  decoder_ = aacDecoder_Open(TT_MP4_ADTS, 1);
+  SB_DCHECK(decoder_ != NULL);
+
+  // Set AAC_PCM_MAX_OUTPUT_CHANNELS to 0 to disable downmixing feature.
+  // It makes the decoder output contain the same number of channels as the
+  // encoded bitstream.
+  AAC_DECODER_ERROR error =
+      aacDecoder_SetParam(decoder_, AAC_PCM_MAX_OUTPUT_CHANNELS, 0);
+  SB_DCHECK(error == AAC_DEC_OK);
+}
+
+void FdkAacAudioDecoder::TeardownCodec() {
+  if (decoder_) {
+    aacDecoder_Close(decoder_);
+    decoder_ = nullptr;
+  }
+}
+
+bool FdkAacAudioDecoder::WriteToFdkDecoder(
+    const scoped_refptr<InputBuffer>& input_buffer) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(input_buffer);
+
+  unsigned char* data = const_cast<unsigned char*>(input_buffer->data());
+  const unsigned int data_size_in_bytes = input_buffer->size();
+  unsigned int left_to_decode_in_bytes = input_buffer->size();
+  AAC_DECODER_ERROR error = aacDecoder_Fill(
+      decoder_, &data, &data_size_in_bytes, &left_to_decode_in_bytes);
+  if (error != AAC_DEC_OK) {
+    SB_LOG(ERROR) << "aacDecoder_Fill() failed with result : " << error;
+    error_cb_(kSbPlayerErrorDecode,
+              FormatString("aacDecoder_Fill() failed with result %d.", error));
+    return false;
+  }
+
+  // Returned |left_to_decode_in_bytes| should always be 0 as DecodeFrame() will
+  // be called immediately on the same thread.
+  SB_DCHECK(left_to_decode_in_bytes == 0);
+  return true;
+}
+
+bool FdkAacAudioDecoder::ReadFromFdkDecoder(DecodeMode mode) {
+  SB_DCHECK(mode == kDecodeModeFlush || mode == kDecodeModeDoNotFlush);
+  int flags = mode == kDecodeModeFlush ? AACDEC_FLUSH : 0;
+
+  AAC_DECODER_ERROR error = aacDecoder_DecodeFrame(
+      decoder_, reinterpret_cast<INT_PCM*>(output_buffer_),
+      kMaxOutputBufferSizeInBytes / sizeof(INT_PCM), flags);
+  if (error != AAC_DEC_OK) {
+    error_cb_(
+        kSbPlayerErrorDecode,
+        FormatString("aacDecoder_DecodeFrame() failed with result %d.", error));
+    return false;
+  }
+
+  TryToUpdateStreamInfo();
+  SB_DCHECK(has_stream_info_);
+  if (audio_data_to_discard_in_bytes_ >= decoded_audio_size_in_bytes_) {
+    // Discard all decoded data in |output_buffer_|.
+    audio_data_to_discard_in_bytes_ -= decoded_audio_size_in_bytes_;
+    return true;
+  }
+
+  // Discard the initial |audio_data_to_discard_in_bytes_| in |output_buffer_|.
+  int offset_in_bytes = audio_data_to_discard_in_bytes_;
+  audio_data_to_discard_in_bytes_ = 0;
+  TryToOutputDecodedAudio(output_buffer_ + offset_in_bytes,
+                          decoded_audio_size_in_bytes_ - offset_in_bytes);
+  return true;
+}
+
+void FdkAacAudioDecoder::TryToUpdateStreamInfo() {
+  if (has_stream_info_) {
+    return;
+  }
+  CStreamInfo* stream_info = aacDecoder_GetStreamInfo(decoder_);
+  SB_DCHECK(stream_info);
+
+  num_channels_ = stream_info->numChannels;
+  samples_per_second_ = stream_info->sampleRate;
+  decoded_audio_size_in_bytes_ =
+      sizeof(int16_t) * stream_info->frameSize * num_channels_;
+  audio_data_to_discard_in_bytes_ =
+      sizeof(int16_t) * stream_info->outputDelay * num_channels_;
+  has_stream_info_ = true;
+}
+
+void FdkAacAudioDecoder::TryToOutputDecodedAudio(const uint8_t* data,
+                                                 int size_in_bytes) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(has_stream_info_);
+
+  while (size_in_bytes > 0 && !timestamp_queue_.empty()) {
+    if (!partially_decoded_audio_) {
+      SB_DCHECK(partially_decoded_audio_data_in_bytes_ == 0);
+      partially_decoded_audio_ = new DecodedAudio(
+          num_channels_, kSbMediaAudioSampleTypeInt16Deprecated,
+          kSbMediaAudioFrameStorageTypeInterleaved, timestamp_queue_.front(),
+          decoded_audio_size_in_bytes_);
+    }
+    int freespace = static_cast<int>(partially_decoded_audio_->size()) -
+                    partially_decoded_audio_data_in_bytes_;
+    if (size_in_bytes >= freespace) {
+      memcpy(partially_decoded_audio_->buffer() +
+                 partially_decoded_audio_data_in_bytes_,
+             data, freespace);
+      data += freespace;
+      size_in_bytes -= freespace;
+      SB_DCHECK(timestamp_queue_.front() ==
+                partially_decoded_audio_->timestamp());
+      timestamp_queue_.pop();
+      decoded_audios_.push(partially_decoded_audio_);
+      Schedule(output_cb_);
+      partially_decoded_audio_ = nullptr;
+      partially_decoded_audio_data_in_bytes_ = 0;
+      continue;
+    }
+    memcpy(partially_decoded_audio_->buffer() +
+               partially_decoded_audio_data_in_bytes_,
+           data, size_in_bytes);
+    partially_decoded_audio_data_in_bytes_ += size_in_bytes;
+    return;
+  }
+}
+
+}  // namespace libfdkaac
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h
new file mode 100644
index 0000000..98a90c2
--- /dev/null
+++ b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h
@@ -0,0 +1,101 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
+#define STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
+
+#include <queue>
+
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/decoded_audio_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/job_queue.h"
+#include "third_party/libfdkaac/include/aacdecoder_lib.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+class FdkAacAudioDecoder : public starboard::player::filter::AudioDecoder,
+                           private starboard::player::JobQueue::JobOwner {
+ public:
+  // The max supportable channels to be decoded for fdk aac is 8.
+  static constexpr int kMaxChannels = 8;
+
+  FdkAacAudioDecoder();
+  ~FdkAacAudioDecoder() override;
+
+  // Overriding functions from starboard::player::filter::AudioDecoder.
+  void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
+  void Decode(const InputBuffers& input_buffers,
+              const ConsumedCB& consumed_cb) override;
+  scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
+  void Reset() override;
+  void WriteEndOfStream() override;
+
+ private:
+  // An AAC access unit can contain at most 2048 PCM samples (when it's HE-AAC).
+  static constexpr int kMaxSamplesPerAccessUnit = 2048;
+  // The max bytes required to store a decoded access unit.
+  static constexpr int kMaxOutputBufferSizeInBytes =
+      sizeof(int16_t) * kMaxSamplesPerAccessUnit * kMaxChannels;
+
+  enum DecodeMode {
+    kDecodeModeFlush,
+    kDecodeModeDoNotFlush,
+  };
+
+  void InitializeCodec();
+  void TeardownCodec();
+  bool WriteToFdkDecoder(const scoped_refptr<InputBuffer>& input_buffer);
+  bool ReadFromFdkDecoder(DecodeMode mode);
+  void TryToUpdateStreamInfo();
+  void TryToOutputDecodedAudio(const uint8_t* audio_data, int size_in_bytes);
+
+  OutputCB output_cb_;
+  ErrorCB error_cb_;
+
+  bool stream_ended_ = false;
+  uint8_t output_buffer_[kMaxOutputBufferSizeInBytes];
+
+  std::queue<scoped_refptr<DecodedAudio>> decoded_audios_;
+  // The DecodedAudio being filled up, will be appended to |decoded_audios_|
+  // once it's fully filled (and |output_cb_| will be called).
+  scoped_refptr<DecodedAudio> partially_decoded_audio_;
+  int partially_decoded_audio_data_in_bytes_ = 0;
+  // Keep timestamps for inputs, which will be used to create DecodedAudio.
+  std::queue<SbTime> timestamp_queue_;
+  // libfdkaac related parameters are listed below.
+  HANDLE_AACDECODER decoder_ = nullptr;
+  // There are two quirks of the fdk aac decoder:
+  // 1. Its output parameters (contained in CStreamInfo) are only filled after
+  //    the first aacDecoder_DecodeFrame() call.
+  // 2. When filled with N aac access units (i.e. InputBuffer), it will produce
+  //    `stream_info->outputDelay + stream_info->frameSize * N` output samples.
+  //    The first `outputDelay` samples should be discarded and the remaining
+  //    samples contain valid output.
+  bool has_stream_info_ = false;
+  int num_channels_ = 0;
+  int samples_per_second_ = 0;
+  size_t decoded_audio_size_in_bytes_ = 0;
+  // How many bytes of audio output left to be discarded.
+  size_t audio_data_to_discard_in_bytes_ = 0;
+};
+
+}  // namespace libfdkaac
+}  // namespace shared
+}  // namespace starboard
+#endif  // STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
diff --git a/starboard/shared/libfdkaac/libfdkaac_library_loader.cc b/starboard/shared/libfdkaac/libfdkaac_library_loader.cc
new file mode 100644
index 0000000..dac61d5
--- /dev/null
+++ b/starboard/shared/libfdkaac/libfdkaac_library_loader.cc
@@ -0,0 +1,105 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <dlfcn.h>
+
+#include "starboard/common/log.h"
+#include "starboard/once.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+namespace {
+const char kLibfdkaacLibraryName[] = "libfdk-aac.so";
+}
+
+SB_ONCE_INITIALIZE_FUNCTION(LibfdkaacHandle, LibfdkaacHandle::GetHandle);
+
+LibfdkaacHandle::LibfdkaacHandle() {
+  LoadLibrary();
+}
+
+LibfdkaacHandle::~LibfdkaacHandle() {
+  if (handle_) {
+    dlclose(handle_);
+  }
+}
+
+bool LibfdkaacHandle::IsLoaded() const {
+  return handle_;
+}
+
+void LibfdkaacHandle::ReportSymbolError() {
+  SB_LOG(ERROR) << "libfdkaac load error: " << dlerror();
+  dlclose(handle_);
+  handle_ = NULL;
+}
+
+void LibfdkaacHandle::LoadLibrary() {
+  SB_DCHECK(!handle_);
+  handle_ = dlopen(kLibfdkaacLibraryName, RTLD_LAZY);
+  if (!handle_) {
+    return;
+  }
+
+#define INITSYMBOL(symbol)                                              \
+  symbol = reinterpret_cast<decltype(symbol)>(dlsym(handle_, #symbol)); \
+  if (!symbol) {                                                        \
+    ReportSymbolError();                                                \
+    return;                                                             \
+  }
+
+  INITSYMBOL(aacDecoder_GetStreamInfo);
+  INITSYMBOL(aacDecoder_Close);
+  INITSYMBOL(aacDecoder_Open);
+  INITSYMBOL(aacDecoder_ConfigRaw);
+  INITSYMBOL(aacDecoder_SetParam);
+  INITSYMBOL(aacDecoder_AncDataInit);
+  INITSYMBOL(aacDecoder_Fill);
+  INITSYMBOL(aacDecoder_DecodeFrame);
+}
+
+CStreamInfo* (*aacDecoder_GetStreamInfo)(HANDLE_AACDECODER self);
+
+void (*aacDecoder_Close)(HANDLE_AACDECODER self);
+
+HANDLE_AACDECODER(*aacDecoder_Open)
+(TRANSPORT_TYPE transportFmt, UINT nrOfLayers);
+
+AAC_DECODER_ERROR(*aacDecoder_ConfigRaw)
+(HANDLE_AACDECODER self, UCHAR* conf[], const UINT length[]);
+
+AAC_DECODER_ERROR(*aacDecoder_SetParam)
+(const HANDLE_AACDECODER self, const AACDEC_PARAM param, const INT value);
+
+AAC_DECODER_ERROR(*aacDecoder_AncDataInit)
+(HANDLE_AACDECODER self, UCHAR* buffer, int size);
+
+AAC_DECODER_ERROR(*aacDecoder_Fill)
+(HANDLE_AACDECODER self,
+ UCHAR* pBuffer[],
+ const UINT bufferSize[],
+ UINT* bytesValid);
+
+AAC_DECODER_ERROR(*aacDecoder_DecodeFrame)
+(HANDLE_AACDECODER self,
+ INT_PCM* pTimeData,
+ const INT timeDataSize,
+ const UINT flags);
+
+}  // namespace libfdkaac
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/shared/libfdkaac/libfdkaac_library_loader.h b/starboard/shared/libfdkaac/libfdkaac_library_loader.h
new file mode 100644
index 0000000..43d642e
--- /dev/null
+++ b/starboard/shared/libfdkaac/libfdkaac_library_loader.h
@@ -0,0 +1,76 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
+#define STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
+
+#include "starboard/shared/internal_only.h"
+#include "third_party/libfdkaac/include/aacdecoder_lib.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+class LibfdkaacHandle {
+ public:
+  LibfdkaacHandle();
+
+  ~LibfdkaacHandle();
+
+  static LibfdkaacHandle* GetHandle();
+
+  bool IsLoaded() const;
+
+ private:
+  void ReportSymbolError();
+
+  void LoadLibrary();
+
+  void* handle_ = NULL;
+};
+
+extern CStreamInfo* (*aacDecoder_GetStreamInfo)(HANDLE_AACDECODER self);
+
+extern void (*aacDecoder_Close)(HANDLE_AACDECODER self);
+
+extern HANDLE_AACDECODER (*aacDecoder_Open)(TRANSPORT_TYPE transportFmt,
+                                            UINT nrOfLayers);
+
+extern AAC_DECODER_ERROR (*aacDecoder_ConfigRaw)(HANDLE_AACDECODER self,
+                                                 UCHAR* conf[],
+                                                 const UINT length[]);
+
+extern AAC_DECODER_ERROR (*aacDecoder_SetParam)(const HANDLE_AACDECODER self,
+                                                const AACDEC_PARAM param,
+                                                const INT value);
+
+extern AAC_DECODER_ERROR (*aacDecoder_AncDataInit)(HANDLE_AACDECODER self,
+                                                   UCHAR* buffer,
+                                                   int size);
+
+extern AAC_DECODER_ERROR (*aacDecoder_Fill)(HANDLE_AACDECODER self,
+                                            UCHAR* pBuffer[],
+                                            const UINT bufferSize[],
+                                            UINT* bytesValid);
+
+extern AAC_DECODER_ERROR (*aacDecoder_DecodeFrame)(HANDLE_AACDECODER self,
+                                                   INT_PCM* pTimeData,
+                                                   const INT timeDataSize,
+                                                   const UINT flags);
+
+}  // namespace libfdkaac
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
diff --git a/starboard/shared/libvpx/vpx_video_decoder.cc b/starboard/shared/libvpx/vpx_video_decoder.cc
index 90e0878..d88a762 100644
--- a/starboard/shared/libvpx/vpx_video_decoder.cc
+++ b/starboard/shared/libvpx/vpx_video_decoder.cc
@@ -56,10 +56,10 @@
   error_cb_ = error_cb;
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
 
   if (stream_ended_) {
@@ -72,6 +72,7 @@
     SB_DCHECK(decoder_thread_);
   }
 
+  const auto& input_buffer = input_buffers[0];
   decoder_thread_->job_queue()->Schedule(
       std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
 }
@@ -85,7 +86,7 @@
   stream_ended_ = true;
 
   if (!decoder_thread_) {
-    // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+    // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
     // don't create the decoder thread and send the EOS frame directly.
     decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
     return;
diff --git a/starboard/shared/libvpx/vpx_video_decoder.h b/starboard/shared/libvpx/vpx_video_decoder.h
index b414374..6fe86b8 100644
--- a/starboard/shared/libvpx/vpx_video_decoder.h
+++ b/starboard/shared/libvpx/vpx_video_decoder.h
@@ -50,8 +50,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
diff --git a/starboard/shared/media_session/playback_state.cc b/starboard/shared/media_session/playback_state.cc
index 47da0d5..b29f2d1 100644
--- a/starboard/shared/media_session/playback_state.cc
+++ b/starboard/shared/media_session/playback_state.cc
@@ -16,8 +16,8 @@
 
 #include <cstring>
 
-#include "cobalt/extension/media_session.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/media_session.h"
 #include "starboard/string.h"
 #include "starboard/system.h"
 
@@ -55,8 +55,7 @@
         SbSystemGetExtension(kCobaltExtensionMediaSessionName));
   }
   if (g_extension &&
-      strcmp(g_extension->name, kCobaltExtensionMediaSessionName) ==
-          0 &&
+      strcmp(g_extension->name, kCobaltExtensionMediaSessionName) == 0 &&
       g_extension->version >= 1) {
     CobaltExtensionMediaSessionPlaybackState ext_state =
         PlaybackStateToMediaSessionPlaybackState(state);
diff --git a/starboard/shared/openh264/openh264_video_decoder.cc b/starboard/shared/openh264/openh264_video_decoder.cc
index cf17fab..7538611 100644
--- a/starboard/shared/openh264/openh264_video_decoder.cc
+++ b/starboard/shared/openh264/openh264_video_decoder.cc
@@ -77,6 +77,7 @@
 
   CancelPendingJobs();
   frames_being_decoded_ = 0;
+  time_sequential_queue_ = TimeSequentialQueue();
 
   ScopedLock lock(decode_target_mutex_);
   frames_ = std::queue<scoped_refptr<CpuVideoFrame>>();
@@ -128,11 +129,12 @@
   }
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
+
   if (stream_ended_) {
     ReportError("WriteInputBuffer() was called after WriteEndOfStream().");
     return;
@@ -141,6 +143,7 @@
     decoder_thread_.reset(new JobThread("openh264_video_decoder"));
     SB_DCHECK(decoder_thread_);
   }
+  const auto& input_buffer = input_buffers[0];
   decoder_thread_->job_queue()->Schedule(
       std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
 }
@@ -203,6 +206,16 @@
   if (frames_being_decoded_ != 0) {
     SB_LOG(WARNING) << "Inconsistency in the number of input/output frames";
   }
+
+  while (!time_sequential_queue_.empty()) {
+    auto output_frame = time_sequential_queue_.top();
+    if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+      ScopedLock lock(decode_target_mutex_);
+      frames_.push(output_frame);
+    }
+    Schedule(std::bind(decoder_status_cb_, kBufferFull, output_frame));
+    time_sequential_queue_.pop();
+  }
 }
 
 void VideoDecoder::ProcessDecodedImage(unsigned char* decoded_frame[],
@@ -226,14 +239,27 @@
       buffer_info.UsrData.sSystemBuffer.iStride[1],
       buffer_info.uiOutYuvTimeStamp, decoded_frame[0], decoded_frame[1],
       decoded_frame[2]);
-  if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
-    ScopedLock lock(decode_target_mutex_);
-    frames_.push(frame);
+
+  bool has_new_output = false;
+  while (!time_sequential_queue_.empty() &&
+         time_sequential_queue_.top()->timestamp() < frame->timestamp()) {
+    has_new_output = true;
+    auto output_frame = time_sequential_queue_.top();
+    if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+      ScopedLock lock(decode_target_mutex_);
+      frames_.push(output_frame);
+    }
+    if (flushing) {
+      Schedule(std::bind(decoder_status_cb_, kBufferFull, output_frame));
+    } else {
+      Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, output_frame));
+    }
+    time_sequential_queue_.pop();
   }
-  if (flushing) {
-    Schedule(std::bind(decoder_status_cb_, kBufferFull, frame));
-  } else {
-    Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, frame));
+  time_sequential_queue_.push(frame);
+
+  if (!has_new_output) {
+    Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, nullptr));
   }
 }
 
diff --git a/starboard/shared/openh264/openh264_video_decoder.h b/starboard/shared/openh264/openh264_video_decoder.h
index 29dccb8..b547b1f 100644
--- a/starboard/shared/openh264/openh264_video_decoder.h
+++ b/starboard/shared/openh264/openh264_video_decoder.h
@@ -17,6 +17,7 @@
 
 #include <queue>
 #include <string>
+#include <vector>
 
 #include "starboard/common/optional.h"
 #include "starboard/common/ref_counted.h"
@@ -52,8 +53,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override { return 12; }
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
@@ -63,6 +63,18 @@
   static const int kDefaultOpenH264BitsDepth = 8;
   typedef ::starboard::shared::starboard::player::filter::CpuVideoFrame
       CpuVideoFrame;
+  // Operator to compare CpuVideoFrame by timestamp.
+  struct VideoFrameTimeStampGreater {
+    bool operator()(const scoped_refptr<CpuVideoFrame>& left,
+                    const scoped_refptr<CpuVideoFrame>& right) const {
+      // In chronological order.
+      return left->timestamp() > right->timestamp();
+    }
+  };
+  typedef std::priority_queue<scoped_refptr<CpuVideoFrame>,
+                              std::vector<scoped_refptr<CpuVideoFrame>>,
+                              VideoFrameTimeStampGreater>
+      TimeSequentialQueue;
 
   void UpdateDecodeTarget_Locked(const scoped_refptr<CpuVideoFrame>& frame);
 
@@ -86,6 +98,11 @@
   DecoderStatusCB decoder_status_cb_;
   ErrorCB error_cb_;
 
+  // Openh264 does NOT always output video frames in chronological order.
+  // |time_sequential_queue_| is used to reorder |CpuVideoFrame|
+  // chronologically.
+  TimeSequentialQueue time_sequential_queue_;
+
   std::queue<scoped_refptr<CpuVideoFrame>> frames_;
 
   bool stream_ended_ = false;
diff --git a/starboard/shared/opus/opus_audio_decoder.cc b/starboard/shared/opus/opus_audio_decoder.cc
index 0a450f1..1a97dc2 100644
--- a/starboard/shared/opus/opus_audio_decoder.cc
+++ b/starboard/shared/opus/opus_audio_decoder.cc
@@ -85,10 +85,10 @@
   error_cb_ = error_cb;
 }
 
-void OpusAudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void OpusAudioDecoder::Decode(const InputBuffers& input_buffers,
                               const ConsumedCB& consumed_cb) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
   SB_DCHECK(output_cb_);
 
   if (stream_ended_) {
@@ -96,6 +96,21 @@
     return;
   }
 
+  for (const auto& input_buffer : input_buffers) {
+    if (!DecodeInternal(input_buffer)) {
+      return;
+    }
+  }
+  Schedule(consumed_cb);
+}
+
+bool OpusAudioDecoder::DecodeInternal(
+    const scoped_refptr<InputBuffer>& input_buffer) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(input_buffer);
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(!stream_ended_);
+
   scoped_refptr<DecodedAudio> decoded_audio = new DecodedAudio(
       audio_sample_info_.number_of_channels, GetSampleType(),
       kSbMediaAudioFrameStorageTypeInterleaved, input_buffer->timestamp(),
@@ -120,8 +135,7 @@
       frames_per_au_ < kMaxOpusFramesPerAU) {
     frames_per_au_ = kMaxOpusFramesPerAU;
     // Send to decode again with the new |frames_per_au_|.
-    Decode(input_buffer, consumed_cb);
-    return;
+    return DecodeInternal(input_buffer);
   }
   if (decoded_frames <= 0) {
     // When the following check fails, it indicates that |frames_per_au_| is
@@ -135,7 +149,7 @@
     error_cb_(kSbPlayerErrorDecode,
               FormatString("%s() failed with error code: %d",
                            kDecodeFunctionName, decoded_frames));
-    return;
+    return false;
   }
 
   frames_per_au_ = decoded_frames;
@@ -144,8 +158,8 @@
                           starboard::media::GetBytesPerSample(GetSampleType()));
 
   decoded_audios_.push(decoded_audio);
-  Schedule(consumed_cb);
-  Schedule(output_cb_);
+  output_cb_();
+  return true;
 }
 
 void OpusAudioDecoder::WriteEndOfStream() {
diff --git a/starboard/shared/opus/opus_audio_decoder.h b/starboard/shared/opus/opus_audio_decoder.h
index c878412..09687f3 100644
--- a/starboard/shared/opus/opus_audio_decoder.h
+++ b/starboard/shared/opus/opus_audio_decoder.h
@@ -44,13 +44,14 @@
 
   // AudioDecoder functions
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
   void Reset() override;
 
  private:
+  bool DecodeInternal(const scoped_refptr<InputBuffer>& input_buffer);
   static const int kMaxOpusFramesPerAU = 9600;
 
   SbMediaAudioSampleType GetSampleType() const;
diff --git a/starboard/shared/posix/free_space.cc b/starboard/shared/posix/free_space.cc
index f66de59..9291936 100644
--- a/starboard/shared/posix/free_space.cc
+++ b/starboard/shared/posix/free_space.cc
@@ -17,9 +17,9 @@
 #include <sys/statvfs.h>
 #include <vector>
 
-#include "cobalt/extension/free_space.h"
 #include "starboard/common/log.h"
 #include "starboard/configuration_constants.h"
+#include "starboard/extension/free_space.h"
 
 namespace starboard {
 namespace shared {
@@ -41,7 +41,9 @@
 }
 
 const CobaltExtensionFreeSpaceApi kFreeSpaceApi = {
-    kCobaltExtensionFreeSpaceName, 1, &MeasureFreeSpace,
+    kCobaltExtensionFreeSpaceName,
+    1,
+    &MeasureFreeSpace,
 };
 
 }  // namespace
diff --git a/starboard/shared/posix/memory_mapped_file.cc b/starboard/shared/posix/memory_mapped_file.cc
index c10bc8c..29fe545 100644
--- a/starboard/shared/posix/memory_mapped_file.cc
+++ b/starboard/shared/posix/memory_mapped_file.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/shared/posix/memory_mapped_file.h"
 
-#include "cobalt/extension/memory_mapped_file.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/memory_mapped_file.h"
 #include "starboard/shared/posix/page_internal.h"
 
 namespace starboard {
@@ -25,7 +25,9 @@
 namespace {
 
 const CobaltExtensionMemoryMappedFileApi kMemoryMappedFileApi = {
-    kCobaltExtensionMemoryMappedFileName, 1, &SbPageMapFile,
+    kCobaltExtensionMemoryMappedFileName,
+    1,
+    &SbPageMapFile,
 };
 
 }  // namespace
diff --git a/starboard/shared/starboard/crash_handler.cc b/starboard/shared/starboard/crash_handler.cc
index 0d6ec2d..a9c998f 100644
--- a/starboard/shared/starboard/crash_handler.cc
+++ b/starboard/shared/starboard/crash_handler.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/shared/starboard/crash_handler.h"
 
-#include "cobalt/extension/crash_handler.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/crash_handler.h"
 #include "starboard/memory.h"
 #include "third_party/crashpad/wrapper/wrapper.h"
 
@@ -33,7 +33,9 @@
 }
 
 const CobaltExtensionCrashHandlerApi kCrashHandlerApi = {
-    kCobaltExtensionCrashHandlerName, 2, &OverrideCrashpadAnnotations,
+    kCobaltExtensionCrashHandlerName,
+    2,
+    &OverrideCrashpadAnnotations,
     &SetString,
 };
 
diff --git a/starboard/shared/starboard/media/media_tests.gni b/starboard/shared/starboard/media/media_tests.gni
index 38c8042..a720a48 100644
--- a/starboard/shared/starboard/media/media_tests.gni
+++ b/starboard/shared/starboard/media/media_tests.gni
@@ -15,6 +15,7 @@
 media_tests_sources = [
   "//starboard/shared/starboard/media/avc_util_test.cc",
   "//starboard/shared/starboard/media/codec_util_test.cc",
+  "//starboard/shared/starboard/media/media_util_test.cc",
   "//starboard/shared/starboard/media/mime_type_test.cc",
   "//starboard/shared/starboard/media/video_capabilities_test.cc",
   "//starboard/shared/starboard/media/vp9_util_test.cc",
diff --git a/starboard/shared/starboard/media/media_util.cc b/starboard/shared/starboard/media/media_util.cc
index 58bb7bf..8ee17fc 100644
--- a/starboard/shared/starboard/media/media_util.cc
+++ b/starboard/shared/starboard/media/media_util.cc
@@ -67,7 +67,7 @@
 }
 
 VideoSampleInfo::VideoSampleInfo() {
-  memset(this, 0, sizeof(SbMediaAudioSampleInfo));
+  memset(this, 0, sizeof(SbMediaVideoSampleInfo));
   codec = kSbMediaVideoCodecNone;
 }
 
diff --git a/starboard/shared/starboard/media/media_util_test.cc b/starboard/shared/starboard/media/media_util_test.cc
new file mode 100644
index 0000000..5c048fd
--- /dev/null
+++ b/starboard/shared/starboard/media/media_util_test.cc
@@ -0,0 +1,40 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/media/media_util.h"
+
+#include "starboard/media.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace starboard {
+namespace shared {
+namespace starboard {
+namespace media {
+namespace {
+
+TEST(VideoSampleInfoTest, DefaultCtor) {
+  VideoSampleInfo video_sample_info;
+  EXPECT_EQ(video_sample_info.codec, kSbMediaVideoCodecNone);
+  // No other members should be accessed if `codec` is `kSbMediaVideoCodecNone`,
+  // however we still want to make sure that the pointer members are set to
+  // nullptr.
+  EXPECT_EQ(video_sample_info.mime, nullptr);
+  EXPECT_EQ(video_sample_info.max_video_capabilities, nullptr);
+}
+
+}  // namespace
+}  // namespace media
+}  // namespace starboard
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/shared/starboard/media/mime_util.cc b/starboard/shared/starboard/media/mime_util.cc
index 6c53aa7..c8656b1 100644
--- a/starboard/shared/starboard/media/mime_util.cc
+++ b/starboard/shared/starboard/media/mime_util.cc
@@ -156,7 +156,8 @@
   std::string cryptoblockformat =
       mime_type.GetParamStringValue("cryptoblockformat", "");
   if (!cryptoblockformat.empty()) {
-    if (mime_type.subtype() != "webm" || cryptoblockformat != "subsample") {
+    if ((mime_type.subtype() != "mp4" && mime_type.subtype() != "webm") ||
+        cryptoblockformat != "subsample") {
       return false;
     }
   }
diff --git a/starboard/shared/starboard/player/BUILD.gn b/starboard/shared/starboard/player/BUILD.gn
index 997a39a..a862317 100644
--- a/starboard/shared/starboard/player/BUILD.gn
+++ b/starboard/shared/starboard/player/BUILD.gn
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import("//starboard/shared/starboard/player/testdata/sha_files.gni")
+import("//starboard/shared/starboard/player/testdata/sha1_files.gni")
 
 static_library("video_dmp") {
   check_includes = false
@@ -35,11 +35,11 @@
   script = "//tools/download_from_gcs.py"
 
   sha_sources = []
-  foreach(sha_file, sha1_files) {
+  foreach(sha1_file, sha1_files) {
     sha_sources += [ string_join("/",
                                  [
                                    "testdata",
-                                   sha_file,
+                                   sha1_file,
                                  ]) ]
   }
 
diff --git a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
index 644ae2f..664544c 100644
--- a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
+++ b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
@@ -14,6 +14,8 @@
 
 #include "starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h"
 
+#include <utility>
+
 #include "starboard/audio_sink.h"
 #include "starboard/common/log.h"
 #include "starboard/common/reset_and_return.h"
@@ -59,35 +61,47 @@
   error_cb_ = error_cb;
 }
 
-void AdaptiveAudioDecoder::Decode(
-    const scoped_refptr<InputBuffer>& input_buffer,
-    const ConsumedCB& consumed_cb) {
+void AdaptiveAudioDecoder::Decode(const InputBuffers& input_buffers,
+                                  const ConsumedCB& consumed_cb) {
   SB_DCHECK(BelongsToCurrentThread());
   SB_DCHECK(!stream_ended_);
   SB_DCHECK(output_cb_);
   SB_DCHECK(error_cb_);
   SB_DCHECK(!flushing_);
-  SB_DCHECK(!pending_input_buffer_);
+  SB_DCHECK(pending_input_buffers_.empty());
   SB_DCHECK(!pending_consumed_cb_);
-  SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeAudio);
-  SB_DCHECK(input_buffer->audio_sample_info().codec != kSbMediaAudioCodecNone);
+  SB_DCHECK(!input_buffers.empty());
+  SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeAudio);
+  SB_DCHECK(input_buffers.front()->audio_sample_info().codec !=
+            kSbMediaAudioCodecNone);
 
   if (!audio_decoder_) {
-    InitializeAudioDecoder(input_buffer->audio_sample_info());
+    InitializeAudioDecoder(input_buffers.front()->audio_sample_info());
     if (audio_decoder_) {
-      audio_decoder_->Decode(input_buffer, consumed_cb);
+      audio_decoder_->Decode(input_buffers, consumed_cb);
     }
     return;
   }
   if (starboard::media::IsAudioSampleInfoSubstantiallyDifferent(
-          input_audio_sample_info_, input_buffer->audio_sample_info())) {
+          input_audio_sample_info_,
+          input_buffers.front()->audio_sample_info())) {
     flushing_ = true;
-    pending_input_buffer_ = input_buffer;
+    pending_input_buffers_ = input_buffers;
     pending_consumed_cb_ = consumed_cb;
     audio_decoder_->WriteEndOfStream();
-  } else {
-    audio_decoder_->Decode(input_buffer, consumed_cb);
+    return;
   }
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+  for (int i = 1; i < input_buffers.size(); i++) {
+    if (starboard::media::IsAudioSampleInfoSubstantiallyDifferent(
+            input_audio_sample_info_, input_buffers[i]->audio_sample_info())) {
+      error_cb_(kSbPlayerErrorDecode,
+                "Configuration switches should NOT happen within a batch.");
+      return;
+    }
+  }
+#endif
+  audio_decoder_->Decode(input_buffers, consumed_cb);
 }
 
 void AdaptiveAudioDecoder::WriteEndOfStream() {
@@ -95,7 +109,7 @@
   SB_DCHECK(!stream_ended_);
   SB_DCHECK(output_cb_);
   SB_DCHECK(error_cb_);
-  SB_DCHECK(!pending_input_buffer_);
+  SB_DCHECK(pending_input_buffers_.empty());
   SB_DCHECK(!pending_consumed_cb_);
 
   stream_ended_ = true;
@@ -139,7 +153,7 @@
   while (!decoded_audios_.empty()) {
     decoded_audios_.pop();
   }
-  pending_input_buffer_ = nullptr;
+  pending_input_buffers_.clear();
   pending_consumed_cb_ = nullptr;
   flushing_ = false;
   stream_ended_ = false;
@@ -213,8 +227,8 @@
       SB_DCHECK(audio_decoder_);
       TeardownAudioDecoder();
       flushing_ = false;
-      Decode(ResetAndReturn(&pending_input_buffer_),
-             ResetAndReturn(&pending_consumed_cb_));
+      InputBuffers input_buffers = std::move(pending_input_buffers_);
+      Decode(input_buffers, ResetAndReturn(&pending_consumed_cb_));
     } else {
       SB_DCHECK(stream_ended_);
       decoded_audios_.push(decoded_audio);
diff --git a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
index 8a997a8..0932965 100644
--- a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
@@ -55,7 +55,7 @@
   ~AdaptiveAudioDecoder() override;
 
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
@@ -82,7 +82,7 @@
   scoped_ptr<filter::AudioDecoder> audio_decoder_;
   scoped_ptr<filter::AudioResampler> resampler_;
   scoped_ptr<filter::AudioChannelLayoutMixer> channel_mixer_;
-  scoped_refptr<InputBuffer> pending_input_buffer_;
+  InputBuffers pending_input_buffers_;
   ConsumedCB pending_consumed_cb_;
   std::queue<scoped_refptr<DecodedAudio>> decoded_audios_;
   bool flushing_ = false;
diff --git a/starboard/shared/starboard/player/filter/audio_decoder_internal.h b/starboard/shared/starboard/player/filter/audio_decoder_internal.h
index 6d4fca9..4e59f55 100644
--- a/starboard/shared/starboard/player/filter/audio_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/audio_decoder_internal.h
@@ -40,6 +40,7 @@
 
   typedef ::starboard::shared::starboard::player::DecodedAudio DecodedAudio;
   typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+  typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
 
   virtual ~AudioDecoder() {}
 
@@ -52,12 +53,12 @@
   virtual void Initialize(const OutputCB& output_cb,
                           const ErrorCB& error_cb) = 0;
 
-  // Decode the encoded audio data stored in |input_buffer|.  Whenever the input
+  // Decode the encoded audio data stored in |input_buffers|. Whenever the input
   // is consumed and the decoder is ready to accept a new input, it calls
   // |consumed_cb|.
   // Note that |consumed_cb| is always called asynchronously on the calling job
   // queue.
-  virtual void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  virtual void Decode(const InputBuffers& input_buffer,
                       const ConsumedCB& consumed_cb) = 0;
 
   // Notice the object that there is no more input data unless Reset() is
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal.h b/starboard/shared/starboard/player/filter/audio_renderer_internal.h
index 4368a45..ab00627 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal.h
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal.h
@@ -31,13 +31,14 @@
   typedef ::starboard::shared::starboard::player::filter::PrerolledCB
       PrerolledCB;
   typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+  typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
 
   virtual ~AudioRenderer() {}
 
   virtual void Initialize(const ErrorCB& error_cb,
                           const PrerolledCB& prerolled_cb,
                           const EndedCB& ended_cb) = 0;
-  virtual void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) = 0;
+  virtual void WriteSamples(const InputBuffers& input_buffers) = 0;
   virtual void WriteEndOfStream() = 0;
 
   virtual void SetVolume(double volume) = 0;
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
index 709cd8b..3e09554 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
@@ -124,21 +124,20 @@
                        error_cb);
 }
 
-void AudioRendererPcm::WriteSample(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void AudioRendererPcm::WriteSamples(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
   SB_DCHECK(can_accept_more_data_);
 
   if (eos_state_ >= kEOSWrittenToDecoder) {
-    SB_LOG(ERROR) << "Appending audio sample at " << input_buffer->timestamp()
-                  << " after EOS reached.";
+    SB_LOG(ERROR) << "Appending audio samples from "
+                  << input_buffers.front()->timestamp() << " to "
+                  << input_buffers.back()->timestamp() << " after EOS reached.";
     return;
   }
 
   can_accept_more_data_ = false;
-
-  decoder_->Decode(input_buffer,
+  decoder_->Decode(input_buffers,
                    std::bind(&AudioRendererPcm::OnDecoderConsumed, this));
   first_input_written_ = true;
 }
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
index 7c37d7b..ed3b734 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
@@ -76,7 +76,7 @@
   void Initialize(const ErrorCB& error_cb,
                   const PrerolledCB& prerolled_cb,
                   const EndedCB& ended_cb) override;
-  void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteSamples(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
 
   void SetVolume(double volume) override;
diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
index b5d1d59..a632764 100644
--- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
+++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
@@ -14,6 +14,8 @@
 
 #include "starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h"
 
+#include <utility>
+
 #include "starboard/audio_sink.h"
 #include "starboard/common/log.h"
 #include "starboard/common/murmurhash2.h"
@@ -207,81 +209,90 @@
   return true;
 }
 
-bool FilterBasedPlayerWorkerHandler::WriteSample(
-    const scoped_refptr<InputBuffer>& input_buffer,
-    bool* written) {
-  SB_DCHECK(input_buffer);
+bool FilterBasedPlayerWorkerHandler::WriteSamples(
+    const InputBuffers& input_buffers,
+    int* samples_written) {
+  SB_DCHECK(!input_buffers.empty());
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(written != NULL);
+  SB_DCHECK(samples_written != NULL);
+  for (const auto& input_buffer : input_buffers) {
+    SB_DCHECK(input_buffer);
+  }
 
-  if (input_buffer->sample_type() == kSbMediaTypeAudio) {
+  *samples_written = 0;
+  if (input_buffers.front()->sample_type() == kSbMediaTypeAudio) {
     if (!audio_renderer_) {
       return false;
     }
 
-    *written = true;
-
     if (audio_renderer_->IsEndOfStreamWritten()) {
       SB_LOG(WARNING) << "Try to write audio sample after EOS is reached";
     } else {
       if (!audio_renderer_->CanAcceptMoreData()) {
-        *written = false;
         return true;
       }
-
-      if (input_buffer->drm_info()) {
-        if (!SbDrmSystemIsValid(drm_system_)) {
-          return false;
+      for (const auto& input_buffer : input_buffers) {
+        if (input_buffer->drm_info()) {
+          if (!SbDrmSystemIsValid(drm_system_)) {
+            return false;
+          }
+          DumpInputHash(input_buffer);
+          SbDrmSystemPrivate::DecryptStatus decrypt_status =
+              drm_system_->Decrypt(input_buffer);
+          if (decrypt_status == SbDrmSystemPrivate::kRetry) {
+            if (*samples_written > 0) {
+              audio_renderer_->WriteSamples(
+                  InputBuffers(input_buffers.begin(),
+                               input_buffers.begin() + *samples_written));
+            }
+            return true;
+          }
+          if (decrypt_status == SbDrmSystemPrivate::kFailure) {
+            return false;
+          }
         }
         DumpInputHash(input_buffer);
-        SbDrmSystemPrivate::DecryptStatus decrypt_status =
-            drm_system_->Decrypt(input_buffer);
-        if (decrypt_status == SbDrmSystemPrivate::kRetry) {
-          *written = false;
-          return true;
-        }
-        if (decrypt_status == SbDrmSystemPrivate::kFailure) {
-          *written = false;
-          return false;
-        }
+        ++*samples_written;
       }
-      DumpInputHash(input_buffer);
-      audio_renderer_->WriteSample(input_buffer);
+      audio_renderer_->WriteSamples(input_buffers);
     }
   } else {
-    SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeVideo);
+    SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo);
 
     if (!video_renderer_) {
       return false;
     }
 
-    *written = true;
-
     if (video_renderer_->IsEndOfStreamWritten()) {
       SB_LOG(WARNING) << "Try to write video sample after EOS is reached";
     } else {
       if (!video_renderer_->CanAcceptMoreData()) {
-        *written = false;
         return true;
       }
-      if (input_buffer->drm_info()) {
-        if (!SbDrmSystemIsValid(drm_system_)) {
-          return false;
+      for (const auto& input_buffer : input_buffers) {
+        if (input_buffer->drm_info()) {
+          if (!SbDrmSystemIsValid(drm_system_)) {
+            return false;
+          }
+          DumpInputHash(input_buffer);
+          SbDrmSystemPrivate::DecryptStatus decrypt_status =
+              drm_system_->Decrypt(input_buffer);
+          if (decrypt_status == SbDrmSystemPrivate::kRetry) {
+            if (*samples_written > 0) {
+              video_renderer_->WriteSamples(
+                  InputBuffers(input_buffers.begin(),
+                               input_buffers.begin() + *samples_written));
+            }
+            return true;
+          }
+          if (decrypt_status == SbDrmSystemPrivate::kFailure) {
+            return false;
+          }
         }
         DumpInputHash(input_buffer);
-        SbDrmSystemPrivate::DecryptStatus decrypt_status =
-            drm_system_->Decrypt(input_buffer);
-        if (decrypt_status == SbDrmSystemPrivate::kRetry) {
-          *written = false;
-          return true;
-        }
-        if (decrypt_status == SbDrmSystemPrivate::kFailure) {
-          *written = false;
-          return false;
-        }
+        ++*samples_written;
       }
-      DumpInputHash(input_buffer);
-      video_renderer_->WriteSample(input_buffer);
+      video_renderer_->WriteSamples(input_buffers);
     }
   }
 
diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
index d6c56b3..17df66d 100644
--- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
+++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
@@ -54,8 +54,8 @@
             UpdatePlayerStateCB update_player_state_cb,
             UpdatePlayerErrorCB update_player_error_cb) override;
   bool Seek(SbTime seek_to_time, int ticket) override;
-  bool WriteSample(const scoped_refptr<InputBuffer>& input_buffer,
-                   bool* written) override;
+  bool WriteSamples(const InputBuffers& input_buffers,
+                    int* samples_written) override;
   bool WriteEndOfStream(SbMediaType sample_type) override;
   bool SetPause(bool pause) override;
   bool SetPlaybackRate(double playback_rate) override;
diff --git a/starboard/shared/starboard/player/filter/mock_audio_decoder.h b/starboard/shared/starboard/player/filter/mock_audio_decoder.h
index c8b120c..5af346e 100644
--- a/starboard/shared/starboard/player/filter/mock_audio_decoder.h
+++ b/starboard/shared/starboard/player/filter/mock_audio_decoder.h
@@ -40,8 +40,7 @@
                    int samples_per_second) {}
 
   MOCK_METHOD2(Initialize, void(const OutputCB&, const ErrorCB&));
-  MOCK_METHOD2(Decode,
-               void(const scoped_refptr<InputBuffer>&, const ConsumedCB&));
+  MOCK_METHOD2(Decode, void(const InputBuffers&, const ConsumedCB&));
   MOCK_METHOD0(WriteEndOfStream, void());
   MOCK_METHOD1(Read, scoped_refptr<DecodedAudio>(int*));
   MOCK_METHOD0(Reset, void());
diff --git a/starboard/shared/starboard/player/filter/stub_audio_decoder.cc b/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
index 3bad2d9..b406e40 100644
--- a/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
+++ b/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
@@ -51,16 +51,19 @@
   error_cb_ = error_cb;
 }
 
-void StubAudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void StubAudioDecoder::Decode(const InputBuffers& input_buffers,
                               const ConsumedCB& consumed_cb) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
+  for (const auto& input_buffer : input_buffers) {
+    SB_DCHECK(input_buffer);
+  }
 
   if (!decoder_thread_) {
     decoder_thread_.reset(new JobThread("stub_audio_decoder"));
   }
   decoder_thread_->job_queue()->Schedule(std::bind(
-      &StubAudioDecoder::DecodeOneBuffer, this, input_buffer, consumed_cb));
+      &StubAudioDecoder::DecodeBuffers, this, input_buffers, consumed_cb));
 }
 
 void StubAudioDecoder::WriteEndOfStream() {
@@ -99,11 +102,17 @@
   CancelPendingJobs();
 }
 
-void StubAudioDecoder::DecodeOneBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer,
-    const ConsumedCB& consumed_cb) {
+void StubAudioDecoder::DecodeBuffers(const InputBuffers& input_buffers,
+                                     const ConsumedCB& consumed_cb) {
   SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+  for (const auto& input_buffer : input_buffers) {
+    DecodeOneBuffer(input_buffer);
+  }
+  decoder_thread_->job_queue()->Schedule(consumed_cb);
+}
 
+void StubAudioDecoder::DecodeOneBuffer(
+    const scoped_refptr<InputBuffer>& input_buffer) {
   // Values to represent what kind of dummy audio to fill the decoded audio
   // we produce with.
   enum FillType {
@@ -177,7 +186,6 @@
       decoder_thread_->job_queue()->Schedule(output_cb_);
     }
   }
-  decoder_thread_->job_queue()->Schedule(consumed_cb);
   last_input_buffer_ = input_buffer;
   total_input_count_++;
 }
diff --git a/starboard/shared/starboard/player/filter/stub_audio_decoder.h b/starboard/shared/starboard/player/filter/stub_audio_decoder.h
index f59c14c..887a67e 100644
--- a/starboard/shared/starboard/player/filter/stub_audio_decoder.h
+++ b/starboard/shared/starboard/player/filter/stub_audio_decoder.h
@@ -38,15 +38,16 @@
 
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
 
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffer,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
   void Reset() override;
 
  private:
-  void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer,
-                       const ConsumedCB& consumed_cb);
+  void DecodeBuffers(const InputBuffers& input_buffers,
+                     const ConsumedCB& consumed_cb);
+  void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer);
   void DecodeEndOfStream();
 
   OutputCB output_cb_;
diff --git a/starboard/shared/starboard/player/filter/stub_video_decoder.cc b/starboard/shared/starboard/player/filter/stub_video_decoder.cc
index 359b48c..7e3fc13 100644
--- a/starboard/shared/starboard/player/filter/stub_video_decoder.cc
+++ b/starboard/shared/starboard/player/filter/stub_video_decoder.cc
@@ -45,16 +45,15 @@
   return 12;
 }
 
-void StubVideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void StubVideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
 
   if (!decoder_thread_) {
     decoder_thread_.reset(new JobThread("stub_video_decoder"));
   }
   decoder_thread_->job_queue()->Schedule(
-      std::bind(&StubVideoDecoder::DecodeOneBuffer, this, input_buffer));
+      std::bind(&StubVideoDecoder::DecodeBuffers, this, input_buffers));
 }
 
 void StubVideoDecoder::WriteEndOfStream() {
@@ -82,41 +81,42 @@
   return kSbDecodeTargetInvalid;
 }
 
-void StubVideoDecoder::DecodeOneBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void StubVideoDecoder::DecodeBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
-  auto& video_sample_info = input_buffer->video_sample_info();
-  if (video_sample_info.is_key_frame) {
-    if (video_sample_info_ != video_sample_info) {
-      SB_LOG(INFO) << "New video sample info: " << video_sample_info;
-      video_sample_info_ = video_sample_info;
+  for (const auto& input_buffer : input_buffers) {
+    auto& video_sample_info = input_buffer->video_sample_info();
+    if (video_sample_info.is_key_frame) {
+      if (video_sample_info_ != video_sample_info) {
+        SB_LOG(INFO) << "New video sample info: " << video_sample_info;
+        video_sample_info_ = video_sample_info;
+      }
     }
-  }
 
-  // Defer sending frames out until we've accumulated a reasonable number.
-  // This allows for input buffers to be out of order, and we expect that
-  // after buffering 8 (arbitrarily chosen) that the first timestamp in the
-  // sorted buffer will be the "correct" timestamp to send out.
-  const int kMaxFramesToDelay = 8;
-  // Send kBufferFull on every 5th input buffer received, starting with the
-  // first.
-  const int kMaxInputBeforeBufferFull = 5;
-  scoped_refptr<VideoFrame> output_frame = NULL;
+    // Defer sending frames out until we've accumulated a reasonable number.
+    // This allows for input buffers to be out of order, and we expect that
+    // after buffering 8 (arbitrarily chosen) that the first timestamp in the
+    // sorted buffer will be the "correct" timestamp to send out.
+    const int kMaxFramesToDelay = 8;
+    // Send kBufferFull on every 5th input buffer received, starting with the
+    // first.
+    const int kMaxInputBeforeBufferFull = 5;
+    scoped_refptr<VideoFrame> output_frame = NULL;
 
-  output_frame_timestamps_.insert(input_buffer->timestamp());
-  if (output_frame_timestamps_.size() > kMaxFramesToDelay) {
-    output_frame = CreateOutputFrame(*output_frame_timestamps_.begin());
-    output_frame_timestamps_.erase(output_frame_timestamps_.begin());
-  }
+    output_frame_timestamps_.insert(input_buffer->timestamp());
+    if (output_frame_timestamps_.size() > kMaxFramesToDelay) {
+      output_frame = CreateOutputFrame(*output_frame_timestamps_.begin());
+      output_frame_timestamps_.erase(output_frame_timestamps_.begin());
+    }
 
-  if (total_input_count_ % kMaxInputBeforeBufferFull == 0) {
+    if (total_input_count_ % kMaxInputBeforeBufferFull == 0) {
+      total_input_count_++;
+      decoder_status_cb_(kBufferFull, output_frame);
+      decoder_status_cb_(kNeedMoreInput, nullptr);
+      continue;
+    }
     total_input_count_++;
-    decoder_status_cb_(kBufferFull, output_frame);
-    decoder_status_cb_(kNeedMoreInput, nullptr);
-    return;
+    decoder_status_cb_(kNeedMoreInput, output_frame);
   }
-  total_input_count_++;
-  decoder_status_cb_(kNeedMoreInput, output_frame);
 }
 
 void StubVideoDecoder::DecodeEndOfStream() {
diff --git a/starboard/shared/starboard/player/filter/stub_video_decoder.h b/starboard/shared/starboard/player/filter/stub_video_decoder.h
index 0199274..fc66d7f 100644
--- a/starboard/shared/starboard/player/filter/stub_video_decoder.h
+++ b/starboard/shared/starboard/player/filter/stub_video_decoder.h
@@ -41,15 +41,14 @@
   SbTime GetPrerollTimeout() const override;
   size_t GetMaxNumberOfCachedFrames() const override;
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
 
   SbDecodeTarget GetCurrentDecodeTarget() override;
 
  private:
-  void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+  void DecodeBuffers(const InputBuffers& input_buffers);
   void DecodeEndOfStream();
 
   scoped_refptr<VideoFrame> CreateOutputFrame(SbTime timestamp) const;
diff --git a/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
index 4c77870..551a0ad 100644
--- a/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <algorithm>
 #include <cmath>
 #include <deque>
 #include <functional>
 #include <memory>
 #include <numeric>
 #include <queue>
+#include <string>
 
 #include "starboard/common/mutex.h"
 #include "starboard/common/scoped_ptr.h"
@@ -42,11 +44,11 @@
 namespace testing {
 namespace {
 
+using std::string;
+using std::vector;
 using ::testing::Bool;
 using ::testing::Combine;
 using ::testing::ValuesIn;
-using std::vector;
-using std::string;
 using video_dmp::VideoDmpReader;
 
 const SbTimeMonotonic kWaitForNextEventTimeOut = 5 * kSbTimeSecond;
@@ -134,7 +136,7 @@
 
     can_accept_more_input_ = false;
     audio_decoder_->Decode(
-        GetAudioInputBuffer(dmp_reader, buffer_index),
+        {GetAudioInputBuffer(dmp_reader, buffer_index)},
         std::bind(&AdaptiveAudioDecoderTest::OnConsumed, this));
   }
 
@@ -289,6 +291,28 @@
   bool can_accept_more_input_ = true;
 };
 
+std::string GetAdaptiveAudioDecoderTestConfigName(
+    ::testing::TestParamInfo<std::tuple<vector<const char*>, bool>> info) {
+  std::vector<const char*> filenames(std::get<0>(info.param));
+  bool using_stub_decoder = std::get<1>(info.param);
+  std::string config_name;
+
+  for (auto name : filenames) {
+    config_name += std::string(name) + "__to__";
+  }
+  if (!config_name.empty()) {
+    // Remove trailing "__to__".
+    config_name.erase(config_name.end() - 6, config_name.end());
+
+    std::replace(config_name.begin(), config_name.end(), '.', '_');
+    if (using_stub_decoder) {
+      config_name += "__stub";
+    }
+  }
+
+  return config_name;
+}
+
 TEST_P(AdaptiveAudioDecoderTest, SingleInput) {
   SbTime playing_duration = 0;
   // Skip buffer 0, as the difference between first and second opus buffer
@@ -387,13 +411,14 @@
                            test_params.back().rend());
 
   SB_LOG_IF(INFO, test_params.empty())
-      << "Test params for AdaptiveAudioDecodeTests is empty.";
+      << "Test params for AdaptiveAudioDecoderTests is empty.";
   return test_params;
 }
 
 INSTANTIATE_TEST_CASE_P(AdaptiveAudioDecoderTests,
                         AdaptiveAudioDecoderTest,
-                        Combine(ValuesIn(GetSupportedTests()), Bool()));
+                        Combine(ValuesIn(GetSupportedTests()), Bool()),
+                        GetAdaptiveAudioDecoderTestConfigName);
 
 }  // namespace
 }  // namespace testing
diff --git a/starboard/shared/starboard/player/filter/testing/audio_channel_layout_mixer_test.cc b/starboard/shared/starboard/player/filter/testing/audio_channel_layout_mixer_test.cc
index 4dad168..56a8985 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_channel_layout_mixer_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_channel_layout_mixer_test.cc
@@ -12,14 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "starboard/shared/starboard/player/filter/audio_channel_layout_mixer.h"
+
 #include <cmath>
 #include <functional>
 #include <numeric>
+#include <string>
 
 #include "starboard/common/ref_counted.h"
 #include "starboard/common/scoped_ptr.h"
 #include "starboard/shared/starboard/player/decoded_audio_internal.h"
-#include "starboard/shared/starboard/player/filter/audio_channel_layout_mixer.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace starboard {
@@ -158,6 +160,22 @@
   SbMediaAudioFrameStorageType storage_type_;
 };
 
+std::string GetAudioChannelLayoutMixerTestConfigName(
+    ::testing::TestParamInfo<std::tuple<SbMediaAudioSampleType,
+                                        SbMediaAudioFrameStorageType>> info) {
+  SbMediaAudioSampleType sample_type = std::get<0>(info.param);
+  SbMediaAudioFrameStorageType frame_storage_type = std::get<1>(info.param);
+
+  return FormatString(
+      "%s_%s",
+      sample_type == kSbMediaAudioSampleTypeInt16Deprecated
+          ? "SampleTypeInt16"
+          : "SampleTypeFloat32",
+      frame_storage_type == kSbMediaAudioFrameStorageTypeInterleaved
+          ? "StorageTypeInterleaved"
+          : "StorageTypePlanar");
+}
+
 TEST_P(AudioChannelLayoutMixerTest, MixToMono) {
   scoped_ptr<AudioChannelLayoutMixer> mixer =
       AudioChannelLayoutMixer::Create(sample_type_, storage_type_, 1);
@@ -332,7 +350,8 @@
                         Combine(Values(kSbMediaAudioSampleTypeInt16Deprecated,
                                        kSbMediaAudioSampleTypeFloat32),
                                 Values(kSbMediaAudioFrameStorageTypeInterleaved,
-                                       kSbMediaAudioFrameStorageTypePlanar)));
+                                       kSbMediaAudioFrameStorageTypePlanar)),
+                        GetAudioChannelLayoutMixerTestConfigName);
 
 }  // namespace
 }  // namespace testing
diff --git a/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc b/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
index e92e02d..ab08076 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
@@ -92,7 +92,7 @@
       return;
     }
     if (current_input_buffer_index_ < number_of_inputs_) {
-      audio_decoder_->Decode(GetAudioInputBuffer(current_input_buffer_index_),
+      audio_decoder_->Decode({GetAudioInputBuffer(current_input_buffer_index_)},
                              std::bind(&AudioDecoderHelper::OnConsumed, this));
       ++current_input_buffer_index_;
     } else {
diff --git a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
index eee3730..2904b6a 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
@@ -14,9 +14,13 @@
 
 #include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
 
+#include <algorithm>
 #include <deque>
 #include <functional>
 #include <map>
+#include <string>
+#include <utility>
+#include <vector>
 
 #include "starboard/common/condition_variable.h"
 #include "starboard/common/media.h"
@@ -51,7 +55,7 @@
 const SbTimeMonotonic kWaitForNextEventTimeOut = 5 * kSbTimeSecond;
 
 class AudioDecoderTest
-    : public ::testing::TestWithParam<std::tuple<const char*, bool> > {
+    : public ::testing::TestWithParam<std::tuple<const char*, bool>> {
  public:
   AudioDecoderTest()
       : test_filename_(std::get<0>(GetParam())),
@@ -136,8 +140,7 @@
     can_accept_more_input_ = false;
 
     last_input_buffer_ = GetAudioInputBuffer(index);
-
-    audio_decoder_->Decode(last_input_buffer_, consumed_cb());
+    audio_decoder_->Decode({last_input_buffer_}, consumed_cb());
   }
 
   // This has to be called when OnOutput() is called.
@@ -306,8 +309,7 @@
     if (iter != invalid_inputs_.end()) {
       std::vector<uint8_t> content(input_buffer->size(), iter->second);
       // Replace the content with invalid data.
-      input_buffer->SetDecryptedContent(content.data(),
-                                        static_cast<int>(content.size()));
+      input_buffer->SetDecryptedContent(std::move(content));
     }
     return input_buffer;
   }
@@ -379,6 +381,16 @@
   bool first_output_received_ = false;
 };
 
+std::string GetAudioDecoderTestConfigName(
+    ::testing::TestParamInfo<std::tuple<const char*, bool>> info) {
+  std::string filename(std::get<0>(info.param));
+  bool using_stub_decoder = std::get<1>(info.param);
+
+  std::replace(filename.begin(), filename.end(), '.', '_');
+
+  return filename + (using_stub_decoder ? "__stub" : "");
+}
+
 TEST_P(AudioDecoderTest, MultiDecoders) {
   const int kDecodersToCreate = 100;
   const int kMinimumNumberOfExtraDecodersRequired = 3;
@@ -638,7 +650,8 @@
     Combine(ValuesIn(GetSupportedAudioTestFiles(kIncludeHeaac,
                                                 6,
                                                 "audiopassthrough=false")),
-            Bool()));
+            Bool()),
+    GetAudioDecoderTestConfigName);
 
 }  // namespace
 }  // namespace testing
diff --git a/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc b/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
index ae7e57c..bfeb7a7 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
@@ -155,9 +155,11 @@
     ASSERT_FALSE(consumed_cb_);
 
     buffers_in_decoder_.insert(input_buffer->data());
-    EXPECT_CALL(*audio_decoder_, Decode(input_buffer, _))
+    InputBuffers input_buffers;
+    input_buffers.push_back(input_buffer);
+    EXPECT_CALL(*audio_decoder_, Decode(input_buffers, _))
         .WillOnce(SaveArg<1>(&consumed_cb_));
-    audio_renderer_->WriteSample(input_buffer);
+    audio_renderer_->WriteSamples(input_buffers);
     job_queue_.RunUntilIdle();
 
     ASSERT_TRUE(consumed_cb_);
diff --git a/starboard/shared/starboard/player/filter/testing/player_components_test.cc b/starboard/shared/starboard/player/filter/testing/player_components_test.cc
index 11f880a..b7287bb 100644
--- a/starboard/shared/starboard/player/filter/testing/player_components_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/player_components_test.cc
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "starboard/common/scoped_ptr.h"
+#include "starboard/common/string.h"
 #include "starboard/media.h"
 #include "starboard/player.h"
 #include "starboard/shared/starboard/player/filter/testing/test_util.h"
@@ -37,11 +38,11 @@
 namespace {
 
 using ::starboard::testing::FakeGraphicsContextProvider;
-using std::placeholders::_1;
-using std::placeholders::_2;
 using std::string;
 using std::unique_ptr;
 using std::vector;
+using std::placeholders::_1;
+using std::placeholders::_2;
 using ::testing::ValuesIn;
 using video_dmp::VideoDmpReader;
 
@@ -259,6 +260,11 @@
     return std::min(next_timestamps[0], next_timestamps[1]);
   }
 
+  SbTime GetMaxWrittenBufferTimestamp() const {
+    return std::max(GetCurrentVideoBufferTimestamp(),
+                    GetCurrentAudioBufferTimestamp());
+  }
+
   void WriteDataUntilPrerolled(SbTime timeout = kDefaultPrerollTimeOut) {
     SbTimeMonotonic start_time = SbTimeGetMonotonicNow();
     SbTime max_timestamp = GetMediaTime() + kMaxWriteAheadDuration;
@@ -373,7 +379,11 @@
     }
     current_time = GetMediaTime();
     // TODO: investigate and reduce the tolerance.
-    ASSERT_LE(std::abs(current_time - duration), 500 * kSbTimeMillisecond);
+    ASSERT_LE(std::abs(current_time - duration), 500 * kSbTimeMillisecond)
+        << "Media time difference is too large, buffered audio("
+        << GetCurrentAudioBufferTimestamp() << "), buffered video ("
+        << GetCurrentVideoBufferTimestamp() << "), current media time is "
+        << GetMediaTime() << ".";
   }
 
   // This function needs to be called periodically to keep player components
@@ -395,8 +405,8 @@
  private:
   // We won't write audio data more than 1s ahead of current media time in
   // cobalt. So, to test with the same condition, we limit max inputs ahead to
-  // 1s in the tests.
-  const SbTime kMaxWriteAheadDuration = kSbTimeSecond;
+  // 1.5s in the tests.
+  const SbTime kMaxWriteAheadDuration = kSbTimeMillisecond * 1500;
 
   void OnError(SbPlayerError error, const std::string& error_message) {
     has_error_ = true;
@@ -447,13 +457,13 @@
     if (GetAudioRenderer() && GetAudioRenderer()->CanAcceptMoreData() &&
         audio_index_ < audio_reader_->number_of_audio_buffers() &&
         GetCurrentAudioBufferTimestamp() < max_timestamp) {
-      GetAudioRenderer()->WriteSample(GetAudioInputBuffer(audio_index_++));
+      GetAudioRenderer()->WriteSamples({GetAudioInputBuffer(audio_index_++)});
       input_buffer_written = true;
     }
     if (GetVideoRenderer() && GetVideoRenderer()->CanAcceptMoreData() &&
         video_index_ < video_reader_->number_of_video_buffers() &&
         GetCurrentVideoBufferTimestamp() < max_timestamp) {
-      GetVideoRenderer()->WriteSample(GetVideoInputBuffer(video_index_++));
+      GetVideoRenderer()->WriteSamples({GetVideoInputBuffer(video_index_++)});
       input_buffer_written = true;
     }
     if (input_buffer_written) {
@@ -480,6 +490,22 @@
   bool video_ended_ = false;
 };
 
+std::string GetPlayerComponentsTestConfigName(
+    ::testing::TestParamInfo<PlayerComponentsTestParam> info) {
+  std::string audio_filename(std::get<0>(info.param));
+  std::string video_filename(std::get<1>(info.param));
+  SbPlayerOutputMode output_mode = std::get<2>(info.param);
+
+  std::string config_name(FormatString(
+      "%s_%s_%s", audio_filename.empty() ? "null" : audio_filename.c_str(),
+      video_filename.empty() ? "null" : video_filename.c_str(),
+      output_mode == kSbPlayerOutputModeDecodeToTexture ? "DecodeToTexture"
+                                                        : "Punchout"));
+
+  std::replace(config_name.begin(), config_name.end(), '.', '_');
+  return config_name;
+}
+
 TEST_P(PlayerComponentsTest, Preroll) {
   Seek(0);
   ASSERT_NO_FATAL_FAILURE(WriteDataUntilPrerolled());
@@ -493,11 +519,10 @@
 
   SbTimeMonotonic play_requested_at = SbTimeGetMonotonicNow();
   Play();
-  SbTime media_duration = std::max(GetCurrentVideoBufferTimestamp(),
-                                   GetCurrentAudioBufferTimestamp());
-  media_duration = std::max(kSbTimeSecond, media_duration);
+  SbTime eos_timestamp =
+      std::max(kSbTimeSecond, GetMaxWrittenBufferTimestamp());
 
-  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(media_duration));
+  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
   ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
 
   // TODO: investigate and reduce the tolerance.
@@ -544,9 +569,7 @@
   ASSERT_EQ(media_time, GetMediaTime());
 
   Play();
-  SbTime duration = std::max(GetCurrentAudioBufferTimestamp(),
-                             GetCurrentVideoBufferTimestamp());
-  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(duration));
+  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(GetMaxWrittenBufferTimestamp()));
   ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
 }
 
@@ -625,7 +648,9 @@
   ASSERT_FALSE(IsPlaying());
 
   Play();
-  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(seek_to_time + kSbTimeSecond));
+  SbTime eos_timestamp =
+      std::max(GetMaxWrittenBufferTimestamp(), seek_to_time + kSbTimeSecond);
+  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
   ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
 }
 
@@ -647,7 +672,9 @@
   ASSERT_FALSE(IsPlaying());
 
   Play();
-  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(seek_to_time + kSbTimeSecond));
+  SbTime eos_timestamp =
+      std::max(GetMaxWrittenBufferTimestamp(), seek_to_time + kSbTimeSecond);
+  ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
   ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
 }
 
@@ -717,7 +744,8 @@
 
 INSTANTIATE_TEST_CASE_P(PlayerComponentsTests,
                         PlayerComponentsTest,
-                        ValuesIn(GetSupportedCreationParameters()));
+                        ValuesIn(GetSupportedCreationParameters()),
+                        GetPlayerComponentsTestConfigName);
 }  // namespace
 }  // namespace testing
 }  // namespace filter
diff --git a/starboard/shared/starboard/player/filter/testing/video_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/video_decoder_test.cc
index 03f0c08..8d38b52 100644
--- a/starboard/shared/starboard/player/filter/testing/video_decoder_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/video_decoder_test.cc
@@ -76,6 +76,23 @@
   VideoDecoderTestFixture fixture_;
 };
 
+std::string GetVideoDecoderTestConfigName(
+    ::testing::TestParamInfo<std::tuple<VideoTestParam, bool>> info) {
+  const char* filename = std::get<0>(std::get<0>(info.param));
+  SbPlayerOutputMode output_mode = std::get<1>(std::get<0>(info.param));
+  bool using_stub_decoder = std::get<1>(info.param);
+
+  std::string config_name(FormatString(
+      "%s_%s%s", filename,
+      output_mode == kSbPlayerOutputModeDecodeToTexture ? "DecodeToTexture"
+                                                        : "Punchout",
+      using_stub_decoder ? "__stub" : ""));
+
+  std::replace(config_name.begin(), config_name.end(), '.', '_');
+
+  return config_name;
+}
+
 TEST_P(VideoDecoderTest, PrerollFrameCount) {
   EXPECT_GT(fixture_.video_decoder()->GetPrerollFrameCount(), 0);
 }
@@ -484,7 +501,8 @@
 
 INSTANTIATE_TEST_CASE_P(VideoDecoderTests,
                         VideoDecoderTest,
-                        Combine(ValuesIn(GetSupportedVideoTests()), Bool()));
+                        Combine(ValuesIn(GetSupportedVideoTests()), Bool()),
+                        GetVideoDecoderTestConfigName);
 
 }  // namespace
 }  // namespace testing
diff --git a/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc b/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
index f274a3e..618359c 100644
--- a/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
+++ b/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
@@ -20,6 +20,7 @@
 #include <map>
 #include <set>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "starboard/common/condition_variable.h"
@@ -232,8 +233,7 @@
     need_more_input_ = false;
     outstanding_inputs_.insert(input_buffer->timestamp());
   }
-
-  video_decoder_->WriteInputBuffer(input_buffer);
+  video_decoder_->WriteInputBuffers({input_buffer});
 }
 
 void VideoDecoderTestFixture::WriteEndOfStream() {
@@ -369,8 +369,7 @@
   if (iter != invalid_inputs_.end()) {
     std::vector<uint8_t> content(input_buffer->size(), iter->second);
     // Replace the content with invalid data.
-    input_buffer->SetDecryptedContent(content.data(),
-                                      static_cast<int>(content.size()));
+    input_buffer->SetDecryptedContent(std::move(content));
   }
   return input_buffer;
 }
diff --git a/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc b/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
index f1f0288..3f860b6 100644
--- a/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
+++ b/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
@@ -29,12 +29,13 @@
 
 namespace {
 
-using starboard::shared::starboard::player::video_dmp::VideoDmpReader;
+using starboard::scoped_ptr;
+using starboard::shared::starboard::player::InputBuffer;
+using starboard::shared::starboard::player::InputBuffers;
+using starboard::shared::starboard::player::JobThread;
 using starboard::shared::starboard::player::filter::AudioRenderer;
 using starboard::shared::starboard::player::filter::PlayerComponents;
-using starboard::shared::starboard::player::InputBuffer;
-using starboard::shared::starboard::player::JobThread;
-using starboard::scoped_ptr;
+using starboard::shared::starboard::player::video_dmp::VideoDmpReader;
 
 #ifdef SB_MEDIA_PLAYER_THREAD_STACK_SIZE
 const int kJobThreadStackSize = SB_MEDIA_PLAYER_THREAD_STACK_SIZE;
@@ -50,11 +51,11 @@
   std::vector<char> content_path(kPathSize);
   SB_CHECK(SbSystemGetPath(kSbSystemPathContentDirectory, content_path.data(),
                            kPathSize));
-  std::string directory_path =
-      std::string(content_path.data()) + kSbFileSepChar + "test" +
-      kSbFileSepChar + "starboard" + kSbFileSepChar + "shared" +
-      kSbFileSepChar + "starboard" + kSbFileSepChar + "player" +
-      kSbFileSepChar + "testdata";
+  std::string directory_path = std::string(content_path.data()) +
+                               kSbFileSepChar + "test" + kSbFileSepChar +
+                               "starboard" + kSbFileSepChar + "shared" +
+                               kSbFileSepChar + "starboard" + kSbFileSepChar +
+                               "player" + kSbFileSepChar + "testdata";
 
   SB_CHECK(SbDirectoryCanOpen(directory_path.c_str()))
       << "Cannot open directory " << directory_path;
@@ -73,8 +74,7 @@
 
 static void DeallocateSampleFunc(SbPlayer player,
                                  void* context,
-                                 const void* sample_buffer) {
-}
+                                 const void* sample_buffer) {}
 
 starboard::scoped_refptr<InputBuffer> GetAudioInputBuffer(size_t index) {
   auto player_sample_info =
@@ -93,9 +93,12 @@
     s_player_components->GetAudioRenderer()->WriteEndOfStream();
     return;
   } else {
+    InputBuffers input_buffers;
     auto input_buffer = GetAudioInputBuffer(s_audio_sample_index);
+
     s_duration = input_buffer->timestamp();
-    s_player_components->GetAudioRenderer()->WriteSample(input_buffer);
+    input_buffers.push_back(std::move(input_buffer));
+    s_player_components->GetAudioRenderer()->WriteSamples(input_buffers);
     ++s_audio_sample_index;
   }
 
diff --git a/starboard/shared/starboard/player/filter/video_decoder_internal.h b/starboard/shared/starboard/player/filter/video_decoder_internal.h
index 0d0db76..f30a843 100644
--- a/starboard/shared/starboard/player/filter/video_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/video_decoder_internal.h
@@ -35,6 +35,7 @@
 class VideoDecoder {
  public:
   typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+  typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
   typedef ::starboard::shared::starboard::player::filter::VideoFrame VideoFrame;
 
   enum Status {
@@ -86,9 +87,8 @@
   // acceptable playback performance.  A number greater than 6 is recommended.
   virtual size_t GetMaxNumberOfCachedFrames() const = 0;
 
-  // Send encoded video frame stored in |input_buffer| to decode.
-  virtual void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) = 0;
+  // Send encoded video frames stored in |input_buffers| to decode.
+  virtual void WriteInputBuffers(const InputBuffers& input_buffers) = 0;
 
   // Note that there won't be more input data unless Reset() is called.
   // |decoder_status_cb| can still be called multiple times afterwards to
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal.h b/starboard/shared/starboard/player/filter/video_renderer_internal.h
index 471dfc7..12546ee 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal.h
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal.h
@@ -39,7 +39,8 @@
                           const EndedCB& ended_cb) = 0;
   virtual int GetDroppedFrames() const = 0;
 
-  virtual void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) = 0;
+  virtual void WriteSamples(const InputBuffers& input_buffers) = 0;
+
   virtual void WriteEndOfStream() = 0;
 
   virtual void Seek(SbTime seek_to_time) = 0;
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
index 0e11da1..ccb7ba8 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
@@ -101,10 +101,12 @@
   }
 }
 
-void VideoRendererImpl::WriteSample(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoRendererImpl::WriteSamples(const InputBuffers& input_buffers) {
   SB_DCHECK(BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
+  for (const auto& input_buffer : input_buffers) {
+    SB_DCHECK(input_buffer);
+  }
 
 #if SB_PLAYER_FILTER_ENABLE_STATE_CHECK
   buffering_state_ = kWaitForConsumption;
@@ -112,8 +114,9 @@
 #endif  // SB_PLAYER_FILTER_ENABLE_STATE_CHECK
 
   if (end_of_stream_written_.load()) {
-    SB_LOG(ERROR) << "Appending video sample at " << input_buffer->timestamp()
-                  << " after EOS reached.";
+    SB_LOG(ERROR) << "Appending video samples from "
+                  << input_buffers.front()->timestamp() << " to "
+                  << input_buffers.back()->timestamp() << " after EOS reached.";
     return;
   }
 
@@ -127,7 +130,7 @@
   SB_DCHECK(need_more_input_.load());
   need_more_input_.store(false);
 
-  decoder_->WriteInputBuffer(input_buffer);
+  decoder_->WriteInputBuffers(input_buffers);
 }
 
 void VideoRendererImpl::WriteEndOfStream() {
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
index a2fd2e4..e456eab 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
@@ -61,7 +61,8 @@
     return algorithm_->GetDroppedFrames();
   }
 
-  void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteSamples(const InputBuffers& input_buffers) override;
+
   void WriteEndOfStream() override;
 
   void Seek(SbTime seek_to_time) override;
diff --git a/starboard/shared/starboard/player/input_buffer_internal.cc b/starboard/shared/starboard/player/input_buffer_internal.cc
index b9b22c2..f3ec61e 100644
--- a/starboard/shared/starboard/player/input_buffer_internal.cc
+++ b/starboard/shared/starboard/player/input_buffer_internal.cc
@@ -18,6 +18,7 @@
 #include <cstring>
 #include <numeric>
 #include <sstream>
+#include <utility>
 
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
@@ -65,18 +66,17 @@
   DeallocateSampleBuffer(data_);
 }
 
-void InputBuffer::SetDecryptedContent(const void* buffer, int size) {
-  SB_DCHECK(size == size_);
+void InputBuffer::SetDecryptedContent(std::vector<uint8_t> decrypted_content) {
+  SB_DCHECK(decrypted_content.size() == size_);
   DeallocateSampleBuffer(data_);
 
-  if (size > 0) {
-    flattened_data_.clear();
-    flattened_data_.assign(static_cast<const uint8_t*>(buffer),
-                           static_cast<const uint8_t*>(buffer) + size);
-    data_ = flattened_data_.data();
-  } else {
+  if (decrypted_content.empty()) {
     data_ = NULL;
+  } else {
+    flattened_data_ = std::move(decrypted_content);
+    data_ = flattened_data_.data();
   }
+
   has_drm_info_ = false;
 }
 
diff --git a/starboard/shared/starboard/player/input_buffer_internal.h b/starboard/shared/starboard/player/input_buffer_internal.h
index 976dd9f..3da8d19 100644
--- a/starboard/shared/starboard/player/input_buffer_internal.h
+++ b/starboard/shared/starboard/player/input_buffer_internal.h
@@ -58,7 +58,7 @@
   const SbDrmSampleInfo* drm_info() const {
     return has_drm_info_ ? &drm_info_ : NULL;
   }
-  void SetDecryptedContent(const void* buffer, int size);
+  void SetDecryptedContent(std::vector<uint8_t> decrypted_content);
 
   std::string ToString() const;
 
@@ -88,6 +88,8 @@
   void operator=(const InputBuffer&) = delete;
 };
 
+typedef std::vector<scoped_refptr<InputBuffer>> InputBuffers;
+
 }  // namespace player
 }  // namespace starboard
 }  // namespace shared
diff --git a/starboard/shared/starboard/player/player_internal.cc b/starboard/shared/starboard/player/player_internal.cc
index 45333ad..a25f74d 100644
--- a/starboard/shared/starboard/player/player_internal.cc
+++ b/starboard/shared/starboard/player/player_internal.cc
@@ -15,6 +15,7 @@
 #include "starboard/shared/starboard/player/player_internal.h"
 
 #include <functional>
+#include <utility>
 
 #include "starboard/common/log.h"
 #if SB_PLAYER_ENABLE_VIDEO_DUMPER
@@ -24,6 +25,7 @@
 namespace {
 
 using starboard::shared::starboard::player::InputBuffer;
+using starboard::shared::starboard::player::InputBuffers;
 using std::placeholders::_1;
 using std::placeholders::_2;
 using std::placeholders::_3;
@@ -100,19 +102,29 @@
   worker_->Seek(seek_to_time, ticket);
 }
 
-void SbPlayerPrivate::WriteSample(const SbPlayerSampleInfo& sample_info) {
-  if (sample_info.type == kSbMediaTypeVideo) {
-    ++total_video_frames_;
-    frame_width_ = sample_info.video_sample_info.frame_width;
-    frame_height_ = sample_info.video_sample_info.frame_height;
+void SbPlayerPrivate::WriteSamples(const SbPlayerSampleInfo* sample_infos,
+                                   int number_of_sample_infos) {
+  SB_DCHECK(sample_infos);
+  SB_DCHECK(number_of_sample_infos > 0);
+
+  if (sample_infos[0].type == kSbMediaTypeVideo) {
+    const auto& last_sample_info = sample_infos[number_of_sample_infos - 1];
+    total_video_frames_ += number_of_sample_infos;
+    frame_width_ = last_sample_info.video_sample_info.frame_width;
+    frame_height_ = last_sample_info.video_sample_info.frame_height;
   }
-  starboard::scoped_refptr<InputBuffer> input_buffer =
-      new InputBuffer(sample_deallocate_func_, this, context_, sample_info);
+
+  InputBuffers input_buffers;
+  input_buffers.reserve(number_of_sample_infos);
+  for (int i = 0; i < number_of_sample_infos; i++) {
+    input_buffers.push_back(new InputBuffer(sample_deallocate_func_, this,
+                                            context_, sample_infos[i]));
 #if SB_PLAYER_ENABLE_VIDEO_DUMPER
-  using ::starboard::shared::starboard::player::video_dmp::VideoDmpWriter;
-  VideoDmpWriter::OnPlayerWriteSample(this, input_buffer);
+    using ::starboard::shared::starboard::player::video_dmp::VideoDmpWriter;
+    VideoDmpWriter::OnPlayerWriteSample(this, input_buffers.back());
 #endif  // SB_PLAYER_ENABLE_VIDEO_DUMPER
-  worker_->WriteSample(input_buffer);
+  }
+  worker_->WriteSamples(std::move(input_buffers));
 }
 
 void SbPlayerPrivate::WriteEndOfStream(SbMediaType stream_type) {
diff --git a/starboard/shared/starboard/player/player_internal.h b/starboard/shared/starboard/player/player_internal.h
index 7033737..a6ffecf 100644
--- a/starboard/shared/starboard/player/player_internal.h
+++ b/starboard/shared/starboard/player/player_internal.h
@@ -42,7 +42,8 @@
       starboard::scoped_ptr<PlayerWorker::Handler> player_worker_handler);
 
   void Seek(SbTime seek_to_time, int ticket);
-  void WriteSample(const SbPlayerSampleInfo& sample_info);
+  void WriteSamples(const SbPlayerSampleInfo* sample_infos,
+                    int number_of_sample_infos);
   void WriteEndOfStream(SbMediaType stream_type);
   void SetBounds(int z_index, int x, int y, int width, int height);
 
diff --git a/starboard/shared/starboard/player/player_worker.cc b/starboard/shared/starboard/player/player_worker.cc
index 2cec1a6..5ee9bc0 100644
--- a/starboard/shared/starboard/player/player_worker.cc
+++ b/starboard/shared/starboard/player/player_worker.cc
@@ -228,8 +228,9 @@
     job_queue_->RemoveJobByToken(write_pending_sample_job_token_);
     write_pending_sample_job_token_.ResetToInvalid();
   }
-  pending_audio_buffer_ = NULL;
-  pending_video_buffer_ = NULL;
+
+  pending_audio_buffers_.clear();
+  pending_video_buffers_.clear();
 
   if (!handler_->Seek(seek_to_time, ticket)) {
     UpdatePlayerError(kSbPlayerErrorDecode, "Failed seek.");
@@ -247,10 +248,9 @@
   }
 }
 
-void PlayerWorker::DoWriteSample(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void PlayerWorker::DoWriteSamples(InputBuffers input_buffers) {
   SB_DCHECK(job_queue_->BelongsToCurrentThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(!input_buffers.empty());
 
   if (player_state_ == kSbPlayerStateInitialized ||
       player_state_ == kSbPlayerStateEndOfStream ||
@@ -264,27 +264,34 @@
     return;
   }
 
-  if (input_buffer->sample_type() == kSbMediaTypeAudio) {
+  SbMediaType media_type = input_buffers.front()->sample_type();
+  if (media_type == kSbMediaTypeAudio) {
     SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
-    SB_DCHECK(!pending_audio_buffer_);
+    SB_DCHECK(pending_audio_buffers_.empty());
   } else {
     SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
-    SB_DCHECK(!pending_video_buffer_);
+    SB_DCHECK(pending_video_buffers_.empty());
   }
-  bool written;
-  bool result = handler_->WriteSample(input_buffer, &written);
+  int samples_written;
+  bool result = handler_->WriteSamples(input_buffers, &samples_written);
   if (!result) {
     UpdatePlayerError(kSbPlayerErrorDecode, "Failed to write sample.");
     return;
   }
-  if (written) {
-    UpdateDecoderState(input_buffer->sample_type(),
-                       kSbPlayerDecoderStateNeedsData);
+  if (samples_written == input_buffers.size()) {
+    UpdateDecoderState(media_type, kSbPlayerDecoderStateNeedsData);
   } else {
-    if (input_buffer->sample_type() == kSbMediaTypeAudio) {
-      pending_audio_buffer_ = input_buffer;
+    SB_DCHECK(samples_written >= 0 && samples_written <= input_buffers.size());
+
+    size_t num_of_pending_buffers = input_buffers.size() - samples_written;
+    input_buffers.erase(input_buffers.begin(),
+                        input_buffers.begin() + samples_written);
+    if (media_type == kSbMediaTypeAudio) {
+      pending_audio_buffers_ = std::move(input_buffers);
+      SB_DCHECK(pending_audio_buffers_.size() == num_of_pending_buffers);
     } else {
-      pending_video_buffer_ = input_buffer;
+      pending_video_buffers_ = std::move(input_buffers);
+      SB_DCHECK(pending_video_buffers_.size() == num_of_pending_buffers);
     }
     if (!write_pending_sample_job_token_.is_valid()) {
       write_pending_sample_job_token_ = job_queue_->Schedule(
@@ -299,13 +306,14 @@
   SB_DCHECK(write_pending_sample_job_token_.is_valid());
   write_pending_sample_job_token_.ResetToInvalid();
 
-  if (pending_audio_buffer_) {
+  if (!pending_audio_buffers_.empty()) {
     SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
-    DoWriteSample(common::ResetAndReturn(&pending_audio_buffer_));
+    DoWriteSamples(std::move(pending_audio_buffers_));
   }
-  if (pending_video_buffer_) {
+  if (!pending_video_buffers_.empty()) {
     SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
-    DoWriteSample(common::ResetAndReturn(&pending_video_buffer_));
+    InputBuffers input_buffers = std::move(pending_video_buffers_);
+    DoWriteSamples(input_buffers);
   }
 }
 
@@ -327,10 +335,10 @@
 
   if (sample_type == kSbMediaTypeAudio) {
     SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
-    SB_DCHECK(!pending_audio_buffer_);
+    SB_DCHECK(pending_audio_buffers_.empty());
   } else {
     SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
-    SB_DCHECK(!pending_video_buffer_);
+    SB_DCHECK(pending_video_buffers_.empty());
   }
 
   if (!handler_->WriteEndOfStream(sample_type)) {
diff --git a/starboard/shared/starboard/player/player_worker.h b/starboard/shared/starboard/player/player_worker.h
index dac9f8f..cf27fb3 100644
--- a/starboard/shared/starboard/player/player_worker.h
+++ b/starboard/shared/starboard/player/player_worker.h
@@ -17,7 +17,9 @@
 
 #include <atomic>
 #include <functional>
+#include <memory>
 #include <string>
+#include <utility>
 
 #include "starboard/common/log.h"
 #include "starboard/common/ref_counted.h"
@@ -63,6 +65,7 @@
    public:
     typedef PlayerWorker::Bounds Bounds;
     typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+    typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
 
     typedef std::function<
         void(SbTime media_time, int dropped_video_frames, bool is_progressing)>
@@ -84,8 +87,8 @@
                       UpdatePlayerStateCB update_player_state_cb,
                       UpdatePlayerErrorCB update_player_error_cb) = 0;
     virtual bool Seek(SbTime seek_to_time, int ticket) = 0;
-    virtual bool WriteSample(const scoped_refptr<InputBuffer>& input_buffer,
-                             bool* written) = 0;
+    virtual bool WriteSamples(const InputBuffers& input_buffers,
+                              int* samples_written) = 0;
     virtual bool WriteEndOfStream(SbMediaType sample_type) = 0;
     virtual bool SetPause(bool pause) = 0;
     virtual bool SetPlaybackRate(double playback_rate) = 0;
@@ -123,9 +126,9 @@
         std::bind(&PlayerWorker::DoSeek, this, seek_to_time, ticket));
   }
 
-  void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) {
-    job_queue_->Schedule(
-        std::bind(&PlayerWorker::DoWriteSample, this, input_buffer));
+  void WriteSamples(InputBuffers input_buffers) {
+    job_queue_->Schedule(std::bind(&PlayerWorker::DoWriteSamples, this,
+                                   std::move(input_buffers)));
   }
 
   void WriteEndOfStream(SbMediaType sample_type) {
@@ -190,7 +193,7 @@
   void RunLoop();
   void DoInit();
   void DoSeek(SbTime seek_to_time, int ticket);
-  void DoWriteSample(const scoped_refptr<InputBuffer>& input_buffer);
+  void DoWriteSamples(InputBuffers input_buffers);
   void DoWritePendingSamples();
   void DoWriteEndOfStream(SbMediaType sample_type);
   void DoSetBounds(Bounds bounds);
@@ -218,8 +221,8 @@
   int ticket_;
 
   SbPlayerState player_state_;
-  scoped_refptr<InputBuffer> pending_audio_buffer_;
-  scoped_refptr<InputBuffer> pending_video_buffer_;
+  InputBuffers pending_audio_buffers_;
+  InputBuffers pending_video_buffers_;
   JobQueue::JobToken write_pending_sample_job_token_;
 };
 
diff --git a/starboard/shared/starboard/player/player_write_sample2.cc b/starboard/shared/starboard/player/player_write_sample2.cc
index 7a3a018..d839aff 100644
--- a/starboard/shared/starboard/player/player_write_sample2.cc
+++ b/starboard/shared/starboard/player/player_write_sample2.cc
@@ -21,12 +21,26 @@
                           SbMediaType sample_type,
                           const SbPlayerSampleInfo* sample_infos,
                           int number_of_sample_infos) {
-  SB_DCHECK(number_of_sample_infos == 1);
-
   if (!SbPlayerIsValid(player)) {
-    SB_DLOG(WARNING) << "player is invalid.";
+    SB_LOG(WARNING) << "player is invalid.";
     return;
   }
+  if (!sample_infos) {
+    SB_LOG(WARNING) << "sample_infos is null.";
+    return;
+  }
+  if (number_of_sample_infos < 1) {
+    SB_LOG(WARNING) << "number_of_sample_infos is " << number_of_sample_infos
+                    << ", which should be greater than or equal to 1";
+    return;
+  }
+  auto max_samples_per_write =
+      SbPlayerGetMaximumNumberOfSamplesPerWrite(player, sample_type);
+  if (number_of_sample_infos > max_samples_per_write) {
+    SB_LOG(WARNING) << "number_of_sample_infos is " << number_of_sample_infos
+                    << ", which should be less than or equal to "
+                    << max_samples_per_write;
+  }
 
-  player->WriteSample(*sample_infos);
+  player->WriteSamples(sample_infos, number_of_sample_infos);
 }
diff --git a/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1 b/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
index 58c5cd1..8f85266 100644
--- a/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
+++ b/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
@@ -1 +1 @@
-f3c82f30f61d744f35a3d37e9de060a13cd1bd90
\ No newline at end of file
+cf63bb5cf3d72b8a524b505fb8ea761a9fdcc362
diff --git a/starboard/shared/starboard/player/testdata/sha_files.gni b/starboard/shared/starboard/player/testdata/sha1_files.gni
similarity index 100%
rename from starboard/shared/starboard/player/testdata/sha_files.gni
rename to starboard/shared/starboard/player/testdata/sha1_files.gni
diff --git a/starboard/shared/widevine/drm_system_widevine.cc b/starboard/shared/widevine/drm_system_widevine.cc
index 59292ef..cd32817 100644
--- a/starboard/shared/widevine/drm_system_widevine.cc
+++ b/starboard/shared/widevine/drm_system_widevine.cc
@@ -15,6 +15,7 @@
 #include "starboard/shared/widevine/drm_system_widevine.h"
 
 #include <algorithm>
+#include <utility>
 #include <vector>
 
 #include "starboard/character.h"
@@ -440,6 +441,7 @@
   input.pattern.clear_blocks = drm_info->encryption_pattern.skip_byte_block;
 
   std::vector<uint8_t> output_data(buffer->size());
+
   wv3cdm::OutputBuffer output;
   output.data = output_data.data();
   output.data_length = output_data.size();
@@ -540,7 +542,7 @@
     }
   }
 
-  buffer->SetDecryptedContent(output_data.data(), output_data.size());
+  buffer->SetDecryptedContent(std::move(output_data));
   return kSuccess;
 }
 
diff --git a/starboard/shared/win32/audio_decoder.cc b/starboard/shared/win32/audio_decoder.cc
index 0b54739..fbcfe44 100644
--- a/starboard/shared/win32/audio_decoder.cc
+++ b/starboard/shared/win32/audio_decoder.cc
@@ -98,14 +98,15 @@
   callback_scheduler_.reset(new CallbackScheduler());
 }
 
-void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void AudioDecoder::Decode(const InputBuffers& input_buffers,
                           const ConsumedCB& consumed_cb) {
   SB_DCHECK(thread_checker_.CalledOnValidThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
 
   callback_scheduler_->SetCallbackOnce(consumed_cb);
   callback_scheduler_->OnCallbackSignaled();
-  const bool can_take_more_data = decoder_thread_->QueueInput(input_buffer);
+  const bool can_take_more_data = decoder_thread_->QueueInput(input_buffers[0]);
   if (can_take_more_data) {
     callback_scheduler_->ScheduleCallbackIfNecessary();
   }
diff --git a/starboard/shared/win32/audio_decoder.h b/starboard/shared/win32/audio_decoder.h
index 18631f0..0467b47 100644
--- a/starboard/shared/win32/audio_decoder.h
+++ b/starboard/shared/win32/audio_decoder.h
@@ -45,7 +45,7 @@
   ~AudioDecoder() override;
 
   void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
-  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+  void Decode(const InputBuffers& input_buffers,
               const ConsumedCB& consumed_cb) override;
   void WriteEndOfStream() override;
   scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/shared/win32/configuration.cc b/starboard/shared/win32/configuration.cc
index 23cd365..835c20d 100644
--- a/starboard/shared/win32/configuration.cc
+++ b/starboard/shared/win32/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/shared/win32/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 namespace starboard {
 namespace shared {
diff --git a/starboard/shared/win32/graphics.cc b/starboard/shared/win32/graphics.cc
index 0259a60..d620908 100644
--- a/starboard/shared/win32/graphics.cc
+++ b/starboard/shared/win32/graphics.cc
@@ -14,7 +14,7 @@
 
 #include "starboard/shared/win32/graphics.h"
 
-#include "cobalt/extension/graphics.h"
+#include "starboard/extension/graphics.h"
 
 namespace starboard {
 namespace shared {
@@ -36,10 +36,10 @@
 }
 
 const CobaltExtensionGraphicsApi kGraphicsApi = {
-  kCobaltExtensionGraphicsName,
-  2,
-  &GetMaximumFrameIntervalInMilliseconds,
-  &GetMinimumFrameIntervalInMilliseconds,
+    kCobaltExtensionGraphicsName,
+    2,
+    &GetMaximumFrameIntervalInMilliseconds,
+    &GetMinimumFrameIntervalInMilliseconds,
 };
 
 }  // namespace
diff --git a/starboard/shared/win32/media_common.cc b/starboard/shared/win32/media_common.cc
index a09843c..1c4a100 100644
--- a/starboard/shared/win32/media_common.cc
+++ b/starboard/shared/win32/media_common.cc
@@ -98,12 +98,59 @@
   return output;
 }
 
+HRESULT CreateAV1Decoder(const IID& iid, void** object) {
+  MFT_REGISTER_TYPE_INFO type_info = {MFMediaType_Video, MFVideoFormat_AV1};
+  MFT_REGISTER_TYPE_INFO output_info = {MFMediaType_Video, MFVideoFormat_NV12};
+
+  IMFActivate** acts;
+  UINT32 acts_num = 0;
+  HRESULT hr = ::MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER,
+                           MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT |
+                               MFT_ENUM_FLAG_UNTRUSTED_STOREMFT,
+                           &type_info, &output_info, &acts, &acts_num);
+  if (FAILED(hr))
+    return hr;
+
+  if (acts_num < 1)
+    return E_FAIL;
+
+  hr = acts[0]->ActivateObject(iid, object);
+  for (UINT32 i = 0; i < acts_num; ++i)
+    acts[i]->Release();
+  CoTaskMemFree(acts);
+  return hr;
+}
+
 HRESULT CreateDecoderTransform(const GUID& decoder_guid,
                                ComPtr<IMFTransform>* transform) {
+  if (decoder_guid == MFVideoFormat_AV1) {
+    return CreateAV1Decoder(IID_PPV_ARGS(transform->GetAddressOf()));
+  }
   return CoCreateInstance(decoder_guid, NULL, CLSCTX_INPROC_SERVER,
                           IID_PPV_ARGS(transform->GetAddressOf()));
 }
 
+bool IsHardwareAv1DecoderSupported() {
+  static bool av1_decoder_supported = false;
+  static bool av1_support_checked = false;
+  if (!av1_support_checked) {
+    MFT_REGISTER_TYPE_INFO type_info = {MFMediaType_Video, MFVideoFormat_AV1};
+    MFT_REGISTER_TYPE_INFO output_info = {MFMediaType_Video,
+                                          MFVideoFormat_NV12};
+
+    IMFActivate** acts;
+    UINT32 acts_num = 0;
+    HRESULT hr = ::MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER,
+                             MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT |
+                                 MFT_ENUM_FLAG_UNTRUSTED_STOREMFT,
+                             &type_info, &output_info, &acts, &acts_num);
+    for (UINT32 i = 0; i < acts_num; ++i)
+      acts[i]->Release();
+    av1_decoder_supported = SUCCEEDED(hr) && acts_num >= 1;
+    av1_support_checked = true;
+  }
+  return av1_decoder_supported;
+}
 }  // namespace win32
 }  // namespace shared
 }  // namespace starboard
diff --git a/starboard/shared/win32/media_common.h b/starboard/shared/win32/media_common.h
index eacedfb..21d1323 100644
--- a/starboard/shared/win32/media_common.h
+++ b/starboard/shared/win32/media_common.h
@@ -69,6 +69,8 @@
 HRESULT CreateDecoderTransform(const GUID& decoder_guid,
                                ComPtr<IMFTransform>* transform);
 
+bool IsHardwareAv1DecoderSupported();
+
 }  // namespace win32
 }  // namespace shared
 }  // namespace starboard
diff --git a/starboard/shared/win32/socket_set_tcp_keep_alive.cc b/starboard/shared/win32/socket_set_tcp_keep_alive.cc
index 5d6988d..4c2b928 100644
--- a/starboard/shared/win32/socket_set_tcp_keep_alive.cc
+++ b/starboard/shared/win32/socket_set_tcp_keep_alive.cc
@@ -22,7 +22,6 @@
 namespace sbwin32 = starboard::shared::win32;
 
 bool SbSocketSetTcpKeepAlive(SbSocket socket, bool value, SbTime period) {
-  const DWORD should_set_keepalive = value;
   bool result = sbwin32::SetBooleanSocketOption(
       socket, SOL_SOCKET, SO_KEEPALIVE, "SO_KEEPALIVE", value);
   return result;
diff --git a/starboard/shared/win32/system_get_extensions.cc b/starboard/shared/win32/system_get_extensions.cc
index c428752..09a5726 100644
--- a/starboard/shared/win32/system_get_extensions.cc
+++ b/starboard/shared/win32/system_get_extensions.cc
@@ -14,9 +14,9 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/graphics.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/graphics.h"
 #include "starboard/shared/win32/configuration.h"
 #include "starboard/shared/win32/graphics.h"
 
diff --git a/starboard/shared/win32/video_decoder.cc b/starboard/shared/win32/video_decoder.cc
index 013dffb..5b506bd 100644
--- a/starboard/shared/win32/video_decoder.cc
+++ b/starboard/shared/win32/video_decoder.cc
@@ -150,7 +150,7 @@
   CheckResult(input_type->SetGUID(MF_MT_SUBTYPE, input_guid));
   CheckResult(input_type->SetUINT32(MF_MT_INTERLACE_MODE,
                                     MFVideoInterlace_Progressive));
-  if (input_guid == MFVideoFormat_VP90) {
+  if (input_guid == MFVideoFormat_VP90 || input_guid == MFVideoFormat_AV1) {
     // Set the expected video resolution. Setting the proper resolution can
     // mitigate a format change, but the decoder will adjust to the real
     // resolution regardless.
@@ -278,13 +278,14 @@
   }
 }
 
-void VideoDecoder::WriteInputBuffer(
-    const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
   SB_DCHECK(thread_checker_.CalledOnValidThread());
-  SB_DCHECK(input_buffer);
+  SB_DCHECK(input_buffers.size() == 1);
+  SB_DCHECK(input_buffers[0]);
   SB_DCHECK(decoder_status_cb_);
   EnsureDecoderThreadRunning();
 
+  const auto& input_buffer = input_buffers[0];
   if (TryUpdateOutputForHdrVideo(input_buffer->video_sample_info())) {
     ScopedLock lock(thread_lock_);
     thread_events_.emplace_back(
@@ -440,7 +441,20 @@
       }
       break;
     }
-    default: { SB_NOTREACHED(); }
+    case kSbMediaVideoCodecAv1: {
+      media_transform =
+          CreateVideoTransform(MFVideoFormat_AV1, MFVideoFormat_AV1,
+                               MFVideoFormat_NV12, device_manager_.Get());
+      priming_output_count_ = 0;
+      if (!media_transform) {
+        SB_LOG(WARNING) << "AV1 hardware decoder creation failed.";
+        return;
+      }
+      break;
+    }
+    default: {
+      SB_NOTREACHED();
+    }
   }
 
   decoder_.reset(
@@ -535,7 +549,7 @@
 
   // NOTE: The video decoder thread will exit after processing the
   // kWriteEndOfStream event. In this case, Reset must be called (which will
-  // then StopDecoderThread) before WriteInputBuffer or WriteEndOfStream again.
+  // then StopDecoderThread) before WriteInputBuffers or WriteEndOfStream again.
   SB_DCHECK(!decoder_thread_stopped_);
 
   if (!SbThreadIsValid(decoder_thread_)) {
diff --git a/starboard/shared/win32/video_decoder.h b/starboard/shared/win32/video_decoder.h
index 2186963..c8a38e0 100644
--- a/starboard/shared/win32/video_decoder.h
+++ b/starboard/shared/win32/video_decoder.h
@@ -59,8 +59,7 @@
   SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
   size_t GetMaxNumberOfCachedFrames() const override;
 
-  void WriteInputBuffer(
-      const scoped_refptr<InputBuffer>& input_buffer) override;
+  void WriteInputBuffers(const InputBuffers& input_buffers) override;
   void WriteEndOfStream() override;
   void Reset() override;
   SbDecodeTarget GetCurrentDecodeTarget() override;
diff --git a/starboard/spin_lock.h b/starboard/spin_lock.h
deleted file mode 100644
index 8d56d9f..0000000
--- a/starboard/spin_lock.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2016 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef STARBOARD_SPIN_LOCK_H_
-#define STARBOARD_SPIN_LOCK_H_
-
-#error "File moved to //starboard/common/spin_lock.h."
-
-#endif  // STARBOARD_SPIN_LOCK_H_
diff --git a/starboard/stub/configuration.cc b/starboard/stub/configuration.cc
index 334d6df..bf6861a 100644
--- a/starboard/stub/configuration.cc
+++ b/starboard/stub/configuration.cc
@@ -14,8 +14,8 @@
 
 #include "starboard/stub/configuration.h"
 
-#include "cobalt/extension/configuration.h"
 #include "starboard/common/configuration_defaults.h"
+#include "starboard/extension/configuration.h"
 
 namespace starboard {
 namespace stub {
diff --git a/starboard/stub/font.cc b/starboard/stub/font.cc
index a93fed7..a52a582 100644
--- a/starboard/stub/font.cc
+++ b/starboard/stub/font.cc
@@ -14,7 +14,7 @@
 
 #include "starboard/stub/font.h"
 
-#include "cobalt/extension/font.h"
+#include "starboard/extension/font.h"
 
 namespace starboard {
 namespace stub {
diff --git a/starboard/stub/javascript_cache.cc b/starboard/stub/javascript_cache.cc
index 82e8bdd..c458fac 100644
--- a/starboard/stub/javascript_cache.cc
+++ b/starboard/stub/javascript_cache.cc
@@ -17,8 +17,8 @@
 #include <functional>
 #include <string>
 
-#include "cobalt/extension/javascript_cache.h"
 #include "starboard/common/log.h"
+#include "starboard/extension/javascript_cache.h"
 #include "starboard/file.h"
 
 namespace starboard {
diff --git a/starboard/stub/system_get_extensions.cc b/starboard/stub/system_get_extensions.cc
index ca1fe9e..c2bc94e 100644
--- a/starboard/stub/system_get_extensions.cc
+++ b/starboard/stub/system_get_extensions.cc
@@ -14,10 +14,10 @@
 
 #include "starboard/system.h"
 
-#include "cobalt/extension/configuration.h"
-#include "cobalt/extension/font.h"
-#include "cobalt/extension/javascript_cache.h"
 #include "starboard/common/string.h"
+#include "starboard/extension/configuration.h"
+#include "starboard/extension/font.h"
+#include "starboard/extension/javascript_cache.h"
 #include "starboard/stub/configuration.h"
 #include "starboard/stub/font.h"
 #include "starboard/stub/javascript_cache.h"
diff --git a/starboard/tools/build.py b/starboard/tools/build.py
index 6566e55..04ffb42 100644
--- a/starboard/tools/build.py
+++ b/starboard/tools/build.py
@@ -149,7 +149,7 @@
                       'x86_64-linux-gnu-clang-chromium-' + clang_spec.revision)
 
 
-def _GetClangBinPath(clang_spec):
+def GetClangBinPath(clang_spec):
   return os.path.join(_GetClangBasePath(clang_spec), 'bin')
 
 
@@ -162,7 +162,7 @@
   download_clang.UpdateClang(target_dir=base_dir, revision=clang_spec.revision)
 
   # update.sh downloads Clang to this path.
-  clang_bin = os.path.join(_GetClangBinPath(clang_spec), 'clang')
+  clang_bin = os.path.join(GetClangBinPath(clang_spec), 'clang')
 
   if not os.path.exists(clang_bin):
     raise RuntimeError('Clang not found.')
diff --git a/starboard/tools/command_line_test.py b/starboard/tools/command_line_test.py
index 8393163..06baa38 100755
--- a/starboard/tools/command_line_test.py
+++ b/starboard/tools/command_line_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Copyright 2017 The Cobalt Authors. All Rights Reserved.
 #
@@ -58,7 +58,7 @@
 
 
 def _SetEnvironBuildConfiguration(config, platform):
-  os.environ['BUILD_CONFIGURATION'] = '%s_%s' % (platform, config)
+  os.environ['BUILD_CONFIGURATION'] = f'{platform}_{config}'
 
 
 def _SetEnviron(config, platform):
@@ -70,13 +70,13 @@
 class CommandLineTest(unittest.TestCase):
 
   def setUp(self):
-    super(CommandLineTest, self).setUp()
+    super().setUp()
     self.environ = os.environ.copy()
     _ClearEnviron()
 
   def tearDown(self):
     _RestoreMapping(os.environ, self.environ)
-    super(CommandLineTest, self).tearDown()
+    super().tearDown()
 
   def testNoEnvironmentRainyDayNoArgs(self):
     arg_parser = _CreateParser()
diff --git a/starboard/tools/download_clang.sh b/starboard/tools/download_clang.sh
index a32d327..662d5d5 100755
--- a/starboard/tools/download_clang.sh
+++ b/starboard/tools/download_clang.sh
@@ -29,9 +29,17 @@
 
 cd /tmp
 mkdir -p ${TOOLCHAIN_HOME}
+
+# Download and extract clang.
 curl --silent -O -J ${BASE_URL}/Linux_x64/clang-${CLANG_VERSION}.tgz
 tar xf clang-${CLANG_VERSION}.tgz -C ${TOOLCHAIN_HOME}
-echo ${CLANG_VERSION} >> ${TOOLCHAIN_HOME}/cr_build_revision
 rm clang-${CLANG_VERSION}.tgz
 
+# Download and extract llvm coverage tools.
+curl --silent -O -J ${BASE_URL}/Linux_x64/llvm-code-coverage-${CLANG_VERSION}.tgz
+tar xf llvm-code-coverage-${CLANG_VERSION}.tgz -C ${TOOLCHAIN_HOME}
+rm llvm-code-coverage-${CLANG_VERSION}.tgz
+
+echo ${CLANG_VERSION} >> ${TOOLCHAIN_HOME}/cr_build_revision
+
 echo "Downloaded clang."
diff --git a/starboard/tools/log_level_test.py b/starboard/tools/log_level_test.py
index 273d775..ec9034e 100755
--- a/starboard/tools/log_level_test.py
+++ b/starboard/tools/log_level_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Copyright 2020 The Cobalt Authors. All Rights Reserved.
 #
diff --git a/starboard/tools/port_symlink.py b/starboard/tools/port_symlink.py
index b77a817..87891e3 100644
--- a/starboard/tools/port_symlink.py
+++ b/starboard/tools/port_symlink.py
@@ -23,6 +23,9 @@
 from starboard.tools import util
 from starboard.tools import win_symlink
 
+# This file is still executed with Python2 in CI.
+# pylint:disable=consider-using-f-string
+
 
 def IsWindows():
   return sys.platform in ['win32', 'cygwin']
@@ -89,7 +92,7 @@
   else:
     os.unlink(path)
     return
-  if os.path.exists(path):
+  if os.path.exists(path) or os.path.islink(path):
     func(path)
 
 
@@ -106,7 +109,7 @@
   class MyParser(argparse.ArgumentParser):
 
     def error(self, message):
-      sys.stderr.write('error: %s\n' % message)
+      sys.stderr.write('error: {}\n'.format(message))
       self.print_help()
       sys.exit(2)
 
diff --git a/starboard/tools/port_symlink_test.py b/starboard/tools/port_symlink_test.py
index 5eb4326..304c6e9 100644
--- a/starboard/tools/port_symlink_test.py
+++ b/starboard/tools/port_symlink_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Copyright 2019 The Cobalt Authors. All Rights Reserved.
 #
@@ -51,7 +51,7 @@
 class PortSymlinkTest(unittest.TestCase):
 
   def setUp(self):
-    super(PortSymlinkTest, self).setUp()
+    super().setUp()
     self.tmp_dir = tempfile.mkdtemp(prefix='port_symlink_')
     self.target_dir = os.path.join(self.tmp_dir, 'target')
     self.inner_dir = os.path.join(self.target_dir, 'inner')
@@ -59,13 +59,13 @@
     self.target_file = os.path.join(self.target_dir, _TARGET_FILENAME)
     _MakeDirs(self.target_dir)
     _MakeDirs(self.inner_dir)
-    with open(self.target_file, 'w') as fd:
+    with open(self.target_file, 'w', encoding='utf-8') as fd:
       fd.write('hallo welt!')
     MakeSymLink(self.target_dir, self.link_dir)
 
   def tearDown(self):
     Rmtree(self.tmp_dir)
-    super(PortSymlinkTest, self).tearDown()
+    super().tearDown()
 
   def testSanity(self):
     self.assertTrue(os.path.isdir(self.tmp_dir))
@@ -88,7 +88,7 @@
     self.assertTrue(IsSymLink(rel_link_dir))
     link_value = ReadSymLink(rel_link_dir)
     self.assertIn(
-        '..', link_value, msg='Expected ".." in relative path %s' % link_value)
+        '..', link_value, msg=f'Expected ".." in relative path {link_value}')
 
   def testDelSymlink(self):
     link_dir2 = os.path.join(self.tmp_dir, 'link2')
@@ -118,7 +118,7 @@
     external_temp_dir = tempfile.mkdtemp()
     try:
       external_temp_file = os.path.join(external_temp_dir, _TARGET_FILENAME)
-      with open(external_temp_file, 'w') as fd:
+      with open(external_temp_file, 'w', encoding='utf-8') as fd:
         fd.write('hallo!')
       link_dir = os.path.join(self.tmp_dir, 'foo', 'link_dir')
       MakeSymLink(external_temp_file, link_dir)
diff --git a/starboard/tools/testing/sharding_configuration.json b/starboard/tools/testing/sharding_configuration.json
index 896fcd9..5497926 100644
--- a/starboard/tools/testing/sharding_configuration.json
+++ b/starboard/tools/testing/sharding_configuration.json
@@ -1,49 +1,6 @@
 {
   "default": [
     {
-      "renderer_test":"*",
-      "bindings_test":"*",
-      "graphics_system_test":"*",
-      "installation_manager_test":"*",
-      "dom_test":"*",
-      "webdriver_test":"*",
-      "crypto_unittests":"*",
-      "media_capture_test":"*",
-      "websocket_test":"*",
-      "text_encoding_test":"*",
-      "zip_unittests":"*",
-      "audio_test":"*",
-      "slot_management_test":"*",
-      "media_session_test":"*",
-      "base_test":"*",
-      "nb_test":"*",
-      "loader_test":"*",
-      "dom_parser_test":"*",
-      "storage_test":"*",
-      "css_parser_test":"*",
-      "cssom_test":"*",
-      "starboard_platform_tests":"*",
-      "browser_test":"*",
-      "elf_loader_test":"*",
-      "drain_file_test":"*",
-      "xhr_test":"*",
-      "csp_test":"*",
-      "layout_test":"*",
-      "math_test":"*",
-      "media_stream_test":"*",
-      "network_test":"*",
-      "render_tree_test":"*",
-      "memory_store_test":"*",
-      "web_animations_test":"*",
-      "app_key_files_test":"*",
-      "eztime_test":"*",
-      "app_key_test":"*",
-      "cwrappers_test":"*",
-      "extension_test":"*",
-      "poem_unittests":"*",
-      "net_unittests": [1, 2]
-    },
-    {
       "base_unittests": [1, 2],
       "layout_tests": "*"
     },
@@ -59,31 +16,6 @@
   ],
   "xb1|ps4|ps5|nxswitch":[
     {
-      "app_key_files_test":"*",
-      "app_key_test":"*",
-      "audio_test":"*",
-      "base_test":"*",
-      "bindings_test":"*",
-      "browser_test":"*",
-      "crypto_unittests":"*",
-      "csp_test":"*",
-      "css_parser_test":"*",
-      "cssom_test":"*",
-      "cwrappers_test":"*",
-      "dom_parser_test":"*",
-      "dom_test":"*",
-      "drain_file_test":"*",
-      "elf_loader_test":"*",
-      "extension_test":"*",
-      "eztime_test":"*",
-      "graphics_system_test":"*",
-      "installation_manager_test":"*",
-      "layout_test":"*",
-      "net_unittests":"*",
-      "nplb":"*",
-      "player_filter_tests":"*"
-    },
-    {
       "base_unittests":"*",
       "layout_tests":"*",
       "loader_test":"*",
@@ -147,54 +79,6 @@
       "nplb": [3, 5],
       "player_filter_tests": [4, 6],
       "renderer_test": [4, 6]
-    },
-    {
-      "app_key_files_test": "*",
-      "app_key_test": "*",
-      "audio_test": "*",
-      "base_test": "*",
-      "base_unittests": [4, 6],
-      "bindings_test": "*",
-      "browser_test": "*",
-      "crypto_unittests": "*",
-      "csp_test": "*",
-      "css_parser_test": "*",
-      "cssom_test": "*",
-      "cwrappers_test": "*",
-      "dom_parser_test": "*",
-      "dom_test": "*",
-      "drain_file_test": "*",
-      "elf_loader_test": "*",
-      "extension_test": "*",
-      "graphics_system_test": "*",
-      "installation_manager_test": "*",
-      "layout_test": "*",
-      "layout_tests": [2, 6],
-      "loader_test": "*",
-      "math_test": "*",
-      "media_capture_test": "*",
-      "media_session_test": "*",
-      "media_stream_test": "*",
-      "memory_store_test": "*",
-      "nb_test": "*",
-      "net_unittests": [6, 6],
-      "network_test": "*",
-      "nplb": [5, 5],
-      "player_filter_tests": [6, 6],
-      "poem_unittests": "*",
-      "render_tree_test": "*",
-      "renderer_test": [3, 6],
-      "slot_management_test": "*",
-      "starboard_platform_tests": "*",
-      "storage_test": "*",
-      "text_encoding_test": "*",
-      "web_animations_test": "*",
-      "web_test": "*",
-      "webdriver_test": "*",
-      "websocket_test": "*",
-      "worker_test": "*",
-      "xhr_test": "*",
-      "zip_unittests": "*"
     }
   ]
 }
diff --git a/starboard/tools/testing/test_runner.py b/starboard/tools/testing/test_runner.py
index 406ce24..78eeed9 100755
--- a/starboard/tools/testing/test_runner.py
+++ b/starboard/tools/testing/test_runner.py
@@ -27,6 +27,7 @@
 import traceback
 
 from six.moves import cStringIO as StringIO
+from starboard.build import clang
 from starboard.tools import abstract_launcher
 from starboard.tools import build
 from starboard.tools import command_line
@@ -36,6 +37,8 @@
 from starboard.tools.testing.test_sharding import ShardingTestConfig
 from starboard.tools.util import SetupDefaultLoggingConfig
 
+# pylint: disable=consider-using-f-string
+
 _FLAKY_RETRY_LIMIT = 4
 _TOTAL_TESTS_REGEX = re.compile(r"^\[==========\] (.*) tests? from .*"
                                 r"test cases? ran. \(.* ms total\)")
@@ -282,7 +285,8 @@
     # Read the sharding configuration from deployed sharding configuration json.
     # Create subset of test targets, and launch params per target.
     try:
-      self.sharding_test_config = ShardingTestConfig(self.platform)
+      self.sharding_test_config = ShardingTestConfig(self.platform,
+                                                     self.test_targets)
     except RuntimeError:
       self.sharding_test_config = None
 
@@ -639,6 +643,7 @@
 
       actual_failed_count = len(actual_failed_tests)
       flaky_failed_count = len(flaky_failed_tests)
+      initial_flaky_failed_count = flaky_failed_count
       filtered_count = len(filtered_tests)
 
       # If our math does not agree with gtest...
@@ -648,9 +653,9 @@
 
       # Retry the flaky test cases that failed, and mark them as passed if they
       # succeed within the retry limit.
+      flaky_passed_tests = []
       if flaky_failed_count > 0:
         logging.info("RE-RUNNING FLAKY TESTS.\n")
-        flaky_passed_tests = []
         for test_case in flaky_failed_tests:
           for retry in range(_FLAKY_RETRY_LIMIT):
             # Sometimes the returned test "name" includes information about the
@@ -673,9 +678,13 @@
 
       test_status = "SUCCEEDED"
 
+      all_flaky_tests_succeeded = initial_flaky_failed_count == len(
+          flaky_passed_tests)
+
       # Always mark as FAILED if we have a non-zero return code, or failing
       # test.
-      if return_code != 0 or actual_failed_count > 0 or flaky_failed_count > 0:
+      if ((return_code != 0 and not all_flaky_tests_succeeded) or
+          actual_failed_count > 0 or flaky_failed_count > 0):
         error = True
         test_status = "FAILED"
         failed_test_groups.append(target_name)
@@ -794,7 +803,7 @@
     for test_target in sorted(self.test_targets.keys()):
       if (self.shard_index is not None) and self.sharding_test_config:
         (run_action, sub_shard_index,
-         sub_shard_count) = self.sharding_test_config.GetTestRunConfig(
+         sub_shard_count) = self.sharding_test_config.get_test_run_config(
              test_target, self.shard_index)
         if run_action == ShardingTestConfig.RUN_FULL_TEST:
           logging.info("SHARD %d RUNS TEST %s (full)", self.shard_index,
@@ -830,17 +839,21 @@
     if not available_profraw_files:
       return
 
+    toolchain_dir = build.GetClangBinPath(clang.GetClangSpecification())
+
     report_name = "report"
     profdata_name = os.path.join(self.coverage_directory,
                                  report_name + ".profdata")
     merge_cmd_list = [
-        "llvm-profdata", "merge", "-sparse=true", "-o", profdata_name
+        os.path.join(toolchain_dir, "llvm-profdata"), "merge", "-sparse=true",
+        "-o", profdata_name
     ]
     merge_cmd_list += available_profraw_files
 
     self._Exec(merge_cmd_list)
     show_cmd_list = [
-        "llvm-cov", "show", "-instr-profile=" + profdata_name, "-format=html",
+        os.path.join(toolchain_dir, "llvm-cov"), "show",
+        "-instr-profile=" + profdata_name, "-format=html",
         "-output-dir=" + os.path.join(self.coverage_directory, "html"),
         available_targets[0]
     ]
@@ -848,8 +861,8 @@
     self._Exec(show_cmd_list)
 
     report_cmd_list = [
-        "llvm-cov", "report", "-instr-profile=" + profdata_name,
-        available_targets[0]
+        os.path.join(toolchain_dir, "llvm-cov"), "report",
+        "-instr-profile=" + profdata_name, available_targets[0]
     ]
     report_cmd_list += ["-object=" + target for target in available_targets[1:]]
     self._Exec(
diff --git a/starboard/tools/testing/test_sharding.py b/starboard/tools/testing/test_sharding.py
index a8bc90e..4ef2dd5 100644
--- a/starboard/tools/testing/test_sharding.py
+++ b/starboard/tools/testing/test_sharding.py
@@ -20,8 +20,9 @@
 
 import os
 import json
+import logging
 
-SHARDING_CONFIG_FILE = os.path.join(
+_SHARDING_CONFIG_FILE = os.path.join(
     os.path.dirname(os.path.abspath(__file__)), 'sharding_configuration.json')
 
 
@@ -34,20 +35,119 @@
   RUN_PARTIAL_TEST = 'run_partial_test'
   SKIP_TEST = 'skip_test'
 
-  def __init__(self, platform):
+  def __init__(self, platform, test_targets, platform_sharding_config=None):
     try:
-      with open(SHARDING_CONFIG_FILE, 'r') as file:
-        sharding_json = json.load(file)
-      # Load this config by default.
-      self.platform_sharding_config = sharding_json['default']
-      # Check for specific platform if specified:
-      for platform_key in sharding_json:
-        if platform in platform_key:
-          self.platform_sharding_config = sharding_json[platform_key]
+      self.platform_sharding_config = platform_sharding_config
+      if not platform_sharding_config:
+        self.platform_sharding_config = self._read_platform_sharding_config(
+            platform)
+      # Create the default shard, and add it to the config.
+      default_shard = self._create_default_shard(self.platform_sharding_config,
+                                                 test_targets)
+      self.platform_sharding_config.insert(0, default_shard)
+      logging.debug(
+          '%s',
+          json.dumps(self.platform_sharding_config, sort_keys=True, indent=2))
     except FileNotFoundError:
       raise RuntimeError('No sharding configuration file found.')
 
-  def GetTestRunConfig(self, test_target, shard_index):
+  def _read_platform_sharding_config(self,
+                                     platform,
+                                     filename=_SHARDING_CONFIG_FILE):
+    with open(filename, 'r') as file:
+      sharding_json = json.load(file)
+      for platform_key in sharding_json:
+        if platform in platform_key:
+          return sharding_json[platform_key]
+      return sharding_json['default']
+
+  def _find_unmapped_test_chunks(self, platform_sharding_config):
+    """
+    This function determines which test chunks have not been assigned by
+    enumerating all possible test chunks, given the existing incomplete sharding
+    configuration.
+
+    E.g. if the sharding configuration consists of:
+    config  = [
+      {
+        "foo_test": [2, 3],
+      },
+      {
+        "foo_test": [1, 3],
+      }
+    ]
+
+    Then we can assume that the [3, 3] chunk is unmapped.
+    """
+    # Iterate over all chunks:
+    # - Create a mapping between test_name and chunk_total
+    # - Record the unmapped chunk_index values for each test_name
+    chunk_totals_by_test = {}
+    unmapped_chunk_index_by_test = {}
+    for shard_chunk in platform_sharding_config:
+      for test_name, chunk_info in shard_chunk.items():
+        # The entire test is represented by '*' in the |chunk_info| field.
+        if not isinstance(chunk_info, list):
+          unmapped_chunk_index_by_test[test_name] = set()
+          continue
+        # The test chunk is represented by [index, count], where:
+        # count >= index >= 1
+        chunk_index, chunk_total = chunk_info
+        if test_name not in chunk_totals_by_test:
+          chunk_totals_by_test[test_name] = chunk_total
+          unmapped_chunk_index_by_test[test_name] = set(
+              i for i in range(1, chunk_total + 1))
+        unmapped_chunk_index_by_test[test_name].remove(chunk_index)
+
+    logging.debug('Printing all tests with unmapped chunks:')
+    for k, v in unmapped_chunk_index_by_test.items():
+      logging.debug('%s: %s', k, v)
+    logging.debug('Printing all tests with corresponding chunk sizes:')
+    logging.debug(json.dumps(chunk_totals_by_test, sort_keys=True, indent=2))
+
+    return unmapped_chunk_index_by_test, chunk_totals_by_test
+
+  def _create_default_shard(self, platform_sharding_config, test_targets):
+    """
+    The default shard consists of all unmapped test chunks, and unmapped tests.
+
+    This function generates the default shard using the list of all test targets
+    and the currently configured set of test chunks.
+
+    A test chunk is defined as a subset of test cases from the test suite, which
+    corresponds to the tests run by GTEST when provided the following arguments:
+
+    $ ./foo_test --gtest_shard_index=INDEX --gtest_total_shards=COUNT
+
+    Where COUNT > INDEX > 1.
+    """
+    unmapped_chunk_index_by_test, chunk_totals_by_test = (
+        self._find_unmapped_test_chunks(platform_sharding_config))
+
+    # Add the entire test to the default shard, if the test is not found in the
+    # list of unmapped chunks.
+    unlisted_tests = set(test_targets) - set(
+        unmapped_chunk_index_by_test.keys())
+    default_shard = {name: '*' for name in unlisted_tests}
+    # Add the unmapped test chunks.
+    for test_name, unmapped_chunks in unmapped_chunk_index_by_test.items():
+      if len(unmapped_chunks) == 0:
+        # Test has no unmapped chunks.
+        continue
+      # Shard cannot contain multiple chunks from the same test.
+      if len(unmapped_chunks) > 1:
+        raise ValueError('Invalid Sharding Configuration: default shard must '
+                         'not contain multiple chunks from the same test ('
+                         'test:{} unmapped_chunks:{}).'.format(
+                             test_name, unmapped_chunks))
+      # Add the unmapped test chunk to the default shard.
+      chunk_index = list(unmapped_chunks)[0]
+      chunk_total = chunk_totals_by_test[test_name]
+      default_shard[test_name] = [chunk_index, chunk_total]
+
+    return default_shard
+
+  def get_test_run_config(self, test_target, shard_index):
     """
     Returns whether the input test is run in the input shard (specified by its
     index). If the test is run as part of the shard, then it also provides the
diff --git a/starboard/tools/testing/test_sharding_test.py b/starboard/tools/testing/test_sharding_test.py
new file mode 100644
index 0000000..8d79fbb
--- /dev/null
+++ b/starboard/tools/testing/test_sharding_test.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Tests the |test_sharding| module."""
+
+import unittest
+
+from starboard.tools.testing import test_sharding
+
+
+class ValidateTestShardingConfigTests(unittest.TestCase):
+  """
+  Test harness for validating the generation of the default shard's test
+  allocation when loading shard configuration.
+  """
+
+  TEST_TARGETS = ['foo', 'bar', 'baz']
+
+  def verify_test_run_config(self, test_target, got_chunk,
+                             expected_default_shard):
+    test_action, chunk_index, chunk_total = got_chunk
+    # Chunk Index is 1-indexed when parsing config.
+    chunk_index += 1
+    # Test is skipped
+    if test_action == test_sharding.ShardingTestConfig.SKIP_TEST:
+      self.assertNotIn(
+          test_target, expected_default_shard,
+          'Test {} is not skipped in default shard.'.format(test_target))
+    # Test is fully run
+    if test_action == test_sharding.ShardingTestConfig.RUN_FULL_TEST:
+      self.assertIn(
+          test_target, expected_default_shard,
+          'Test {} is not present in default shard'.format(test_target))
+      self.assertEqual(
+          expected_default_shard[test_target], '*',
+          'Test {} is not fully run in default shard. Instead is '
+          'run partially ({} of {} chunks)'.format(test_target, chunk_index,
+                                                   chunk_total))
+    # Test is partially run
+    if test_action == test_sharding.ShardingTestConfig.RUN_PARTIAL_TEST:
+      self.assertIn(
+          test_target, expected_default_shard,
+          'Test {} is not present in default shard'.format(test_target))
+      self.assertEqual(expected_default_shard[test_target][0], chunk_index,
+                       'Test {} has incorrect chunk index'.format(test_target))
+      self.assertEqual(expected_default_shard[test_target][1], chunk_total,
+                       'Test {} has incorrect chunk total'.format(test_target))
+
+  def load_sharding_config(self, input_shards, expected_default_shard):
+    sharding_config = test_sharding.ShardingTestConfig(
+        'default', self.TEST_TARGETS, platform_sharding_config=input_shards)
+    for test_target in self.TEST_TARGETS:
+      got_chunk = sharding_config.get_test_run_config(test_target, 0)
+      self.verify_test_run_config(test_target, got_chunk,
+                                  expected_default_shard)
+
+  def testNoTestsInShardWhole(self):
+    input_shards = [{'foo': '*'}, {'bar': '*'}, {'baz': '*'}]
+    expected_default_shard = {}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testNoTestsInShardChunked(self):
+    input_shards = [{
+        'foo': [1, 2],
+        'baz': [2, 2]
+    }, {
+        'bar': [1, 2],
+        'foo': [2, 2]
+    }, {
+        'baz': [1, 2],
+        'bar': [2, 2]
+    }]
+    expected_default_shard = {}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testWholeTestsInShard(self):
+    input_shards = [{'foo': '*'}]
+    expected_default_shard = {'bar': '*', 'baz': '*'}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testPartialTestsInShard(self):
+    # Output shard contains a test split into 2 chunks.
+    input_shards = [{'foo': '*'}, {'baz': [2, 2]}, {'bar': '*'}]
+    expected_default_shard = {'baz': [1, 2]}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+    # Output shard contains a test split into 3 chunks.
+    input_shards = [{'foo': [3, 3]}, {'foo': [1, 3]}, {'bar': '*', 'baz': '*'}]
+    expected_default_shard = {'foo': [2, 3]}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testWholeAndPartialTestsInShard(self):
+    # Output shard contains a test split into 2 chunks.
+    input_shards = [{'foo': '*'}, {'bar': [2, 2]}]
+    expected_default_shard = {'bar': [1, 2], 'baz': '*'}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+    # Output shard contains a test split into 3 chunks.
+    input_shards = [{'bar': [1, 3]}, {'bar': [2, 3]}]
+    expected_default_shard = {'bar': [3, 3], 'foo': '*', 'baz': '*'}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testManyPartialTestsInShard(self):
+    input_shards = [{
+        'bar': [1, 3]
+    }, {
+        'bar': [2, 3]
+    }, {
+        'baz': [1, 2],
+        'foo': [2, 2]
+    }]
+    expected_default_shard = {'bar': [3, 3], 'foo': [1, 2], 'baz': [2, 2]}
+    self.load_sharding_config(input_shards, expected_default_shard)
+
+  def testInvalidPossibleConfig(self):
+    input_shards = [{'foo': [1, 3]}]
+    expected_default_shard = {}  # No valid output shard is expected.
+    self.assertRaises(ValueError, self.load_sharding_config, input_shards,
+                      expected_default_shard)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/starboard/tools/unzip_file.py b/starboard/tools/unzip_file.py
new file mode 100644
index 0000000..55196d7
--- /dev/null
+++ b/starboard/tools/unzip_file.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Simple helper to unzip a given archive to a specified directory."""
+
+import argparse
+import sys
+import zipfile
+
+
+def main():
+  argument_parser = argparse.ArgumentParser()
+  argument_parser.add_argument('--zip_file', required=True)
+  argument_parser.add_argument('--output_dir', required=True)
+  args = argument_parser.parse_args()
+
+  with zipfile.ZipFile(args.zip_file, 'r') as zip_ref:
+    zip_ref.extractall(args.output_dir)
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/starboard/tools/win_symlink_fast_test.py b/starboard/tools/win_symlink_fast_test.py
index 9def20b..f828de5 100644
--- a/starboard/tools/win_symlink_fast_test.py
+++ b/starboard/tools/win_symlink_fast_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 # Copyright 2019 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/starboard/tools/win_symlink_test.py b/starboard/tools/win_symlink_test.py
index ffad024..4ea889a 100644
--- a/starboard/tools/win_symlink_test.py
+++ b/starboard/tools/win_symlink_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 # Copyright 2018 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -56,7 +56,7 @@
       external_temp_dir = tempfile.mkdtemp()
       try:
         external_temp_file = os.path.join(external_temp_dir, 'test.txt')
-        with open(external_temp_file, 'w') as fd:
+        with open(external_temp_file, 'w', encoding='utf-8') as fd:
           fd.write('HI')
         link_dir = os.path.join(self.tmp_dir, 'foo', 'link_dir')
         MakeSymLink(external_temp_file, link_dir)
@@ -71,7 +71,7 @@
       external_temp_dir = tempfile.mkdtemp()
       try:
         external_temp_file = os.path.join(external_temp_dir, 'test.txt')
-        with open(external_temp_file, 'w') as fd:
+        with open(external_temp_file, 'w', encoding='utf-8') as fd:
           fd.write('HI')
         link_dir = os.path.join(self.tmp_dir, 'foo', 'link_dir')
         MakeSymLink(external_temp_file, link_dir)
diff --git a/starboard/win/win32/cobalt/configuration.py b/starboard/win/win32/cobalt/configuration.py
index 3a44162..e342987 100644
--- a/starboard/win/win32/cobalt/configuration.py
+++ b/starboard/win/win32/cobalt/configuration.py
@@ -44,10 +44,6 @@
           '*TaskScheduler*',
           'TaskTraits*',
       ],
-      # Tracked by b/245347178
-      'persistent_settings_test': [
-          'PersistentSettingTest.*',
-      ],
       'renderer_test': [
           # Flaky test is still being counted as a fail.
           ('RendererPipelineTest.FLAKY_'
diff --git a/starboard/win/win32/test_filters.py b/starboard/win/win32/test_filters.py
index 6da9bf0..8146601 100644
--- a/starboard/win/win32/test_filters.py
+++ b/starboard/win/win32/test_filters.py
@@ -34,8 +34,8 @@
         'SbSystemGetPathTest.ReturnsRequiredPaths',
         'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.SeekAndDestroy/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
         'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.NoInput/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
-        'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.SingleInput/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
-        'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.MultipleInputs/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
+        'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.WriteSingleBatch/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
+        'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.WriteMultipleBatches/audio__null__video_beneath_the_canopy_137_avc_dmp_output_DecodeToTexture',
         'SbSocketAddressTypes/SbSocketBindTest.RainyDayBadInterface/type_ipv6_filter_ipv6',
         'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDayDestination/type_ipv6',
         'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDaySourceForDestination/type_ipv6',
@@ -45,44 +45,8 @@
     'player_filter_tests': [
         # These tests fail on our VMs for win-win32 builds due to missing
         # or non functioning system video decoders.
-        'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/0',
-        'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/2',
-        'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/0',
-        'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/2',
-        'VideoDecoderTests/VideoDecoderTest.GetCurrentDecodeTargetBeforeWriteInputBuffer/0',
-        'VideoDecoderTests/VideoDecoderTest.GetCurrentDecodeTargetBeforeWriteInputBuffer/2',
-        'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/0',
-        'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/2',
-        'VideoDecoderTests/VideoDecoderTest.MaxNumberOfCachedFrames/0',
-        'VideoDecoderTests/VideoDecoderTest.MaxNumberOfCachedFrames/2',
-        'VideoDecoderTests/VideoDecoderTest.MultipleInputs/0',
-        'VideoDecoderTests/VideoDecoderTest.MultipleInputs/2',
-        'VideoDecoderTests/VideoDecoderTest.MultipleInvalidInput/0',
-        'VideoDecoderTests/VideoDecoderTest.MultipleInvalidInput/2',
-        'VideoDecoderTests/VideoDecoderTest.MultipleResets/0',
-        'VideoDecoderTests/VideoDecoderTest.MultipleResets/2',
-        'VideoDecoderTests/VideoDecoderTest.MultipleValidInputsAfterInvalidKeyFrame/0',
-        'VideoDecoderTests/VideoDecoderTest.MultipleValidInputsAfterInvalidKeyFrame/2',
-        'VideoDecoderTests/VideoDecoderTest.OutputModeSupported/0',
-        'VideoDecoderTests/VideoDecoderTest.OutputModeSupported/2',
-        'VideoDecoderTests/VideoDecoderTest.Preroll/0',
-        'VideoDecoderTests/VideoDecoderTest.Preroll/2',
-        'VideoDecoderTests/VideoDecoderTest.PrerollFrameCount/0',
-        'VideoDecoderTests/VideoDecoderTest.PrerollFrameCount/2',
-        'VideoDecoderTests/VideoDecoderTest.PrerollTimeout/0',
-        'VideoDecoderTests/VideoDecoderTest.PrerollTimeout/2',
-        'VideoDecoderTests/VideoDecoderTest.ResetAfterInput/0',
-        'VideoDecoderTests/VideoDecoderTest.ResetAfterInput/2',
-        'VideoDecoderTests/VideoDecoderTest.ResetBeforeInput/0',
-        'VideoDecoderTests/VideoDecoderTest.ResetBeforeInput/2',
-        'VideoDecoderTests/VideoDecoderTest.SingleInput/0',
-        'VideoDecoderTests/VideoDecoderTest.SingleInput/2',
-        'VideoDecoderTests/VideoDecoderTest.SingleInvalidKeyFrame/0',
-        'VideoDecoderTests/VideoDecoderTest.SingleInvalidKeyFrame/2',
-        'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/0',
-        'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/1',
-        'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/2',
-        'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/3',
+        'VideoDecoderTests/VideoDecoderTest.*/beneath_the_canopy_137_avc_dmp_DecodeToTexture*',
+        'VideoDecoderTests/VideoDecoderTest.*/black_test_avc_1080p_30to60_fps_dmp_DecodeToTexture*',
 
         # PlayerComponentsTests fail on our VMs. Preroll callback is always not called in
         # 5 seconds, which causes timeout error.
@@ -110,7 +74,7 @@
       logging.error('COBALT_WIN_BUILDBOT_DISABLE_TESTS=1, Tests are disabled.')
       return [test_filter.DISABLE_TESTING]
     else:
-      filters = super(WinWin32TestFilters, self).GetTestFilters()
+      filters = super().GetTestFilters()
       for target, tests in _FILTERED_TESTS.items():
         filters.extend(test_filter.TestFilter(target, test) for test in tests)
       if os.environ.get('EXPERIMENTAL_CI', '0') == '1':
diff --git a/third_party/android_game_activity/README.md b/third_party/android_game_activity/README.md
deleted file mode 100644
index 75ec583..0000000
--- a/third_party/android_game_activity/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Android GameActivity code description
-
-The source code in this directory is copied from the AndroidX GameActivity
-release package, matching the version specified in
- `//starboard/android/apk/app/build.gradle`.
-
-To learn more about GameActivity, refer to [the official GameActivity
-documenation](https://d.android.com/games/agdk/game-activity).
-
-## Updating instructions
-
-To update GameActivity to the latest version, do the following:
-
-1. In
-   `//starboard/android/apk/app/build.gradle`, update the dependency version
-   for `androidx.games::games-activity`. The current version is `1.2.1`, and
-   you can find the latest version from [the AndroidX games release website]
-   (https://developer.android.com/jetpack/androidx/releases/games).
-1. Build Cobalt. This triggers gradle to downloaded the release package to
-   its local cache (normally under the `$HOME/.gradle` folder).
-1. Find the downloaded game-activity package, usually under
-   `$HOME/.gradle/caches/...`. The directory structure should match the
-   structure under `//third_party/android_game_activity/include/...`. You can
-   use `find` with a specific file to locate the exact path for the package,
-   as shown in the following example:
-   ```
-     find   ~/.gradle/caches   | grep   GameActivity.cpp
-   ```
-1. Copy all C++ files for the matching games-activity version to this
-   directory (the directory that is hosting this README.md file). For example,
-   with version 1.2.1, the path might be `$HOME/.gradle/caches/transforms-3/355ab20937e7dabe38cca2293f9f651b/transformed/jetified-games-activity-1.2.1/prefab/modules/game-activity/include/game-activity/GameActivity.cpp`, just pull the content from
-   `.../jetified-games-activity-1.2.1/prefab/modules/game-activity`:
-   ```
-   pushd ${cobalt_src_dir}/third_party/android_game_activity
-   rm -fr include module.json
-   cp -fr .../jetified-games-activity-1.2.1/prefab/modules/game-activity  third_party/android_game_activity/
-   popd
-   ```
diff --git a/third_party/android_game_activity/include/game-activity/GameActivity.cpp b/third_party/android_game_activity/include/game-activity/GameActivity.cpp
deleted file mode 100644
index 14ae78c..0000000
--- a/third_party/android_game_activity/include/game-activity/GameActivity.cpp
+++ /dev/null
@@ -1,1275 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-#define LOG_TAG "GameActivity"
-
-#include "GameActivity.h"
-
-#include <android/api-level.h>
-#include <android/asset_manager.h>
-#include <android/asset_manager_jni.h>
-#include <android/log.h>
-#include <android/looper.h>
-#include <android/native_window.h>
-#include <android/native_window_jni.h>
-#include <dlfcn.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <jni.h>
-#include <poll.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/system_properties.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <memory>
-#include <mutex>
-#include <string>
-
-// TODO(b/187147166): these functions were extracted from the Game SDK
-// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used
-// instead.
-namespace {
-
-#if __ANDROID_API__ >= 26
-std::string getSystemPropViaCallback(const char *key,
-                                     const char *default_value = "") {
-    const prop_info *prop = __system_property_find(key);
-    if (prop == nullptr) {
-        return default_value;
-    }
-    std::string return_value;
-    auto thunk = [](void *cookie, const char * /*name*/, const char *value,
-                    uint32_t /*serial*/) {
-        if (value != nullptr) {
-            std::string *r = static_cast<std::string *>(cookie);
-            *r = value;
-        }
-    };
-    __system_property_read_callback(prop, thunk, &return_value);
-    return return_value;
-}
-#else
-std::string getSystemPropViaGet(const char *key,
-                                const char *default_value = "") {
-    char buffer[PROP_VALUE_MAX + 1] = "";  // +1 for terminator
-    int bufferLen = __system_property_get(key, buffer);
-    if (bufferLen > 0)
-        return buffer;
-    else
-        return "";
-}
-#endif
-
-std::string GetSystemProp(const char *key, const char *default_value = "") {
-#if __ANDROID_API__ >= 26
-    return getSystemPropViaCallback(key, default_value);
-#else
-    return getSystemPropViaGet(key, default_value);
-#endif
-}
-
-int GetSystemPropAsInt(const char *key, int default_value = 0) {
-    std::string prop = GetSystemProp(key);
-    return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
-}
-
-struct OwnedGameTextInputState {
-    OwnedGameTextInputState &operator=(const GameTextInputState &rhs) {
-        inner = rhs;
-        owned_string = std::string(rhs.text_UTF8, rhs.text_length);
-        inner.text_UTF8 = owned_string.data();
-        return *this;
-    }
-    GameTextInputState inner;
-    std::string owned_string;
-};
-
-}  // anonymous namespace
-
-#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);
-#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);
-#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);
-#ifdef NDEBUG
-#define ALOGV(...)
-#else
-#define ALOGV(...) \
-    __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
-#endif
-
-/* Returns 2nd arg.  Used to substitute default value if caller's vararg list
- * is empty.
- */
-#define __android_second(first, second, ...) second
-
-/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise
- * returns nothing.
- */
-#define __android_rest(first, ...) , ##__VA_ARGS__
-
-#define android_printAssert(cond, tag, fmt...) \
-    __android_log_assert(cond, tag,            \
-                         __android_second(0, ##fmt, NULL) __android_rest(fmt))
-
-#define CONDITION(cond) (__builtin_expect((cond) != 0, 0))
-
-#ifndef LOG_ALWAYS_FATAL_IF
-#define LOG_ALWAYS_FATAL_IF(cond, ...)                                \
-    ((CONDITION(cond))                                                \
-         ? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \
-         : (void)0)
-#endif
-
-#ifndef LOG_ALWAYS_FATAL
-#define LOG_ALWAYS_FATAL(...) \
-    (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
-#endif
-
-/*
- * Simplified macro to send a warning system log message using current LOG_TAG.
- */
-#ifndef SLOGW
-#define SLOGW(...) \
-    ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
-#endif
-
-#ifndef SLOGW_IF
-#define SLOGW_IF(cond, ...)                                                    \
-    ((__predict_false(cond))                                                   \
-         ? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
-         : (void)0)
-#endif
-
-/*
- * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that
- * are stripped out of release builds.
- */
-#if LOG_NDEBUG
-
-#ifndef LOG_FATAL_IF
-#define LOG_FATAL_IF(cond, ...) ((void)0)
-#endif
-#ifndef LOG_FATAL
-#define LOG_FATAL(...) ((void)0)
-#endif
-
-#else
-
-#ifndef LOG_FATAL_IF
-#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
-#endif
-#ifndef LOG_FATAL
-#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
-#endif
-
-#endif
-
-/*
- * Assertion that generates a log message when the assertion fails.
- * Stripped out of release builds.  Uses the current LOG_TAG.
- */
-#ifndef ALOG_ASSERT
-#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
-#endif
-
-#define LOG_TRACE(...)
-
-#ifndef NELEM
-#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
-#endif
-
-/*
- * JNI methods of the GameActivity Java class.
- */
-static struct {
-    jmethodID finish;
-    jmethodID setWindowFlags;
-    jmethodID getWindowInsets;
-    jmethodID getWaterfallInsets;
-    jmethodID setImeEditorInfoFields;
-} gGameActivityClassInfo;
-
-/*
- * JNI fields of the androidx.core.graphics.Insets Java class.
- */
-static struct {
-    jfieldID left;
-    jfieldID right;
-    jfieldID top;
-    jfieldID bottom;
-} gInsetsClassInfo;
-
-/*
- * JNI methods of the WindowInsetsCompat.Type Java class.
- */
-static struct {
-    jmethodID methods[GAMECOMMON_INSETS_TYPE_COUNT];
-    jclass clazz;
-} gWindowInsetsCompatTypeClassInfo;
-
-/*
- * Contains a command to be executed by the GameActivity
- * on the application main thread.
- */
-struct ActivityWork {
-    int32_t cmd;
-    int64_t arg1;
-    int64_t arg2;
-};
-
-/*
- * The type of commands that can be passed to the GameActivity and that
- * are executed on the application main thread.
- */
-enum {
-    CMD_FINISH = 1,
-    CMD_SET_WINDOW_FORMAT,
-    CMD_SET_WINDOW_FLAGS,
-    CMD_SHOW_SOFT_INPUT,
-    CMD_HIDE_SOFT_INPUT,
-    CMD_SET_SOFT_INPUT_STATE
-};
-
-/*
- * Write a command to be executed by the GameActivity on the application main
- * thread.
- */
-static void write_work(int fd, int32_t cmd, int64_t arg1 = 0,
-                       int64_t arg2 = 0) {
-    ActivityWork work;
-    work.cmd = cmd;
-    work.arg1 = arg1;
-    work.arg2 = arg2;
-
-    LOG_TRACE("write_work: cmd=%d", cmd);
-restart:
-    int res = write(fd, &work, sizeof(work));
-    if (res < 0 && errno == EINTR) {
-        goto restart;
-    }
-
-    if (res == sizeof(work)) return;
-
-    if (res < 0) {
-        ALOGW("Failed writing to work fd: %s", strerror(errno));
-    } else {
-        ALOGW("Truncated writing to work fd: %d", res);
-    }
-}
-
-/*
- * Read commands to be executed by the GameActivity on the application main
- * thread.
- */
-static bool read_work(int fd, ActivityWork *outWork) {
-    int res = read(fd, outWork, sizeof(ActivityWork));
-    // no need to worry about EINTR, poll loop will just come back again.
-    if (res == sizeof(ActivityWork)) return true;
-
-    if (res < 0) {
-        ALOGW("Failed reading work fd: %s", strerror(errno));
-    } else {
-        ALOGW("Truncated reading work fd: %d", res);
-    }
-    return false;
-}
-
-/*
- * Native state for interacting with the GameActivity class.
- */
-struct NativeCode : public GameActivity {
-    NativeCode() {
-        memset((GameActivity *)this, 0, sizeof(GameActivity));
-        memset(&callbacks, 0, sizeof(callbacks));
-        memset(&insetsState, 0, sizeof(insetsState));
-        nativeWindow = NULL;
-        mainWorkRead = mainWorkWrite = -1;
-        gameTextInput = NULL;
-    }
-
-    ~NativeCode() {
-        if (callbacks.onDestroy != NULL) {
-            callbacks.onDestroy(this);
-        }
-        if (env != NULL) {
-            if (javaGameActivity != NULL) {
-                env->DeleteGlobalRef(javaGameActivity);
-            }
-            if (javaAssetManager != NULL) {
-                env->DeleteGlobalRef(javaAssetManager);
-            }
-        }
-        GameTextInput_destroy(gameTextInput);
-        if (looper != NULL && mainWorkRead >= 0) {
-            ALooper_removeFd(looper, mainWorkRead);
-        }
-        ALooper_release(looper);
-        looper = NULL;
-
-        setSurface(NULL);
-        if (mainWorkRead >= 0) close(mainWorkRead);
-        if (mainWorkWrite >= 0) close(mainWorkWrite);
-    }
-
-    void setSurface(jobject _surface) {
-        if (nativeWindow != NULL) {
-            ANativeWindow_release(nativeWindow);
-        }
-        if (_surface != NULL) {
-            nativeWindow = ANativeWindow_fromSurface(env, _surface);
-        } else {
-            nativeWindow = NULL;
-        }
-    }
-
-    GameActivityCallbacks callbacks;
-
-    std::string internalDataPathObj;
-    std::string externalDataPathObj;
-    std::string obbPathObj;
-
-    ANativeWindow *nativeWindow;
-    int32_t lastWindowWidth;
-    int32_t lastWindowHeight;
-
-    // These are used to wake up the main thread to process work.
-    int mainWorkRead;
-    int mainWorkWrite;
-    ALooper *looper;
-
-    // Need to hold on to a reference here in case the upper layers destroy our
-    // AssetManager.
-    jobject javaAssetManager;
-
-    GameTextInput *gameTextInput;
-    // Set by users in GameActivity_setTextInputState, then passed to
-    // GameTextInput.
-    OwnedGameTextInputState gameTextInputState;
-    std::mutex gameTextInputStateMutex;
-
-    ARect insetsState[GAMECOMMON_INSETS_TYPE_COUNT];
-};
-
-extern "C" void GameActivity_finish(GameActivity *activity) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    write_work(code->mainWorkWrite, CMD_FINISH, 0);
-}
-
-extern "C" void GameActivity_setWindowFlags(GameActivity *activity,
-                                            uint32_t values, uint32_t mask) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    write_work(code->mainWorkWrite, CMD_SET_WINDOW_FLAGS, values, mask);
-}
-
-extern "C" void GameActivity_showSoftInput(GameActivity *activity,
-                                           uint32_t flags) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    write_work(code->mainWorkWrite, CMD_SHOW_SOFT_INPUT, flags);
-}
-
-extern "C" void GameActivity_setTextInputState(
-    GameActivity *activity, const GameTextInputState *state) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    std::lock_guard<std::mutex> lock(code->gameTextInputStateMutex);
-    code->gameTextInputState = *state;
-    write_work(code->mainWorkWrite, CMD_SET_SOFT_INPUT_STATE);
-}
-
-extern "C" void GameActivity_getTextInputState(
-    GameActivity *activity, GameTextInputGetStateCallback callback,
-    void *context) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    return GameTextInput_getState(code->gameTextInput, callback, context);
-}
-
-extern "C" void GameActivity_hideSoftInput(GameActivity *activity,
-                                           uint32_t flags) {
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    write_work(code->mainWorkWrite, CMD_HIDE_SOFT_INPUT, flags);
-}
-
-extern "C" void GameActivity_getWindowInsets(GameActivity *activity,
-                                             GameCommonInsetsType type,
-                                             ARect *insets) {
-    if (type < 0 || type >= GAMECOMMON_INSETS_TYPE_COUNT) return;
-    NativeCode *code = static_cast<NativeCode *>(activity);
-    *insets = code->insetsState[type];
-}
-
-extern "C" GameTextInput *GameActivity_getTextInput(
-    const GameActivity *activity) {
-    const NativeCode *code = static_cast<const NativeCode *>(activity);
-    return code->gameTextInput;
-}
-
-/*
- * Log the JNI exception, if any.
- */
-static void checkAndClearException(JNIEnv *env, const char *methodName) {
-    if (env->ExceptionCheck()) {
-        ALOGE("Exception while running %s", methodName);
-        env->ExceptionDescribe();
-        env->ExceptionClear();
-    }
-}
-
-/*
- * Callback for handling native events on the application's main thread.
- */
-static int mainWorkCallback(int fd, int events, void *data) {
-    ALOGD("************** mainWorkCallback *********");
-    NativeCode *code = (NativeCode *)data;
-    if ((events & POLLIN) == 0) {
-        return 1;
-    }
-
-    ActivityWork work;
-    if (!read_work(code->mainWorkRead, &work)) {
-        return 1;
-    }
-    LOG_TRACE("mainWorkCallback: cmd=%d", work.cmd);
-    switch (work.cmd) {
-        case CMD_FINISH: {
-            code->env->CallVoidMethod(code->javaGameActivity,
-                                      gGameActivityClassInfo.finish);
-            checkAndClearException(code->env, "finish");
-        } break;
-        case CMD_SET_WINDOW_FLAGS: {
-            code->env->CallVoidMethod(code->javaGameActivity,
-                                      gGameActivityClassInfo.setWindowFlags,
-                                      work.arg1, work.arg2);
-            checkAndClearException(code->env, "setWindowFlags");
-        } break;
-        case CMD_SHOW_SOFT_INPUT: {
-            GameTextInput_showIme(code->gameTextInput, work.arg1);
-        } break;
-        case CMD_SET_SOFT_INPUT_STATE: {
-            std::lock_guard<std::mutex> lock(code->gameTextInputStateMutex);
-            GameTextInput_setState(code->gameTextInput,
-                                   &code->gameTextInputState.inner);
-            checkAndClearException(code->env, "setTextInputState");
-        } break;
-        case CMD_HIDE_SOFT_INPUT: {
-            GameTextInput_hideIme(code->gameTextInput, work.arg1);
-        } break;
-        default:
-            ALOGW("Unknown work command: %d", work.cmd);
-            break;
-    }
-
-    return 1;
-}
-
-// ------------------------------------------------------------------------
-static thread_local std::string g_error_msg;
-
-static jlong initializeNativeCode_native(JNIEnv *env, jobject javaGameActivity,
-                                   jstring internalDataDir, jstring obbDir,
-                                   jstring externalDataDir, jobject jAssetMgr,
-                                   jbyteArray savedState) {
-    LOG_TRACE("initializeNativeCode_native");
-    NativeCode *code = NULL;
-
-    code = new NativeCode();
-
-    code->looper = ALooper_forThread();
-    if (code->looper == nullptr) {
-        g_error_msg = "Unable to retrieve native ALooper";
-        ALOGW("%s", g_error_msg.c_str());
-        delete code;
-        return 0;
-    }
-    ALooper_acquire(code->looper);
-
-    int msgpipe[2];
-    if (pipe(msgpipe)) {
-        g_error_msg = "could not create pipe: ";
-        g_error_msg += strerror(errno);
-
-        ALOGW("%s", g_error_msg.c_str());
-        delete code;
-        return 0;
-    }
-    code->mainWorkRead = msgpipe[0];
-    code->mainWorkWrite = msgpipe[1];
-    int result = fcntl(code->mainWorkRead, F_SETFL, O_NONBLOCK);
-    SLOGW_IF(result != 0,
-             "Could not make main work read pipe "
-             "non-blocking: %s",
-             strerror(errno));
-    result = fcntl(code->mainWorkWrite, F_SETFL, O_NONBLOCK);
-    SLOGW_IF(result != 0,
-             "Could not make main work write pipe "
-             "non-blocking: %s",
-             strerror(errno));
-    ALooper_addFd(code->looper, code->mainWorkRead, 0, ALOOPER_EVENT_INPUT,
-                  mainWorkCallback, code);
-
-    code->GameActivity::callbacks = &code->callbacks;
-    if (env->GetJavaVM(&code->vm) < 0) {
-        ALOGW("GameActivity GetJavaVM failed");
-        delete code;
-        return 0;
-    }
-    code->env = env;
-    code->javaGameActivity = env->NewGlobalRef(javaGameActivity);
-
-    const char *dirStr =
-        internalDataDir ? env->GetStringUTFChars(internalDataDir, NULL) : "";
-    code->internalDataPathObj = dirStr;
-    code->internalDataPath = code->internalDataPathObj.c_str();
-    if (internalDataDir) env->ReleaseStringUTFChars(internalDataDir, dirStr);
-
-    dirStr =
-        externalDataDir ? env->GetStringUTFChars(externalDataDir, NULL) : "";
-    code->externalDataPathObj = dirStr;
-    code->externalDataPath = code->externalDataPathObj.c_str();
-    if (externalDataDir) env->ReleaseStringUTFChars(externalDataDir, dirStr);
-
-    code->javaAssetManager = env->NewGlobalRef(jAssetMgr);
-    code->assetManager = AAssetManager_fromJava(env, jAssetMgr);
-
-    dirStr = obbDir ? env->GetStringUTFChars(obbDir, NULL) : "";
-    code->obbPathObj = dirStr;
-    code->obbPath = code->obbPathObj.c_str();
-    if (obbDir) env->ReleaseStringUTFChars(obbDir, dirStr);
-
-    jbyte *rawSavedState = NULL;
-    jsize rawSavedSize = 0;
-    if (savedState != NULL) {
-        rawSavedState = env->GetByteArrayElements(savedState, NULL);
-        rawSavedSize = env->GetArrayLength(savedState);
-    }
-    GameActivity_onCreate(code, rawSavedState, rawSavedSize);
-
-    code->gameTextInput = GameTextInput_init(env, 0);
-    GameTextInput_setEventCallback(code->gameTextInput,
-                                   reinterpret_cast<GameTextInputEventCallback>(
-                                       code->callbacks.onTextInputEvent),
-                                   code);
-
-    if (rawSavedState != NULL) {
-        env->ReleaseByteArrayElements(savedState, rawSavedState, 0);
-    }
-
-    return reinterpret_cast<jlong>(code);
-}
-
-static jstring getDlError_native(JNIEnv *env, jobject javaGameActivity) {
-    jstring result = env->NewStringUTF(g_error_msg.c_str());
-    g_error_msg.clear();
-    return result;
-}
-
-static void terminateNativeCode_native(JNIEnv *env, jobject javaGameActivity,
-                                    jlong handle) {
-    LOG_TRACE("terminateNativeCode_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        delete code;
-    }
-}
-
-static void onStart_native(JNIEnv *env, jobject javaGameActivity,
-                           jlong handle) {
-    ALOGV("onStart_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onStart != NULL) {
-            code->callbacks.onStart(code);
-        }
-    }
-}
-
-static void onResume_native(JNIEnv *env, jobject javaGameActivity,
-                            jlong handle) {
-    LOG_TRACE("onResume_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onResume != NULL) {
-            code->callbacks.onResume(code);
-        }
-    }
-}
-
-struct SaveInstanceLocals {
-    JNIEnv *env;
-    jbyteArray array;
-};
-
-static jbyteArray onSaveInstanceState_native(JNIEnv *env,
-                                             jobject javaGameActivity,
-                                             jlong handle) {
-    LOG_TRACE("onSaveInstanceState_native");
-
-    SaveInstanceLocals locals{
-        env, NULL};  // Passed through the user's state prep function.
-
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onSaveInstanceState != NULL) {
-            code->callbacks.onSaveInstanceState(
-                code,
-                [](const char *bytes, int len, void *context) {
-                    auto locals = static_cast<SaveInstanceLocals *>(context);
-                    if (len > 0) {
-                        locals->array = locals->env->NewByteArray(len);
-                        if (locals->array != NULL) {
-                            locals->env->SetByteArrayRegion(
-                                locals->array, 0, len, (const jbyte *)bytes);
-                        }
-                    }
-                },
-                &locals);
-        }
-    }
-    return locals.array;
-}
-
-static void onPause_native(JNIEnv *env, jobject javaGameActivity,
-                           jlong handle) {
-    LOG_TRACE("onPause_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onPause != NULL) {
-            code->callbacks.onPause(code);
-        }
-    }
-}
-
-static void onStop_native(JNIEnv *env, jobject javaGameActivity, jlong handle) {
-    LOG_TRACE("onStop_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onStop != NULL) {
-            code->callbacks.onStop(code);
-        }
-    }
-}
-
-static void onConfigurationChanged_native(JNIEnv *env, jobject javaGameActivity,
-                                          jlong handle) {
-    LOG_TRACE("onConfigurationChanged_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onConfigurationChanged != NULL) {
-            code->callbacks.onConfigurationChanged(code);
-        }
-    }
-}
-
-static void onTrimMemory_native(JNIEnv *env, jobject javaGameActivity,
-                                jlong handle, jint level) {
-    LOG_TRACE("onTrimMemory_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onTrimMemory != NULL) {
-            code->callbacks.onTrimMemory(code, level);
-        }
-    }
-}
-
-static void onWindowFocusChanged_native(JNIEnv *env, jobject javaGameActivity,
-                                        jlong handle, jboolean focused) {
-    LOG_TRACE("onWindowFocusChanged_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->callbacks.onWindowFocusChanged != NULL) {
-            code->callbacks.onWindowFocusChanged(code, focused ? 1 : 0);
-        }
-    }
-}
-
-static void onSurfaceCreated_native(JNIEnv *env, jobject javaGameActivity,
-                                    jlong handle, jobject surface) {
-    ALOGV("onSurfaceCreated_native");
-    LOG_TRACE("onSurfaceCreated_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        code->setSurface(surface);
-
-        if (code->nativeWindow != NULL &&
-            code->callbacks.onNativeWindowCreated != NULL) {
-            code->callbacks.onNativeWindowCreated(code, code->nativeWindow);
-        }
-    }
-}
-
-static void onSurfaceChanged_native(JNIEnv *env, jobject javaGameActivity,
-                                    jlong handle, jobject surface, jint format,
-                                    jint width, jint height) {
-    LOG_TRACE("onSurfaceChanged_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        ANativeWindow *oldNativeWindow = code->nativeWindow;
-        // Fix for window being destroyed behind the scenes on older Android
-        // versions.
-        if (oldNativeWindow != NULL) {
-            ANativeWindow_acquire(oldNativeWindow);
-        }
-        code->setSurface(surface);
-        if (oldNativeWindow != code->nativeWindow) {
-            if (oldNativeWindow != NULL &&
-                code->callbacks.onNativeWindowDestroyed != NULL) {
-                code->callbacks.onNativeWindowDestroyed(code, oldNativeWindow);
-            }
-            if (code->nativeWindow != NULL) {
-                if (code->callbacks.onNativeWindowCreated != NULL) {
-                    code->callbacks.onNativeWindowCreated(code,
-                                                          code->nativeWindow);
-                }
-
-                code->lastWindowWidth =
-                    ANativeWindow_getWidth(code->nativeWindow);
-                code->lastWindowHeight =
-                    ANativeWindow_getHeight(code->nativeWindow);
-            }
-        } else {
-            // Maybe it was resized?
-            int32_t newWidth = ANativeWindow_getWidth(code->nativeWindow);
-            int32_t newHeight = ANativeWindow_getHeight(code->nativeWindow);
-            if (newWidth != code->lastWindowWidth ||
-                newHeight != code->lastWindowHeight) {
-                if (code->callbacks.onNativeWindowResized != NULL) {
-                    code->callbacks.onNativeWindowResized(
-                        code, code->nativeWindow, newWidth, newHeight);
-                }
-            }
-        }
-        // Release the window we acquired earlier.
-        if (oldNativeWindow != NULL) {
-            ANativeWindow_release(oldNativeWindow);
-        }
-    }
-}
-
-static void onSurfaceRedrawNeeded_native(JNIEnv *env, jobject javaGameActivity,
-                                         jlong handle) {
-    LOG_TRACE("onSurfaceRedrawNeeded_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->nativeWindow != NULL &&
-            code->callbacks.onNativeWindowRedrawNeeded != NULL) {
-            code->callbacks.onNativeWindowRedrawNeeded(code,
-                                                       code->nativeWindow);
-        }
-    }
-}
-
-static void onSurfaceDestroyed_native(JNIEnv *env, jobject javaGameActivity,
-                                      jlong handle) {
-    LOG_TRACE("onSurfaceDestroyed_native");
-    if (handle != 0) {
-        NativeCode *code = (NativeCode *)handle;
-        if (code->nativeWindow != NULL &&
-            code->callbacks.onNativeWindowDestroyed != NULL) {
-            code->callbacks.onNativeWindowDestroyed(code, code->nativeWindow);
-        }
-        code->setSurface(NULL);
-    }
-}
-
-static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
-    /* AMOTION_EVENT_AXIS_X */ true,
-    /* AMOTION_EVENT_AXIS_Y */ true,
-    // Disable all other axes by default (they can be enabled using
-    // `GameActivityPointerAxes_enableAxis`).
-    false};
-
-extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
-    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
-        return;
-    }
-
-    enabledAxes[axis] = true;
-}
-
-extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
-    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
-        return;
-    }
-
-    enabledAxes[axis] = false;
-}
-
-extern "C" void GameActivity_setImeEditorInfo(GameActivity *activity,
-                                              int inputType, int actionId,
-                                              int imeOptions) {
-    JNIEnv *env;
-    if (activity->vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
-        env->CallVoidMethod(activity->javaGameActivity,
-                            gGameActivityClassInfo.setImeEditorInfoFields,
-                            inputType, actionId, imeOptions);
-    }
-}
-
-static struct {
-    jmethodID getDeviceId;
-    jmethodID getSource;
-    jmethodID getAction;
-
-    jmethodID getEventTime;
-    jmethodID getDownTime;
-
-    jmethodID getFlags;
-    jmethodID getMetaState;
-
-    jmethodID getActionButton;
-    jmethodID getButtonState;
-    jmethodID getClassification;
-    jmethodID getEdgeFlags;
-
-    jmethodID getPointerCount;
-    jmethodID getPointerId;
-    jmethodID getRawX;
-    jmethodID getRawY;
-    jmethodID getXPrecision;
-    jmethodID getYPrecision;
-    jmethodID getAxisValue;
-} gMotionEventClassInfo;
-
-extern "C" void GameActivityMotionEvent_fromJava(
-    JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) {
-    static bool gMotionEventClassInfoInitialized = false;
-    if (!gMotionEventClassInfoInitialized) {
-        int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
-        gMotionEventClassInfo = {0};
-        jclass motionEventClass = env->FindClass("android/view/MotionEvent");
-        gMotionEventClassInfo.getDeviceId =
-            env->GetMethodID(motionEventClass, "getDeviceId", "()I");
-        gMotionEventClassInfo.getSource =
-            env->GetMethodID(motionEventClass, "getSource", "()I");
-        gMotionEventClassInfo.getAction =
-            env->GetMethodID(motionEventClass, "getAction", "()I");
-        gMotionEventClassInfo.getEventTime =
-            env->GetMethodID(motionEventClass, "getEventTime", "()J");
-        gMotionEventClassInfo.getDownTime =
-            env->GetMethodID(motionEventClass, "getDownTime", "()J");
-        gMotionEventClassInfo.getFlags =
-            env->GetMethodID(motionEventClass, "getFlags", "()I");
-        gMotionEventClassInfo.getMetaState =
-            env->GetMethodID(motionEventClass, "getMetaState", "()I");
-        if (sdkVersion >= 23) {
-            gMotionEventClassInfo.getActionButton =
-                env->GetMethodID(motionEventClass, "getActionButton", "()I");
-        }
-        if (sdkVersion >= 14) {
-            gMotionEventClassInfo.getButtonState =
-                env->GetMethodID(motionEventClass, "getButtonState", "()I");
-        }
-        if (sdkVersion >= 29) {
-            gMotionEventClassInfo.getClassification =
-                env->GetMethodID(motionEventClass, "getClassification", "()I");
-        }
-        gMotionEventClassInfo.getEdgeFlags =
-            env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");
-        gMotionEventClassInfo.getPointerCount =
-            env->GetMethodID(motionEventClass, "getPointerCount", "()I");
-        gMotionEventClassInfo.getPointerId =
-            env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
-        if (sdkVersion >= 29) {
-            gMotionEventClassInfo.getRawX =
-                env->GetMethodID(motionEventClass, "getRawX", "(I)F");
-            gMotionEventClassInfo.getRawY =
-                env->GetMethodID(motionEventClass, "getRawY", "(I)F");
-        }
-        gMotionEventClassInfo.getXPrecision =
-            env->GetMethodID(motionEventClass, "getXPrecision", "()F");
-        gMotionEventClassInfo.getYPrecision =
-            env->GetMethodID(motionEventClass, "getYPrecision", "()F");
-        gMotionEventClassInfo.getAxisValue =
-            env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");
-
-        gMotionEventClassInfoInitialized = true;
-    }
-
-    int pointerCount =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
-    pointerCount =
-        std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
-    out_event->pointerCount = pointerCount;
-    for (int i = 0; i < pointerCount; ++i) {
-        out_event->pointers[i] = {
-            /*id=*/env->CallIntMethod(motionEvent,
-                                      gMotionEventClassInfo.getPointerId, i),
-            /*axisValues=*/{0},
-            /*rawX=*/gMotionEventClassInfo.getRawX
-                ? env->CallFloatMethod(motionEvent,
-                                       gMotionEventClassInfo.getRawX, i)
-                : 0,
-            /*rawY=*/gMotionEventClassInfo.getRawY
-                ? env->CallFloatMethod(motionEvent,
-                                       gMotionEventClassInfo.getRawY, i)
-                : 0,
-        };
-
-        for (int axisIndex = 0;
-             axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
-            if (enabledAxes[axisIndex]) {
-                out_event->pointers[i].axisValues[axisIndex] =
-                    env->CallFloatMethod(motionEvent,
-                                         gMotionEventClassInfo.getAxisValue,
-                                         axisIndex, i);
-            }
-        }
-    }
-
-    out_event->deviceId =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
-    out_event->source =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
-    out_event->action =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
-    out_event->eventTime =
-        env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
-        1000000;
-    out_event->downTime =
-        env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
-        1000000;
-    out_event->flags =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
-    out_event->metaState =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
-    out_event->actionButton =
-        gMotionEventClassInfo.getActionButton
-            ? env->CallIntMethod(motionEvent,
-                                 gMotionEventClassInfo.getActionButton)
-            : 0;
-    out_event->buttonState =
-        gMotionEventClassInfo.getButtonState
-            ? env->CallIntMethod(motionEvent,
-                                 gMotionEventClassInfo.getButtonState)
-            : 0;
-    out_event->classification =
-        gMotionEventClassInfo.getClassification
-            ? env->CallIntMethod(motionEvent,
-                                 gMotionEventClassInfo.getClassification)
-            : 0;
-    out_event->edgeFlags =
-        env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
-    out_event->precisionX =
-        env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
-    out_event->precisionY =
-        env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
-}
-
-static struct {
-    jmethodID getDeviceId;
-    jmethodID getSource;
-    jmethodID getAction;
-
-    jmethodID getEventTime;
-    jmethodID getDownTime;
-
-    jmethodID getFlags;
-    jmethodID getMetaState;
-
-    jmethodID getModifiers;
-    jmethodID getRepeatCount;
-    jmethodID getKeyCode;
-    jmethodID getUnicodeChar;
-} gKeyEventClassInfo;
-
-extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
-                                              GameActivityKeyEvent *out_event) {
-    static bool gKeyEventClassInfoInitialized = false;
-    if (!gKeyEventClassInfoInitialized) {
-        int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
-        gKeyEventClassInfo = {0};
-        jclass keyEventClass = env->FindClass("android/view/KeyEvent");
-        gKeyEventClassInfo.getDeviceId =
-            env->GetMethodID(keyEventClass, "getDeviceId", "()I");
-        gKeyEventClassInfo.getSource =
-            env->GetMethodID(keyEventClass, "getSource", "()I");
-        gKeyEventClassInfo.getAction =
-            env->GetMethodID(keyEventClass, "getAction", "()I");
-        gKeyEventClassInfo.getEventTime =
-            env->GetMethodID(keyEventClass, "getEventTime", "()J");
-        gKeyEventClassInfo.getDownTime =
-            env->GetMethodID(keyEventClass, "getDownTime", "()J");
-        gKeyEventClassInfo.getFlags =
-            env->GetMethodID(keyEventClass, "getFlags", "()I");
-        gKeyEventClassInfo.getMetaState =
-            env->GetMethodID(keyEventClass, "getMetaState", "()I");
-        if (sdkVersion >= 13) {
-            gKeyEventClassInfo.getModifiers =
-                env->GetMethodID(keyEventClass, "getModifiers", "()I");
-        }
-        gKeyEventClassInfo.getRepeatCount =
-            env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
-        gKeyEventClassInfo.getKeyCode =
-            env->GetMethodID(keyEventClass, "getKeyCode", "()I");
-        gKeyEventClassInfo.getUnicodeChar =
-            env->GetMethodID(keyEventClass, "getUnicodeChar", "()I");
-
-        gKeyEventClassInfoInitialized = true;
-    }
-
-    *out_event = {
-        /*deviceId=*/env->CallIntMethod(keyEvent,
-                                        gKeyEventClassInfo.getDeviceId),
-        /*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource),
-        /*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction),
-        // TODO: introduce a millisecondsToNanoseconds helper:
-        /*eventTime=*/
-        env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) *
-            1000000,
-        /*downTime=*/
-        env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000,
-        /*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags),
-        /*metaState=*/
-        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState),
-        /*modifiers=*/gKeyEventClassInfo.getModifiers
-            ? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers)
-            : 0,
-        /*repeatCount=*/
-        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount),
-        /*keyCode=*/
-        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode),
-        /*unicodeChar=*/
-        env->CallIntMethod(keyEvent, gKeyEventClassInfo.getUnicodeChar)};
-}
-
-static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity,
-                                jlong handle, jobject motionEvent) {
-    if (handle == 0) return false;
-    NativeCode *code = (NativeCode *)handle;
-    if (code->callbacks.onTouchEvent == nullptr) return false;
-
-    static GameActivityMotionEvent c_event;
-    GameActivityMotionEvent_fromJava(env, motionEvent, &c_event);
-    return code->callbacks.onTouchEvent(code, &c_event);
-}
-
-static bool onKeyUp_native(JNIEnv *env, jobject javaGameActivity, jlong handle,
-                           jobject keyEvent) {
-    if (handle == 0) return false;
-    NativeCode *code = (NativeCode *)handle;
-    if (code->callbacks.onKeyUp == nullptr) return false;
-
-    static GameActivityKeyEvent c_event;
-    GameActivityKeyEvent_fromJava(env, keyEvent, &c_event);
-    return code->callbacks.onKeyUp(code, &c_event);
-}
-
-static bool onKeyDown_native(JNIEnv *env, jobject javaGameActivity,
-                             jlong handle, jobject keyEvent) {
-    if (handle == 0) return false;
-    NativeCode *code = (NativeCode *)handle;
-    if (code->callbacks.onKeyDown == nullptr) return false;
-
-    static GameActivityKeyEvent c_event;
-    GameActivityKeyEvent_fromJava(env, keyEvent, &c_event);
-    return code->callbacks.onKeyDown(code, &c_event);
-}
-
-static void onTextInput_native(JNIEnv *env, jobject activity, jlong handle,
-                               jobject textInputEvent) {
-    if (handle == 0) return;
-    NativeCode *code = (NativeCode *)handle;
-    GameTextInput_processEvent(code->gameTextInput, textInputEvent);
-}
-
-static void onWindowInsetsChanged_native(JNIEnv *env, jobject activity,
-                                         jlong handle) {
-    if (handle == 0) return;
-    NativeCode *code = (NativeCode *)handle;
-    if (code->callbacks.onWindowInsetsChanged == nullptr) return;
-    for (int type = 0; type < GAMECOMMON_INSETS_TYPE_COUNT; ++type) {
-        jobject jinsets;
-        // Note that waterfall insets are handled differently on the Java side.
-        if (type == GAMECOMMON_INSETS_TYPE_WATERFALL) {
-            jinsets = env->CallObjectMethod(
-                code->javaGameActivity,
-                gGameActivityClassInfo.getWaterfallInsets);
-        } else {
-            jint jtype = env->CallStaticIntMethod(
-                gWindowInsetsCompatTypeClassInfo.clazz,
-                gWindowInsetsCompatTypeClassInfo.methods[type]);
-            jinsets = env->CallObjectMethod(
-                code->javaGameActivity, gGameActivityClassInfo.getWindowInsets,
-                jtype);
-        }
-        ARect &insets = code->insetsState[type];
-        if (jinsets == nullptr) {
-            insets.left = 0;
-            insets.right = 0;
-            insets.top = 0;
-            insets.bottom = 0;
-        } else {
-            insets.left = env->GetIntField(jinsets, gInsetsClassInfo.left);
-            insets.right = env->GetIntField(jinsets, gInsetsClassInfo.right);
-            insets.top = env->GetIntField(jinsets, gInsetsClassInfo.top);
-            insets.bottom = env->GetIntField(jinsets, gInsetsClassInfo.bottom);
-        }
-    }
-    GameTextInput_processImeInsets(
-        code->gameTextInput, &code->insetsState[GAMECOMMON_INSETS_TYPE_IME]);
-    code->callbacks.onWindowInsetsChanged(code);
-}
-
-static void setInputConnection_native(JNIEnv *env, jobject activity,
-                                      jlong handle, jobject inputConnection) {
-    NativeCode *code = (NativeCode *)handle;
-    GameTextInput_setInputConnection(code->gameTextInput, inputConnection);
-}
-
-static const JNINativeMethod g_methods[] = {
-    {"initializeNativeCode",
-     "(Ljava/lang/String;Ljava/lang/String;"
-     "Ljava/lang/String;Landroid/content/res/AssetManager;[B)J",
-     (void *)initializeNativeCode_native},
-    {"getDlError", "()Ljava/lang/String;", (void *)getDlError_native},
-    {"terminateNativeCode", "(J)V", (void *)terminateNativeCode_native},
-    {"onStartNative", "(J)V", (void *)onStart_native},
-    {"onResumeNative", "(J)V", (void *)onResume_native},
-    {"onSaveInstanceStateNative", "(J)[B", (void *)onSaveInstanceState_native},
-    {"onPauseNative", "(J)V", (void *)onPause_native},
-    {"onStopNative", "(J)V", (void *)onStop_native},
-    {"onConfigurationChangedNative", "(J)V",
-     (void *)onConfigurationChanged_native},
-    {"onTrimMemoryNative", "(JI)V", (void *)onTrimMemory_native},
-    {"onWindowFocusChangedNative", "(JZ)V",
-     (void *)onWindowFocusChanged_native},
-    {"onSurfaceCreatedNative", "(JLandroid/view/Surface;)V",
-     (void *)onSurfaceCreated_native},
-    {"onSurfaceChangedNative", "(JLandroid/view/Surface;III)V",
-     (void *)onSurfaceChanged_native},
-    {"onSurfaceRedrawNeededNative", "(JLandroid/view/Surface;)V",
-     (void *)onSurfaceRedrawNeeded_native},
-    {"onSurfaceDestroyedNative", "(J)V", (void *)onSurfaceDestroyed_native},
-    {"onTouchEventNative", "(JLandroid/view/MotionEvent;)Z",
-     (void *)onTouchEvent_native},
-    {"onKeyDownNative", "(JLandroid/view/KeyEvent;)Z",
-     (void *)onKeyDown_native},
-    {"onKeyUpNative", "(JLandroid/view/KeyEvent;)Z", (void *)onKeyUp_native},
-    {"onTextInputEventNative",
-     "(JLcom/google/androidgamesdk/gametextinput/State;)V",
-     (void *)onTextInput_native},
-    {"onWindowInsetsChangedNative", "(J)V",
-     (void *)onWindowInsetsChanged_native},
-    {"setInputConnectionNative",
-     "(JLcom/google/androidgamesdk/gametextinput/InputConnection;)V",
-     (void *)setInputConnection_native},
-};
-
-static const char *const kGameActivityPathName =
-    "com/google/androidgamesdk/GameActivity";
-
-static const char *const kInsetsPathName = "androidx/core/graphics/Insets";
-
-static const char *const kWindowInsetsCompatTypePathName =
-    "androidx/core/view/WindowInsetsCompat$Type";
-
-#define FIND_CLASS(var, className)   \
-    var = env->FindClass(className); \
-    LOG_FATAL_IF(!var, "Unable to find class %s", className);
-
-#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor)  \
-    var = env->GetMethodID(clazz, methodName, fieldDescriptor); \
-    LOG_FATAL_IF(!var, "Unable to find method %s", methodName);
-
-#define GET_STATIC_METHOD_ID(var, clazz, methodName, fieldDescriptor) \
-    var = env->GetStaticMethodID(clazz, methodName, fieldDescriptor); \
-    LOG_FATAL_IF(!var, "Unable to find static method %s", methodName);
-
-#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor)  \
-    var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
-    LOG_FATAL_IF(!var, "Unable to find field %s", fieldName);
-
-static int jniRegisterNativeMethods(JNIEnv *env, const char *className,
-                                    const JNINativeMethod *methods,
-                                    int numMethods) {
-    ALOGV("Registering %s's %d native methods...", className, numMethods);
-    jclass clazz = env->FindClass(className);
-    LOG_FATAL_IF(clazz == nullptr,
-                 "Native registration unable to find class '%s'; aborting...",
-                 className);
-    int result = env->RegisterNatives(clazz, methods, numMethods);
-    env->DeleteLocalRef(clazz);
-    if (result == 0) {
-        return 0;
-    }
-
-    // Failure to register natives is fatal. Try to report the corresponding
-    // exception, otherwise abort with generic failure message.
-    jthrowable thrown = env->ExceptionOccurred();
-    if (thrown != NULL) {
-        env->ExceptionDescribe();
-        env->DeleteLocalRef(thrown);
-    }
-    LOG_FATAL("RegisterNatives failed for '%s'; aborting...", className);
-}
-
-extern "C" int GameActivity_register(JNIEnv *env) {
-    ALOGD("GameActivity_register");
-    jclass activity_class;
-    FIND_CLASS(activity_class, kGameActivityPathName);
-    GET_METHOD_ID(gGameActivityClassInfo.finish, activity_class, "finish",
-                  "()V");
-    GET_METHOD_ID(gGameActivityClassInfo.setWindowFlags, activity_class,
-                  "setWindowFlags", "(II)V");
-    GET_METHOD_ID(gGameActivityClassInfo.getWindowInsets, activity_class,
-                  "getWindowInsets", "(I)Landroidx/core/graphics/Insets;");
-    GET_METHOD_ID(gGameActivityClassInfo.getWaterfallInsets, activity_class,
-                  "getWaterfallInsets", "()Landroidx/core/graphics/Insets;");
-    GET_METHOD_ID(gGameActivityClassInfo.setImeEditorInfoFields, activity_class,
-                  "setImeEditorInfoFields", "(III)V");
-    jclass insets_class;
-    FIND_CLASS(insets_class, kInsetsPathName);
-    GET_FIELD_ID(gInsetsClassInfo.left, insets_class, "left", "I");
-    GET_FIELD_ID(gInsetsClassInfo.right, insets_class, "right", "I");
-    GET_FIELD_ID(gInsetsClassInfo.top, insets_class, "top", "I");
-    GET_FIELD_ID(gInsetsClassInfo.bottom, insets_class, "bottom", "I");
-    jclass windowInsetsCompatType_class;
-    FIND_CLASS(windowInsetsCompatType_class, kWindowInsetsCompatTypePathName);
-    gWindowInsetsCompatTypeClassInfo.clazz =
-        (jclass)env->NewGlobalRef(windowInsetsCompatType_class);
-    // These names must match, in order, the GameCommonInsetsType enum fields
-    // Note that waterfall is handled differently by the insets API, so we
-    // exclude it here.
-    const char *methodNames[GAMECOMMON_INSETS_TYPE_WATERFALL] = {
-        "captionBar",
-        "displayCutout",
-        "ime",
-        "mandatorySystemGestures",
-        "navigationBars",
-        "statusBars",
-        "systemBars",
-        "systemGestures",
-        "tappableElement"};
-    for (int i = 0; i < GAMECOMMON_INSETS_TYPE_WATERFALL; ++i) {
-        GET_STATIC_METHOD_ID(gWindowInsetsCompatTypeClassInfo.methods[i],
-                             windowInsetsCompatType_class, methodNames[i],
-                             "()I");
-    }
-    return jniRegisterNativeMethods(env, kGameActivityPathName, g_methods,
-                                    NELEM(g_methods));
-}
-
-// Register this method so that GameActiviy_register does not need to be called
-// manually.
-extern "C" JNIEXPORT jlong JNICALL
-Java_com_google_androidgamesdk_GameActivity_initializeNativeCode(
-    JNIEnv *env, jobject javaGameActivity,
-    jstring internalDataDir, jstring obbDir, jstring externalDataDir,
-    jobject jAssetMgr, jbyteArray savedState) {
-    GameActivity_register(env);
-    jlong nativeCode = initializeNativeCode_native(
-        env, javaGameActivity,internalDataDir, obbDir,
-        externalDataDir, jAssetMgr, savedState);
-    return nativeCode;
-}
diff --git a/third_party/android_game_activity/include/game-activity/GameActivity.h b/third_party/android_game_activity/include/game-activity/GameActivity.h
deleted file mode 100644
index 814a68b..0000000
--- a/third_party/android_game_activity/include/game-activity/GameActivity.h
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @addtogroup GameActivity Game Activity
- * The interface to use GameActivity.
- * @{
- */
-
-/**
- * @file GameActivity.h
- */
-
-#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_H
-#define ANDROID_GAME_SDK_GAME_ACTIVITY_H
-
-#include <android/asset_manager.h>
-#include <android/input.h>
-#include <android/native_window.h>
-#include <android/rect.h>
-#include <jni.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <sys/types.h>
-
-#include "game-text-input/gametextinput.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * {@link GameActivityCallbacks}
- */
-struct GameActivityCallbacks;
-
-/**
- * This structure defines the native side of an android.app.GameActivity.
- * It is created by the framework, and handed to the application's native
- * code as it is being launched.
- */
-typedef struct GameActivity {
-    /**
-     * Pointer to the callback function table of the native application.
-     * You can set the functions here to your own callbacks.  The callbacks
-     * pointer itself here should not be changed; it is allocated and managed
-     * for you by the framework.
-     */
-    struct GameActivityCallbacks* callbacks;
-
-    /**
-     * The global handle on the process's Java VM.
-     */
-    JavaVM* vm;
-
-    /**
-     * JNI context for the main thread of the app.  Note that this field
-     * can ONLY be used from the main thread of the process; that is, the
-     * thread that calls into the GameActivityCallbacks.
-     */
-    JNIEnv* env;
-
-    /**
-     * The GameActivity object handle.
-     */
-    jobject javaGameActivity;
-
-    /**
-     * Path to this application's internal data directory.
-     */
-    const char* internalDataPath;
-
-    /**
-     * Path to this application's external (removable/mountable) data directory.
-     */
-    const char* externalDataPath;
-
-    /**
-     * The platform's SDK version code.
-     */
-    int32_t sdkVersion;
-
-    /**
-     * This is the native instance of the application.  It is not used by
-     * the framework, but can be set by the application to its own instance
-     * state.
-     */
-    void* instance;
-
-    /**
-     * Pointer to the Asset Manager instance for the application.  The
-     * application uses this to access binary assets bundled inside its own .apk
-     * file.
-     */
-    AAssetManager* assetManager;
-
-    /**
-     * Available starting with Honeycomb: path to the directory containing
-     * the application's OBB files (if any).  If the app doesn't have any
-     * OBB files, this directory may not exist.
-     */
-    const char* obbPath;
-} GameActivity;
-
-/**
- * The maximum number of axes supported in an Android MotionEvent.
- * See https://developer.android.com/ndk/reference/group/input.
- */
-#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48
-
-/**
- * \brief Describe information about a pointer, found in a
- * GameActivityMotionEvent.
- *
- * You can read values directly from this structure, or use helper functions
- * (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and
- * `GameActivityPointerAxes_getAxisValue`).
- *
- * The X axis and Y axis are enabled by default but any other axis that you want
- * to read **must** be enabled first, using
- * `GameActivityPointerAxes_enableAxis`.
- *
- * \see GameActivityMotionEvent
- */
-typedef struct GameActivityPointerAxes {
-    int32_t id;
-    float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
-    float rawX;
-    float rawY;
-} GameActivityPointerAxes;
-
-/** \brief Get the current X coordinate of the pointer. */
-inline float GameActivityPointerAxes_getX(
-    const GameActivityPointerAxes* pointerInfo) {
-    return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X];
-}
-
-/** \brief Get the current Y coordinate of the pointer. */
-inline float GameActivityPointerAxes_getY(
-    const GameActivityPointerAxes* pointerInfo) {
-    return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
-}
-
-/**
- * \brief Enable the specified axis, so that its value is reported in the
- * GameActivityPointerAxes structures stored in a motion event.
- *
- * You must enable any axis that you want to read, apart from
- * `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by
- * default.
- *
- * If the axis index is out of range, nothing is done.
- */
-void GameActivityPointerAxes_enableAxis(int32_t axis);
-
-/**
- * \brief Disable the specified axis. Its value won't be reported in the
- * GameActivityPointerAxes structures stored in a motion event anymore.
- *
- * Apart from X and Y, any axis that you want to read **must** be enabled first,
- * using `GameActivityPointerAxes_enableAxis`.
- *
- * If the axis index is out of range, nothing is done.
- */
-void GameActivityPointerAxes_disableAxis(int32_t axis);
-
-/**
- * \brief Get the value of the requested axis.
- *
- * Apart from X and Y, any axis that you want to read **must** be enabled first,
- * using `GameActivityPointerAxes_enableAxis`.
- *
- * Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`,
- * `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...)
- * in https://developer.android.com/ndk/reference/group/input.
- *
- * @param pointerInfo The structure containing information about the pointer,
- * obtained from GameActivityMotionEvent.
- * @param axis The axis to get the value from
- * @return The value of the axis, or 0 if the axis is invalid or was not
- * enabled.
- */
-inline float GameActivityPointerAxes_getAxisValue(
-    GameActivityPointerAxes* pointerInfo, int32_t axis) {
-    if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
-        return 0;
-    }
-
-    return pointerInfo->axisValues[axis];
-}
-
-/**
- * The maximum number of pointers returned inside a motion event.
- */
-#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE)
-#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \
-    GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
-#else
-#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
-#endif
-
-/**
- * \brief Describe a motion event that happened on the GameActivity SurfaceView.
- *
- * This is 1:1 mapping to the information contained in a Java `MotionEvent`
- * (see https://developer.android.com/reference/android/view/MotionEvent).
- */
-typedef struct GameActivityMotionEvent {
-    int32_t deviceId;
-    int32_t source;
-    int32_t action;
-
-    int64_t eventTime;
-    int64_t downTime;
-
-    int32_t flags;
-    int32_t metaState;
-
-    int32_t actionButton;
-    int32_t buttonState;
-    int32_t classification;
-    int32_t edgeFlags;
-
-    uint32_t pointerCount;
-    GameActivityPointerAxes
-        pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
-
-    float precisionX;
-    float precisionY;
-} GameActivityMotionEvent;
-
-/**
- * \brief Describe a key event that happened on the GameActivity SurfaceView.
- *
- * This is 1:1 mapping to the information contained in a Java `KeyEvent`
- * (see https://developer.android.com/reference/android/view/KeyEvent).
- */
-typedef struct GameActivityKeyEvent {
-    int32_t deviceId;
-    int32_t source;
-    int32_t action;
-
-    int64_t eventTime;
-    int64_t downTime;
-
-    int32_t flags;
-    int32_t metaState;
-
-    int32_t modifiers;
-    int32_t repeatCount;
-    int32_t keyCode;
-    int32_t unicodeChar;
-} GameActivityKeyEvent;
-
-/**
- * A function the user should call from their callback with the data, its length
- * and the library- supplied context.
- */
-typedef void (*SaveInstanceStateRecallback)(const char* bytes, int len,
-                                            void* context);
-
-/**
- * These are the callbacks the framework makes into a native application.
- * All of these callbacks happen on the main thread of the application.
- * By default, all callbacks are NULL; set to a pointer to your own function
- * to have it called.
- */
-typedef struct GameActivityCallbacks {
-    /**
-     * GameActivity has started.  See Java documentation for Activity.onStart()
-     * for more information.
-     */
-    void (*onStart)(GameActivity* activity);
-
-    /**
-     * GameActivity has resumed.  See Java documentation for Activity.onResume()
-     * for more information.
-     */
-    void (*onResume)(GameActivity* activity);
-
-    /**
-     * The framework is asking GameActivity to save its current instance state.
-     * See the Java documentation for Activity.onSaveInstanceState() for more
-     * information. The user should call the recallback with their data, its
-     * length and the provided context; they retain ownership of the data. Note
-     * that the saved state will be persisted, so it can not contain any active
-     * entities (pointers to memory, file descriptors, etc).
-     */
-    void (*onSaveInstanceState)(GameActivity* activity,
-                                SaveInstanceStateRecallback recallback,
-                                void* context);
-
-    /**
-     * GameActivity has paused.  See Java documentation for Activity.onPause()
-     * for more information.
-     */
-    void (*onPause)(GameActivity* activity);
-
-    /**
-     * GameActivity has stopped.  See Java documentation for Activity.onStop()
-     * for more information.
-     */
-    void (*onStop)(GameActivity* activity);
-
-    /**
-     * GameActivity is being destroyed.  See Java documentation for
-     * Activity.onDestroy() for more information.
-     */
-    void (*onDestroy)(GameActivity* activity);
-
-    /**
-     * Focus has changed in this GameActivity's window.  This is often used,
-     * for example, to pause a game when it loses input focus.
-     */
-    void (*onWindowFocusChanged)(GameActivity* activity, bool hasFocus);
-
-    /**
-     * The drawing window for this native activity has been created.  You
-     * can use the given native window object to start drawing.
-     */
-    void (*onNativeWindowCreated)(GameActivity* activity,
-                                  ANativeWindow* window);
-
-    /**
-     * The drawing window for this native activity has been resized.  You should
-     * retrieve the new size from the window and ensure that your rendering in
-     * it now matches.
-     */
-    void (*onNativeWindowResized)(GameActivity* activity, ANativeWindow* window,
-                                  int32_t newWidth, int32_t newHeight);
-
-    /**
-     * The drawing window for this native activity needs to be redrawn.  To
-     * avoid transient artifacts during screen changes (such resizing after
-     * rotation), applications should not return from this function until they
-     * have finished drawing their window in its current state.
-     */
-    void (*onNativeWindowRedrawNeeded)(GameActivity* activity,
-                                       ANativeWindow* window);
-
-    /**
-     * The drawing window for this native activity is going to be destroyed.
-     * You MUST ensure that you do not touch the window object after returning
-     * from this function: in the common case of drawing to the window from
-     * another thread, that means the implementation of this callback must
-     * properly synchronize with the other thread to stop its drawing before
-     * returning from here.
-     */
-    void (*onNativeWindowDestroyed)(GameActivity* activity,
-                                    ANativeWindow* window);
-
-    /**
-     * The current device AConfiguration has changed.  The new configuration can
-     * be retrieved from assetManager.
-     */
-    void (*onConfigurationChanged)(GameActivity* activity);
-
-    /**
-     * The system is running low on memory.  Use this callback to release
-     * resources you do not need, to help the system avoid killing more
-     * important processes.
-     */
-    void (*onTrimMemory)(GameActivity* activity, int level);
-
-    /**
-     * Callback called for every MotionEvent done on the GameActivity
-     * SurfaceView. Ownership of `event` is maintained by the library and it is
-     * only valid during the callback.
-     */
-    bool (*onTouchEvent)(GameActivity* activity,
-                         const GameActivityMotionEvent* event);
-
-    /**
-     * Callback called for every key down event on the GameActivity SurfaceView.
-     * Ownership of `event` is maintained by the library and it is only valid
-     * during the callback.
-     */
-    bool (*onKeyDown)(GameActivity* activity,
-                      const GameActivityKeyEvent* event);
-
-    /**
-     * Callback called for every key up event on the GameActivity SurfaceView.
-     * Ownership of `event` is maintained by the library and it is only valid
-     * during the callback.
-     */
-    bool (*onKeyUp)(GameActivity* activity, const GameActivityKeyEvent* event);
-
-    /**
-     * Callback called for every soft-keyboard text input event.
-     * Ownership of `state` is maintained by the library and it is only valid
-     * during the callback.
-     */
-    void (*onTextInputEvent)(GameActivity* activity,
-                             const GameTextInputState* state);
-
-    /**
-     * Callback called when WindowInsets of the main app window have changed.
-     * Call GameActivity_getWindowInsets to retrieve the insets themselves.
-     */
-    void (*onWindowInsetsChanged)(GameActivity* activity);
-} GameActivityCallbacks;
-
-/**
- * \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
- *
- * This is done automatically by the GameActivity: see `onTouchEvent` to set
- * a callback to consume the received events.
- * This function can be used if you re-implement events handling in your own
- * activity.
- * Ownership of out_event is maintained by the caller.
- */
-void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
-                                      GameActivityMotionEvent* out_event);
-
-/**
- * \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
- *
- * This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
- * to set a callback to consume the received events.
- * This function can be used if you re-implement events handling in your own
- * activity.
- * Ownership of out_event is maintained by the caller.
- */
-void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
-                                   GameActivityKeyEvent* out_event);
-
-/**
- * This is the function that must be in the native code to instantiate the
- * application's native activity.  It is called with the activity instance (see
- * above); if the code is being instantiated from a previously saved instance,
- * the savedState will be non-NULL and point to the saved data.  You must make
- * any copy of this data you need -- it will be released after you return from
- * this function.
- */
-typedef void GameActivity_createFunc(GameActivity* activity, void* savedState,
-                                     size_t savedStateSize);
-
-/**
- * The name of the function that NativeInstance looks for when launching its
- * native code.  This is the default function that is used, you can specify
- * "android.app.func_name" string meta-data in your manifest to use a different
- * function.
- */
-extern GameActivity_createFunc GameActivity_onCreate;
-
-/**
- * Finish the given activity.  Its finish() method will be called, causing it
- * to be stopped and destroyed.  Note that this method can be called from
- * *any* thread; it will send a message to the main thread of the process
- * where the Java finish call will take place.
- */
-void GameActivity_finish(GameActivity* activity);
-
-/**
- * Flags for GameActivity_setWindowFlags,
- * as per the Java API at android.view.WindowManager.LayoutParams.
- */
-enum GameActivitySetWindowFlags {
-    /**
-     * As long as this window is visible to the user, allow the lock
-     * screen to activate while the screen is on.  This can be used
-     * independently, or in combination with {@link
-     * GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} and/or {@link
-     * GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}
-     */
-    GAMEACTIVITY_FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001,
-    /** Everything behind this window will be dimmed. */
-    GAMEACTIVITY_FLAG_DIM_BEHIND = 0x00000002,
-    /**
-     * Blur everything behind this window.
-     * @deprecated Blurring is no longer supported.
-     */
-    GAMEACTIVITY_FLAG_BLUR_BEHIND = 0x00000004,
-    /**
-     * This window won't ever get key input focus, so the
-     * user can not send key or other button events to it.  Those will
-     * instead go to whatever focusable window is behind it.  This flag
-     * will also enable {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL} whether or not
-     * that is explicitly set.
-     *
-     * Setting this flag also implies that the window will not need to
-     * interact with
-     * a soft input method, so it will be Z-ordered and positioned
-     * independently of any active input method (typically this means it
-     * gets Z-ordered on top of the input method, so it can use the full
-     * screen for its content and cover the input method if needed.  You
-     * can use {@link GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM} to modify this
-     * behavior.
-     */
-    GAMEACTIVITY_FLAG_NOT_FOCUSABLE = 0x00000008,
-    /** This window can never receive touch events. */
-    GAMEACTIVITY_FLAG_NOT_TOUCHABLE = 0x00000010,
-    /**
-     * Even when this window is focusable (its
-     * {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set), allow any pointer
-     * events outside of the window to be sent to the windows behind it.
-     * Otherwise it will consume all pointer events itself, regardless of
-     * whether they are inside of the window.
-     */
-    GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL = 0x00000020,
-    /**
-     * When set, if the device is asleep when the touch
-     * screen is pressed, you will receive this first touch event.  Usually
-     * the first touch event is consumed by the system since the user can
-     * not see what they are pressing on.
-     *
-     * @deprecated This flag has no effect.
-     */
-    GAMEACTIVITY_FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040,
-    /**
-     * As long as this window is visible to the user, keep
-     * the device's screen turned on and bright.
-     */
-    GAMEACTIVITY_FLAG_KEEP_SCREEN_ON = 0x00000080,
-    /**
-     * Place the window within the entire screen, ignoring
-     * decorations around the border (such as the status bar).  The
-     * window must correctly position its contents to take the screen
-     * decoration into account.
-     */
-    GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN = 0x00000100,
-    /** Allows the window to extend outside of the screen. */
-    GAMEACTIVITY_FLAG_LAYOUT_NO_LIMITS = 0x00000200,
-    /**
-     * Hide all screen decorations (such as the status
-     * bar) while this window is displayed.  This allows the window to
-     * use the entire display space for itself -- the status bar will
-     * be hidden when an app window with this flag set is on the top
-     * layer. A fullscreen window will ignore a value of {@link
-     * GAMEACTIVITY_SOFT_INPUT_ADJUST_RESIZE}; the window will stay
-     * fullscreen and will not resize.
-     */
-    GAMEACTIVITY_FLAG_FULLSCREEN = 0x00000400,
-    /**
-     * Override {@link GAMEACTIVITY_FLAG_FULLSCREEN} and force the
-     * screen decorations (such as the status bar) to be shown.
-     */
-    GAMEACTIVITY_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800,
-    /**
-     * Turn on dithering when compositing this window to
-     * the screen.
-     * @deprecated This flag is no longer used.
-     */
-    GAMEACTIVITY_FLAG_DITHER = 0x00001000,
-    /**
-     * Treat the content of the window as secure, preventing
-     * it from appearing in screenshots or from being viewed on non-secure
-     * displays.
-     */
-    GAMEACTIVITY_FLAG_SECURE = 0x00002000,
-    /**
-     * A special mode where the layout parameters are used
-     * to perform scaling of the surface when it is composited to the
-     * screen.
-     */
-    GAMEACTIVITY_FLAG_SCALED = 0x00004000,
-    /**
-     * Intended for windows that will often be used when the user is
-     * holding the screen against their face, it will aggressively
-     * filter the event stream to prevent unintended presses in this
-     * situation that may not be desired for a particular window, when
-     * such an event stream is detected, the application will receive
-     * a {@link AMOTION_EVENT_ACTION_CANCEL} to indicate this so
-     * applications can handle this accordingly by taking no action on
-     * the event until the finger is released.
-     */
-    GAMEACTIVITY_FLAG_IGNORE_CHEEK_PRESSES = 0x00008000,
-    /**
-     * A special option only for use in combination with
-     * {@link GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN}.  When requesting layout in
-     * the screen your window may appear on top of or behind screen decorations
-     * such as the status bar.  By also including this flag, the window
-     * manager will report the inset rectangle needed to ensure your
-     * content is not covered by screen decorations.
-     */
-    GAMEACTIVITY_FLAG_LAYOUT_INSET_DECOR = 0x00010000,
-    /**
-     * Invert the state of {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} with
-     * respect to how this window interacts with the current method.
-     * That is, if FLAG_NOT_FOCUSABLE is set and this flag is set,
-     * then the window will behave as if it needs to interact with the
-     * input method and thus be placed behind/away from it; if {@link
-     * GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set and this flag is set,
-     * then the window will behave as if it doesn't need to interact
-     * with the input method and can be placed to use more space and
-     * cover the input method.
-     */
-    GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM = 0x00020000,
-    /**
-     * If you have set {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL}, you
-     * can set this flag to receive a single special MotionEvent with
-     * the action
-     * {@link AMOTION_EVENT_ACTION_OUTSIDE} for
-     * touches that occur outside of your window.  Note that you will not
-     * receive the full down/move/up gesture, only the location of the
-     * first down as an {@link AMOTION_EVENT_ACTION_OUTSIDE}.
-     */
-    GAMEACTIVITY_FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000,
-    /**
-     * Special flag to let windows be shown when the screen
-     * is locked. This will let application windows take precedence over
-     * key guard or any other lock screens. Can be used with
-     * {@link GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} to turn screen on and display
-     * windows directly before showing the key guard window.  Can be used with
-     * {@link GAMEACTIVITY_FLAG_DISMISS_KEYGUARD} to automatically fully
-     * dismisss non-secure keyguards.  This flag only applies to the top-most
-     * full-screen window.
-     */
-    GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED = 0x00080000,
-    /**
-     * Ask that the system wallpaper be shown behind
-     * your window.  The window surface must be translucent to be able
-     * to actually see the wallpaper behind it; this flag just ensures
-     * that the wallpaper surface will be there if this window actually
-     * has translucent regions.
-     */
-    GAMEACTIVITY_FLAG_SHOW_WALLPAPER = 0x00100000,
-    /**
-     * When set as a window is being added or made
-     * visible, once the window has been shown then the system will
-     * poke the power manager's user activity (as if the user had woken
-     * up the device) to turn the screen on.
-     */
-    GAMEACTIVITY_FLAG_TURN_SCREEN_ON = 0x00200000,
-    /**
-     * When set the window will cause the keyguard to
-     * be dismissed, only if it is not a secure lock keyguard.  Because such
-     * a keyguard is not needed for security, it will never re-appear if
-     * the user navigates to another window (in contrast to
-     * {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}, which will only temporarily
-     * hide both secure and non-secure keyguards but ensure they reappear
-     * when the user moves to another UI that doesn't hide them).
-     * If the keyguard is currently active and is secure (requires an
-     * unlock pattern) than the user will still need to confirm it before
-     * seeing this window, unless {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} has
-     * also been set.
-     */
-    GAMEACTIVITY_FLAG_DISMISS_KEYGUARD = 0x00400000,
-};
-
-/**
- * Change the window flags of the given activity.  Calls getWindow().setFlags()
- * of the given activity.
- * Note that some flags must be set before the window decoration is created,
- * see
- * https://developer.android.com/reference/android/view/Window#setFlags(int,%20int).
- * Note also that this method can be called from
- * *any* thread; it will send a message to the main thread of the process
- * where the Java finish call will take place.
- */
-void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags,
-                                 uint32_t removeFlags);
-
-/**
- * Flags for GameActivity_showSoftInput; see the Java InputMethodManager
- * API for documentation.
- */
-enum GameActivityShowSoftInputFlags {
-    /**
-     * Implicit request to show the input window, not as the result
-     * of a direct request by the user.
-     */
-    GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT = 0x0001,
-
-    /**
-     * The user has forced the input method open (such as by
-     * long-pressing menu) so it should not be closed until they
-     * explicitly do so.
-     */
-    GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED = 0x0002,
-};
-
-/**
- * Show the IME while in the given activity.  Calls
- * InputMethodManager.showSoftInput() for the given activity.  Note that this
- * method can be called from *any* thread; it will send a message to the main
- * thread of the process where the Java call will take place.
- */
-void GameActivity_showSoftInput(GameActivity* activity, uint32_t flags);
-
-/**
- * Set the text entry state (see documentation of the GameTextInputState struct
- * in the Game Text Input library reference).
- *
- * Ownership of the state is maintained by the caller.
- */
-void GameActivity_setTextInputState(GameActivity* activity,
-                                    const GameTextInputState* state);
-
-/**
- * Get the last-received text entry state (see documentation of the
- * GameTextInputState struct in the Game Text Input library reference).
- *
- */
-void GameActivity_getTextInputState(GameActivity* activity,
-                                    GameTextInputGetStateCallback callback,
-                                    void* context);
-
-/**
- * Get a pointer to the GameTextInput library instance.
- */
-GameTextInput* GameActivity_getTextInput(const GameActivity* activity);
-
-/**
- * Flags for GameActivity_hideSoftInput; see the Java InputMethodManager
- * API for documentation.
- */
-enum GameActivityHideSoftInputFlags {
-    /**
-     * The soft input window should only be hidden if it was not
-     * explicitly shown by the user.
-     */
-    GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY = 0x0001,
-    /**
-     * The soft input window should normally be hidden, unless it was
-     * originally shown with {@link GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED}.
-     */
-    GAMEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS = 0x0002,
-};
-
-/**
- * Hide the IME while in the given activity.  Calls
- * InputMethodManager.hideSoftInput() for the given activity.  Note that this
- * method can be called from *any* thread; it will send a message to the main
- * thread of the process where the Java finish call will take place.
- */
-void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags);
-
-/**
- * Get the current window insets of the particular component. See
- * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
- * for more details.
- * You can use these insets to influence what you show on the screen.
- */
-void GameActivity_getWindowInsets(GameActivity* activity,
-                                  GameCommonInsetsType type, ARect* insets);
-
-/**
- * Set options on how the IME behaves when it is requested for text input.
- * See
- * https://developer.android.com/reference/android/view/inputmethod/EditorInfo
- * for the meaning of inputType, actionId and imeOptions.
- *
- * Note that this function will attach the current thread to the JVM if it is
- * not already attached, so the caller must detach the thread from the JVM
- * before the thread is destroyed using DetachCurrentThread.
- */
-void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
-                                   int actionId, int imeOptions);
-
-#ifdef __cplusplus
-}
-#endif
-
-/** @} */
-
-#endif  // ANDROID_GAME_SDK_GAME_ACTIVITY_H
diff --git a/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.c b/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.c
deleted file mode 100644
index 282aa18..0000000
--- a/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.c
+++ /dev/null
@@ -1,591 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "android_native_app_glue.h"
-
-#include <android/log.h>
-#include <errno.h>
-#include <jni.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include <unistd.h>
-
-#define LOGI(...) \
-    ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
-#define LOGE(...) \
-    ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__))
-#define LOGW(...) \
-    ((void)__android_log_print(ANDROID_LOG_WARN, "threaded_app", __VA_ARGS__))
-#define LOGW_ONCE(...)                                        \
-    do {                                                       \
-        static bool alogw_once##__FILE__##__LINE__##__ = true; \
-        if (alogw_once##__FILE__##__LINE__##__) {              \
-            alogw_once##__FILE__##__LINE__##__ = false;        \
-            LOGW(__VA_ARGS__);                                \
-        }                                                      \
-    } while (0)
-
-/* For debug builds, always enable the debug traces in this library */
-#ifndef NDEBUG
-#define LOGV(...)                                                   \
-    ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", \
-                               __VA_ARGS__))
-#else
-#define LOGV(...) ((void)0)
-#endif
-
-static void free_saved_state(struct android_app* android_app) {
-    pthread_mutex_lock(&android_app->mutex);
-    if (android_app->savedState != NULL) {
-        free(android_app->savedState);
-        android_app->savedState = NULL;
-        android_app->savedStateSize = 0;
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-}
-
-int8_t android_app_read_cmd(struct android_app* android_app) {
-    int8_t cmd;
-    if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) {
-        LOGE("No data on command pipe!");
-        return -1;
-    }
-    if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app);
-    return cmd;
-}
-
-static void print_cur_config(struct android_app* android_app) {
-    char lang[2], country[2];
-    AConfiguration_getLanguage(android_app->config, lang);
-    AConfiguration_getCountry(android_app->config, country);
-
-    LOGV(
-        "Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
-        "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
-        "modetype=%d modenight=%d",
-        AConfiguration_getMcc(android_app->config),
-        AConfiguration_getMnc(android_app->config), lang[0], lang[1],
-        country[0], country[1],
-        AConfiguration_getOrientation(android_app->config),
-        AConfiguration_getTouchscreen(android_app->config),
-        AConfiguration_getDensity(android_app->config),
-        AConfiguration_getKeyboard(android_app->config),
-        AConfiguration_getNavigation(android_app->config),
-        AConfiguration_getKeysHidden(android_app->config),
-        AConfiguration_getNavHidden(android_app->config),
-        AConfiguration_getSdkVersion(android_app->config),
-        AConfiguration_getScreenSize(android_app->config),
-        AConfiguration_getScreenLong(android_app->config),
-        AConfiguration_getUiModeType(android_app->config),
-        AConfiguration_getUiModeNight(android_app->config));
-}
-
-void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
-    switch (cmd) {
-        case UNUSED_APP_CMD_INPUT_CHANGED:
-            LOGV("UNUSED_APP_CMD_INPUT_CHANGED");
-            // Do nothing. This can be used in the future to handle AInputQueue
-            // natively, like done in NativeActivity.
-            break;
-
-        case APP_CMD_INIT_WINDOW:
-            LOGV("APP_CMD_INIT_WINDOW");
-            pthread_mutex_lock(&android_app->mutex);
-            android_app->window = android_app->pendingWindow;
-            pthread_cond_broadcast(&android_app->cond);
-            pthread_mutex_unlock(&android_app->mutex);
-            break;
-
-        case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW");
-            pthread_cond_broadcast(&android_app->cond);
-            break;
-
-        case APP_CMD_RESUME:
-        case APP_CMD_START:
-        case APP_CMD_PAUSE:
-        case APP_CMD_STOP:
-            LOGV("activityState=%d", cmd);
-            pthread_mutex_lock(&android_app->mutex);
-            android_app->activityState = cmd;
-            pthread_cond_broadcast(&android_app->cond);
-            pthread_mutex_unlock(&android_app->mutex);
-            break;
-
-        case APP_CMD_CONFIG_CHANGED:
-            LOGV("APP_CMD_CONFIG_CHANGED");
-            AConfiguration_fromAssetManager(
-                android_app->config, android_app->activity->assetManager);
-            print_cur_config(android_app);
-            break;
-
-        case APP_CMD_DESTROY:
-            LOGV("APP_CMD_DESTROY");
-            android_app->destroyRequested = 1;
-            break;
-    }
-}
-
-void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
-    switch (cmd) {
-        case APP_CMD_TERM_WINDOW:
-            LOGV("APP_CMD_TERM_WINDOW");
-            pthread_mutex_lock(&android_app->mutex);
-            android_app->window = NULL;
-            pthread_cond_broadcast(&android_app->cond);
-            pthread_mutex_unlock(&android_app->mutex);
-            break;
-
-        case APP_CMD_SAVE_STATE:
-            LOGV("APP_CMD_SAVE_STATE");
-            pthread_mutex_lock(&android_app->mutex);
-            android_app->stateSaved = 1;
-            pthread_cond_broadcast(&android_app->cond);
-            pthread_mutex_unlock(&android_app->mutex);
-            break;
-
-        case APP_CMD_RESUME:
-            free_saved_state(android_app);
-            break;
-    }
-}
-
-void app_dummy() {}
-
-static void android_app_destroy(struct android_app* android_app) {
-    LOGV("android_app_destroy!");
-    free_saved_state(android_app);
-    pthread_mutex_lock(&android_app->mutex);
-
-    AConfiguration_delete(android_app->config);
-    android_app->destroyed = 1;
-    pthread_cond_broadcast(&android_app->cond);
-    pthread_mutex_unlock(&android_app->mutex);
-    // Can't touch android_app object after this.
-}
-
-static void process_cmd(struct android_app* app,
-                        struct android_poll_source* source) {
-    int8_t cmd = android_app_read_cmd(app);
-    android_app_pre_exec_cmd(app, cmd);
-    if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
-    android_app_post_exec_cmd(app, cmd);
-}
-
-// This is run on a separate thread (i.e: not the main thread).
-static void* android_app_entry(void* param) {
-    struct android_app* android_app = (struct android_app*)param;
-
-    LOGV("android_app_entry called");
-    android_app->config = AConfiguration_new();
-    LOGV("android_app = %p", android_app);
-    LOGV("config = %p", android_app->config);
-    LOGV("activity = %p", android_app->activity);
-    LOGV("assetmanager = %p", android_app->activity->assetManager);
-    AConfiguration_fromAssetManager(android_app->config,
-                                    android_app->activity->assetManager);
-
-    print_cur_config(android_app);
-
-    android_app->cmdPollSource.id = LOOPER_ID_MAIN;
-    android_app->cmdPollSource.app = android_app;
-    android_app->cmdPollSource.process = process_cmd;
-
-    ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
-    ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN,
-                  ALOOPER_EVENT_INPUT, NULL, &android_app->cmdPollSource);
-    android_app->looper = looper;
-
-    pthread_mutex_lock(&android_app->mutex);
-    android_app->running = 1;
-    pthread_cond_broadcast(&android_app->cond);
-    pthread_mutex_unlock(&android_app->mutex);
-
-    android_main(android_app);
-
-    android_app_destroy(android_app);
-    return NULL;
-}
-
-// Codes from https://developer.android.com/reference/android/view/KeyEvent
-#define KEY_EVENT_KEYCODE_VOLUME_DOWN 25
-#define KEY_EVENT_KEYCODE_VOLUME_MUTE 164
-#define KEY_EVENT_KEYCODE_VOLUME_UP 24
-#define KEY_EVENT_KEYCODE_CAMERA 27
-#define KEY_EVENT_KEYCODE_ZOOM_IN 168
-#define KEY_EVENT_KEYCODE_ZOOM_OUT 169
-
-// Double-buffer the key event filter to avoid race condition.
-static bool default_key_filter(const GameActivityKeyEvent* event) {
-    // Ignore camera, volume, etc. buttons
-    return !(event->keyCode == KEY_EVENT_KEYCODE_VOLUME_DOWN ||
-             event->keyCode == KEY_EVENT_KEYCODE_VOLUME_MUTE ||
-             event->keyCode == KEY_EVENT_KEYCODE_VOLUME_UP ||
-             event->keyCode == KEY_EVENT_KEYCODE_CAMERA ||
-             event->keyCode == KEY_EVENT_KEYCODE_ZOOM_IN ||
-             event->keyCode == KEY_EVENT_KEYCODE_ZOOM_OUT);
-}
-
-// See
-// https://developer.android.com/reference/android/view/InputDevice#SOURCE_TOUCHSCREEN
-#define SOURCE_TOUCHSCREEN 0x00001002
-
-static bool default_motion_filter(const GameActivityMotionEvent* event) {
-    // Ignore any non-touch events.
-    return event->source == SOURCE_TOUCHSCREEN;
-}
-
-// --------------------------------------------------------------------
-// Native activity interaction (called from main thread)
-// --------------------------------------------------------------------
-
-static struct android_app* android_app_create(GameActivity* activity,
-                                              void* savedState,
-                                              size_t savedStateSize) {
-    //  struct android_app* android_app = calloc(1, sizeof(struct android_app));
-    struct android_app* android_app =
-        (struct android_app*)malloc(sizeof(struct android_app));
-    memset(android_app, 0, sizeof(struct android_app));
-    android_app->activity = activity;
-
-    pthread_mutex_init(&android_app->mutex, NULL);
-    pthread_cond_init(&android_app->cond, NULL);
-
-    if (savedState != NULL) {
-        android_app->savedState = malloc(savedStateSize);
-        android_app->savedStateSize = savedStateSize;
-        memcpy(android_app->savedState, savedState, savedStateSize);
-    }
-
-    int msgpipe[2];
-    if (pipe(msgpipe)) {
-        LOGE("could not create pipe: %s", strerror(errno));
-        return NULL;
-    }
-    android_app->msgread = msgpipe[0];
-    android_app->msgwrite = msgpipe[1];
-
-    android_app->keyEventFilter = default_key_filter;
-    android_app->motionEventFilter = default_motion_filter;
-
-    LOGV("Launching android_app_entry in a thread");
-    pthread_attr_t attr;
-    pthread_attr_init(&attr);
-    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
-    pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
-
-    // Wait for thread to start.
-    pthread_mutex_lock(&android_app->mutex);
-    while (!android_app->running) {
-        pthread_cond_wait(&android_app->cond, &android_app->mutex);
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-
-    return android_app;
-}
-
-static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
-    if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
-        LOGE("Failure writing android_app cmd: %s", strerror(errno));
-    }
-}
-
-static void android_app_set_window(struct android_app* android_app,
-                                   ANativeWindow* window) {
-    LOGV("android_app_set_window called");
-    pthread_mutex_lock(&android_app->mutex);
-    if (android_app->pendingWindow != NULL) {
-        android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
-    }
-    android_app->pendingWindow = window;
-    if (window != NULL) {
-        android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
-    }
-    while (android_app->window != android_app->pendingWindow) {
-        pthread_cond_wait(&android_app->cond, &android_app->mutex);
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-}
-
-static void android_app_set_activity_state(struct android_app* android_app,
-                                           int8_t cmd) {
-    pthread_mutex_lock(&android_app->mutex);
-    android_app_write_cmd(android_app, cmd);
-    while (android_app->activityState != cmd) {
-        pthread_cond_wait(&android_app->cond, &android_app->mutex);
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-}
-
-static void android_app_free(struct android_app* android_app) {
-    pthread_mutex_lock(&android_app->mutex);
-    android_app_write_cmd(android_app, APP_CMD_DESTROY);
-    while (!android_app->destroyed) {
-        pthread_cond_wait(&android_app->cond, &android_app->mutex);
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-
-    close(android_app->msgread);
-    close(android_app->msgwrite);
-    pthread_cond_destroy(&android_app->cond);
-    pthread_mutex_destroy(&android_app->mutex);
-    free(android_app);
-}
-
-static inline struct android_app* ToApp(GameActivity* activity) {
-    return (struct android_app*)activity->instance;
-}
-
-static void onDestroy(GameActivity* activity) {
-    LOGV("Destroy: %p", activity);
-    android_app_free(ToApp(activity));
-}
-
-static void onStart(GameActivity* activity) {
-    LOGV("Start: %p", activity);
-    android_app_set_activity_state(ToApp(activity), APP_CMD_START);
-}
-
-static void onResume(GameActivity* activity) {
-    LOGV("Resume: %p", activity);
-    android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
-}
-
-static void onSaveInstanceState(GameActivity* activity,
-                                SaveInstanceStateRecallback recallback,
-                                void* context) {
-    LOGV("SaveInstanceState: %p", activity);
-
-    struct android_app* android_app = ToApp(activity);
-    void* savedState = NULL;
-    pthread_mutex_lock(&android_app->mutex);
-    android_app->stateSaved = 0;
-    android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
-    while (!android_app->stateSaved) {
-        pthread_cond_wait(&android_app->cond, &android_app->mutex);
-    }
-
-    if (android_app->savedState != NULL) {
-        // Tell the Java side about our state.
-        recallback((const char*)android_app->savedState,
-                   android_app->savedStateSize, context);
-        // Now we can free it.
-        free(android_app->savedState);
-        android_app->savedState = NULL;
-        android_app->savedStateSize = 0;
-    }
-
-    pthread_mutex_unlock(&android_app->mutex);
-}
-
-static void onPause(GameActivity* activity) {
-    LOGV("Pause: %p", activity);
-    android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
-}
-
-static void onStop(GameActivity* activity) {
-    LOGV("Stop: %p", activity);
-    android_app_set_activity_state(ToApp(activity), APP_CMD_STOP);
-}
-
-static void onConfigurationChanged(GameActivity* activity) {
-    LOGV("ConfigurationChanged: %p", activity);
-    android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED);
-}
-
-static void onTrimMemory(GameActivity* activity, int level) {
-    LOGV("TrimMemory: %p %d", activity, level);
-    android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY);
-}
-
-static void onWindowFocusChanged(GameActivity* activity, bool focused) {
-    LOGV("WindowFocusChanged: %p -- %d", activity, focused);
-    android_app_write_cmd(ToApp(activity),
-                          focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
-}
-
-static void onNativeWindowCreated(GameActivity* activity,
-                                  ANativeWindow* window) {
-    LOGV("NativeWindowCreated: %p -- %p", activity, window);
-    android_app_set_window(ToApp(activity), window);
-}
-
-static void onNativeWindowDestroyed(GameActivity* activity,
-                                    ANativeWindow* window) {
-    LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
-    android_app_set_window(ToApp(activity), NULL);
-}
-
-static void onNativeWindowRedrawNeeded(GameActivity* activity,
-                                       ANativeWindow* window) {
-    LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window);
-    android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED);
-}
-
-static void onNativeWindowResized(GameActivity* activity, ANativeWindow* window,
-                                  int32_t width, int32_t height) {
-    LOGV("NativeWindowResized: %p -- %p ( %d x %d )", activity, window, width,
-         height);
-    android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
-}
-
-void android_app_set_motion_event_filter(struct android_app* app,
-                                         android_motion_event_filter filter) {
-    pthread_mutex_lock(&app->mutex);
-    app->motionEventFilter = filter;
-    pthread_mutex_unlock(&app->mutex);
-}
-
-static bool onTouchEvent(GameActivity* activity,
-                         const GameActivityMotionEvent* event) {
-    struct android_app* android_app = ToApp(activity);
-    pthread_mutex_lock(&android_app->mutex);
-
-    if (android_app->motionEventFilter != NULL &&
-        !android_app->motionEventFilter(event)) {
-        pthread_mutex_unlock(&android_app->mutex);
-        return false;
-    }
-
-    struct android_input_buffer* inputBuffer =
-        &android_app->inputBuffers[android_app->currentInputBuffer];
-
-    // Add to the list of active motion events
-    if (inputBuffer->motionEventsCount <
-        NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS) {
-        int new_ix = inputBuffer->motionEventsCount;
-        memcpy(&inputBuffer->motionEvents[new_ix], event,
-               sizeof(GameActivityMotionEvent));
-        ++inputBuffer->motionEventsCount;
-    } else {
-        LOGW_ONCE("Motion event will be dropped because the number of unconsumed motion"
-             " events exceeded NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS (%d). Consider setting"
-             " NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE to a larger value",
-             NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS);
-    }
-    pthread_mutex_unlock(&android_app->mutex);
-    return true;
-}
-
-struct android_input_buffer* android_app_swap_input_buffers(
-    struct android_app* android_app) {
-    pthread_mutex_lock(&android_app->mutex);
-
-    struct android_input_buffer* inputBuffer =
-        &android_app->inputBuffers[android_app->currentInputBuffer];
-
-    if (inputBuffer->motionEventsCount == 0 &&
-        inputBuffer->keyEventsCount == 0) {
-        inputBuffer = NULL;
-    } else {
-        android_app->currentInputBuffer =
-            (android_app->currentInputBuffer + 1) %
-            NATIVE_APP_GLUE_MAX_INPUT_BUFFERS;
-    }
-
-    pthread_mutex_unlock(&android_app->mutex);
-
-    return inputBuffer;
-}
-
-void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
-    inputBuffer->motionEventsCount = 0;
-}
-
-void android_app_set_key_event_filter(struct android_app* app,
-                                      android_key_event_filter filter) {
-    pthread_mutex_lock(&app->mutex);
-    app->keyEventFilter = filter;
-    pthread_mutex_unlock(&app->mutex);
-}
-
-static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
-    struct android_app* android_app = ToApp(activity);
-    pthread_mutex_lock(&android_app->mutex);
-
-    if (android_app->keyEventFilter != NULL &&
-        !android_app->keyEventFilter(event)) {
-        pthread_mutex_unlock(&android_app->mutex);
-        return false;
-    }
-
-    struct android_input_buffer* inputBuffer =
-        &android_app->inputBuffers[android_app->currentInputBuffer];
-
-    // Add to the list of active key down events
-    if (inputBuffer->keyEventsCount < NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS) {
-        int new_ix = inputBuffer->keyEventsCount;
-        memcpy(&inputBuffer->keyEvents[new_ix], event,
-               sizeof(GameActivityKeyEvent));
-        ++inputBuffer->keyEventsCount;
-    } else {
-        LOGW_ONCE("Key event will be dropped because the number of unconsumed key events exceeded"
-             " NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS (%d). Consider setting"
-             " NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE to a larger value",
-             NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS);
-    }
-
-    pthread_mutex_unlock(&android_app->mutex);
-    return true;
-}
-
-void android_app_clear_key_events(struct android_input_buffer* inputBuffer) {
-    inputBuffer->keyEventsCount = 0;
-}
-
-static void onTextInputEvent(GameActivity* activity,
-                             const GameTextInputState* state) {
-    struct android_app* android_app = ToApp(activity);
-    pthread_mutex_lock(&android_app->mutex);
-
-    android_app->textInputState = 1;
-    pthread_mutex_unlock(&android_app->mutex);
-}
-
-static void onWindowInsetsChanged(GameActivity* activity) {
-    LOGV("WindowInsetsChanged: %p", activity);
-    android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED);
-}
-
-JNIEXPORT
-void GameActivity_onCreate(GameActivity* activity, void* savedState,
-                           size_t savedStateSize) {
-    LOGV("Creating: %p", activity);
-    activity->callbacks->onDestroy = onDestroy;
-    activity->callbacks->onStart = onStart;
-    activity->callbacks->onResume = onResume;
-    activity->callbacks->onSaveInstanceState = onSaveInstanceState;
-    activity->callbacks->onPause = onPause;
-    activity->callbacks->onStop = onStop;
-    activity->callbacks->onTouchEvent = onTouchEvent;
-    activity->callbacks->onKeyDown = onKey;
-    activity->callbacks->onKeyUp = onKey;
-    activity->callbacks->onTextInputEvent = onTextInputEvent;
-    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
-    activity->callbacks->onTrimMemory = onTrimMemory;
-    activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
-    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
-    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
-    activity->callbacks->onNativeWindowRedrawNeeded =
-        onNativeWindowRedrawNeeded;
-    activity->callbacks->onNativeWindowResized = onNativeWindowResized;
-    activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged;
-    LOGV("Callbacks set: %p", activity->callbacks);
-
-    activity->instance =
-        android_app_create(activity, savedState, savedStateSize);
-}
diff --git a/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.h b/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.h
deleted file mode 100644
index 9dfe3a5..0000000
--- a/third_party/android_game_activity/include/game-activity/native_app_glue/android_native_app_glue.h
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-/**
- * @addtogroup android_native_app_glue Native App Glue library
- * The glue library to interface your game loop with GameActivity.
- * @{
- */
-
-#include <android/configuration.h>
-#include <android/looper.h>
-#include <poll.h>
-#include <pthread.h>
-#include <sched.h>
-
-#include "game-activity/GameActivity.h"
-
-#if (defined NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE)
-#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS \
-    NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE
-#else
-#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS 16
-#endif
-
-#if (defined NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE)
-#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS \
-    NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE
-#else
-#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS 4
-#endif
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * The GameActivity interface provided by <game-activity/GameActivity.h>
- * is based on a set of application-provided callbacks that will be called
- * by the Activity's main thread when certain events occur.
- *
- * This means that each one of this callbacks _should_ _not_ block, or they
- * risk having the system force-close the application. This programming
- * model is direct, lightweight, but constraining.
- *
- * The 'android_native_app_glue' static library is used to provide a different
- * execution model where the application can implement its own main event
- * loop in a different thread instead. Here's how it works:
- *
- * 1/ The application must provide a function named "android_main()" that
- *    will be called when the activity is created, in a new thread that is
- *    distinct from the activity's main thread.
- *
- * 2/ android_main() receives a pointer to a valid "android_app" structure
- *    that contains references to other important objects, e.g. the
- *    GameActivity obejct instance the application is running in.
- *
- * 3/ the "android_app" object holds an ALooper instance that already
- *    listens to activity lifecycle events (e.g. "pause", "resume").
- *    See APP_CMD_XXX declarations below.
- *
- *    This corresponds to an ALooper identifier returned by
- *    ALooper_pollOnce with value LOOPER_ID_MAIN.
- *
- *    Your application can use the same ALooper to listen to additional
- *    file-descriptors.  They can either be callback based, or with return
- *    identifiers starting with LOOPER_ID_USER.
- *
- * 4/ Whenever you receive a LOOPER_ID_MAIN event,
- *    the returned data will point to an android_poll_source structure.  You
- *    can call the process() function on it, and fill in android_app->onAppCmd
- *    to be called for your own processing of the event.
- *
- *    Alternatively, you can call the low-level functions to read and process
- *    the data directly...  look at the process_cmd() and process_input()
- *    implementations in the glue to see how to do this.
- *
- * See the sample named "native-activity" that comes with the NDK with a
- * full usage example.  Also look at the documentation of GameActivity.
- */
-
-struct android_app;
-
-/**
- * Data associated with an ALooper fd that will be returned as the "outData"
- * when that source has data ready.
- */
-struct android_poll_source {
-    /**
-     * The identifier of this source.  May be LOOPER_ID_MAIN or
-     * LOOPER_ID_INPUT.
-     */
-    int32_t id;
-
-    /** The android_app this ident is associated with. */
-    struct android_app* app;
-
-    /**
-     * Function to call to perform the standard processing of data from
-     * this source.
-     */
-    void (*process)(struct android_app* app,
-                    struct android_poll_source* source);
-};
-
-struct android_input_buffer {
-    /**
-     * Pointer to a read-only array of pointers to GameActivityMotionEvent.
-     * Only the first motionEventsCount events are valid.
-     */
-    GameActivityMotionEvent motionEvents[NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS];
-
-    /**
-     * The number of valid motion events in `motionEvents`.
-     */
-    uint64_t motionEventsCount;
-
-    /**
-     * Pointer to a read-only array of pointers to GameActivityKeyEvent.
-     * Only the first keyEventsCount events are valid.
-     */
-    GameActivityKeyEvent keyEvents[NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS];
-
-    /**
-     * The number of valid "Key" events in `keyEvents`.
-     */
-    uint64_t keyEventsCount;
-};
-
-/**
- * Function pointer declaration for the filtering of key events.
- * A function with this signature should be passed to
- * android_app_set_key_event_filter and return false for any events that should
- * not be handled by android_native_app_glue. These events will be handled by
- * the system instead.
- */
-typedef bool (*android_key_event_filter)(const GameActivityKeyEvent*);
-
-/**
- * Function pointer definition for the filtering of motion events.
- * A function with this signature should be passed to
- * android_app_set_motion_event_filter and return false for any events that
- * should not be handled by android_native_app_glue. These events will be
- * handled by the system instead.
- */
-typedef bool (*android_motion_event_filter)(const GameActivityMotionEvent*);
-
-/**
- * This is the interface for the standard glue code of a threaded
- * application.  In this model, the application's code is running
- * in its own thread separate from the main thread of the process.
- * It is not required that this thread be associated with the Java
- * VM, although it will need to be in order to make JNI calls any
- * Java objects.
- */
-struct android_app {
-    /**
-     * An optional pointer to application-defined state.
-     */
-    void* userData;
-
-    /**
-     * A required callback for processing main app commands (`APP_CMD_*`).
-     * This is called each frame if there are app commands that need processing.
-     */
-    void (*onAppCmd)(struct android_app* app, int32_t cmd);
-
-    /** The GameActivity object instance that this app is running in. */
-    GameActivity* activity;
-
-    /** The current configuration the app is running in. */
-    AConfiguration* config;
-
-    /**
-     * The last activity saved state, as provided at creation time.
-     * It is NULL if there was no state.  You can use this as you need; the
-     * memory will remain around until you call android_app_exec_cmd() for
-     * APP_CMD_RESUME, at which point it will be freed and savedState set to
-     * NULL. These variables should only be changed when processing a
-     * APP_CMD_SAVE_STATE, at which point they will be initialized to NULL and
-     * you can malloc your state and place the information here.  In that case
-     * the memory will be freed for you later.
-     */
-    void* savedState;
-
-    /**
-     * The size of the activity saved state. It is 0 if `savedState` is NULL.
-     */
-    size_t savedStateSize;
-
-    /** The ALooper associated with the app's thread. */
-    ALooper* looper;
-
-    /** When non-NULL, this is the window surface that the app can draw in. */
-    ANativeWindow* window;
-
-    /**
-     * Current content rectangle of the window; this is the area where the
-     * window's content should be placed to be seen by the user.
-     */
-    ARect contentRect;
-
-    /**
-     * Current state of the app's activity.  May be either APP_CMD_START,
-     * APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP.
-     */
-    int activityState;
-
-    /**
-     * This is non-zero when the application's GameActivity is being
-     * destroyed and waiting for the app thread to complete.
-     */
-    int destroyRequested;
-
-#define NATIVE_APP_GLUE_MAX_INPUT_BUFFERS 2
-
-    /**
-     * This is used for buffering input from GameActivity. Once ready, the
-     * application thread switches the buffers and processes what was
-     * accumulated.
-     */
-    struct android_input_buffer inputBuffers[NATIVE_APP_GLUE_MAX_INPUT_BUFFERS];
-
-    int currentInputBuffer;
-
-    /**
-     * 0 if no text input event is outstanding, 1 if it is.
-     * Use `GameActivity_getTextInputState` to get information
-     * about the text entered by the user.
-     */
-    int textInputState;
-
-    // Below are "private" implementation of the glue code.
-    /** @cond INTERNAL */
-
-    pthread_mutex_t mutex;
-    pthread_cond_t cond;
-
-    int msgread;
-    int msgwrite;
-
-    pthread_t thread;
-
-    struct android_poll_source cmdPollSource;
-
-    int running;
-    int stateSaved;
-    int destroyed;
-    int redrawNeeded;
-    ANativeWindow* pendingWindow;
-    ARect pendingContentRect;
-
-    android_key_event_filter keyEventFilter;
-    android_motion_event_filter motionEventFilter;
-
-    /** @endcond */
-};
-
-/**
- * Looper ID of commands coming from the app's main thread, an AInputQueue or
- * user-defined sources.
- */
-enum NativeAppGlueLooperId {
-    /**
-     * Looper data ID of commands coming from the app's main thread, which
-     * is returned as an identifier from ALooper_pollOnce().  The data for this
-     * identifier is a pointer to an android_poll_source structure.
-     * These can be retrieved and processed with android_app_read_cmd()
-     * and android_app_exec_cmd().
-     */
-    LOOPER_ID_MAIN = 1,
-
-    /**
-     * Unused. Reserved for future use when usage of AInputQueue will be
-     * supported.
-     */
-    LOOPER_ID_INPUT = 2,
-
-    /**
-     * Start of user-defined ALooper identifiers.
-     */
-    LOOPER_ID_USER = 3,
-};
-
-/**
- * Commands passed from the application's main Java thread to the game's thread.
- */
-enum NativeAppGlueAppCmd {
-    /**
-     * Unused. Reserved for future use when usage of AInputQueue will be
-     * supported.
-     */
-    UNUSED_APP_CMD_INPUT_CHANGED,
-
-    /**
-     * Command from main thread: a new ANativeWindow is ready for use.  Upon
-     * receiving this command, android_app->window will contain the new window
-     * surface.
-     */
-    APP_CMD_INIT_WINDOW,
-
-    /**
-     * Command from main thread: the existing ANativeWindow needs to be
-     * terminated.  Upon receiving this command, android_app->window still
-     * contains the existing window; after calling android_app_exec_cmd
-     * it will be set to NULL.
-     */
-    APP_CMD_TERM_WINDOW,
-
-    /**
-     * Command from main thread: the current ANativeWindow has been resized.
-     * Please redraw with its new size.
-     */
-    APP_CMD_WINDOW_RESIZED,
-
-    /**
-     * Command from main thread: the system needs that the current ANativeWindow
-     * be redrawn.  You should redraw the window before handing this to
-     * android_app_exec_cmd() in order to avoid transient drawing glitches.
-     */
-    APP_CMD_WINDOW_REDRAW_NEEDED,
-
-    /**
-     * Command from main thread: the content area of the window has changed,
-     * such as from the soft input window being shown or hidden.  You can
-     * find the new content rect in android_app::contentRect.
-     */
-    APP_CMD_CONTENT_RECT_CHANGED,
-
-    /**
-     * Command from main thread: the app's activity window has gained
-     * input focus.
-     */
-    APP_CMD_GAINED_FOCUS,
-
-    /**
-     * Command from main thread: the app's activity window has lost
-     * input focus.
-     */
-    APP_CMD_LOST_FOCUS,
-
-    /**
-     * Command from main thread: the current device configuration has changed.
-     */
-    APP_CMD_CONFIG_CHANGED,
-
-    /**
-     * Command from main thread: the system is running low on memory.
-     * Try to reduce your memory use.
-     */
-    APP_CMD_LOW_MEMORY,
-
-    /**
-     * Command from main thread: the app's activity has been started.
-     */
-    APP_CMD_START,
-
-    /**
-     * Command from main thread: the app's activity has been resumed.
-     */
-    APP_CMD_RESUME,
-
-    /**
-     * Command from main thread: the app should generate a new saved state
-     * for itself, to restore from later if needed.  If you have saved state,
-     * allocate it with malloc and place it in android_app.savedState with
-     * the size in android_app.savedStateSize.  The will be freed for you
-     * later.
-     */
-    APP_CMD_SAVE_STATE,
-
-    /**
-     * Command from main thread: the app's activity has been paused.
-     */
-    APP_CMD_PAUSE,
-
-    /**
-     * Command from main thread: the app's activity has been stopped.
-     */
-    APP_CMD_STOP,
-
-    /**
-     * Command from main thread: the app's activity is being destroyed,
-     * and waiting for the app thread to clean up and exit before proceeding.
-     */
-    APP_CMD_DESTROY,
-
-    /**
-     * Command from main thread: the app's insets have changed.
-     */
-    APP_CMD_WINDOW_INSETS_CHANGED,
-
-};
-
-/**
- * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
- * app command message.
- */
-int8_t android_app_read_cmd(struct android_app* android_app);
-
-/**
- * Call with the command returned by android_app_read_cmd() to do the
- * initial pre-processing of the given command.  You can perform your own
- * actions for the command after calling this function.
- */
-void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
-
-/**
- * Call with the command returned by android_app_read_cmd() to do the
- * final post-processing of the given command.  You must have done your own
- * actions for the command before calling this function.
- */
-void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
-
-/**
- * Call this before processing input events to get the events buffer.
- * The function returns NULL if there are no events to process.
- */
-struct android_input_buffer* android_app_swap_input_buffers(
-    struct android_app* android_app);
-
-/**
- * Clear the array of motion events that were waiting to be handled, and release
- * each of them.
- *
- * This method should be called after you have processed the motion events in
- * your game loop. You should handle events at each iteration of your game loop.
- */
-void android_app_clear_motion_events(struct android_input_buffer* inputBuffer);
-
-/**
- * Clear the array of key events that were waiting to be handled, and release
- * each of them.
- *
- * This method should be called after you have processed the key up events in
- * your game loop. You should handle events at each iteration of your game loop.
- */
-void android_app_clear_key_events(struct android_input_buffer* inputBuffer);
-
-/**
- * This is the function that application code must implement, representing
- * the main entry to the app.
- */
-extern void android_main(struct android_app* app);
-
-/**
- * Set the filter to use when processing key events.
- * Any events for which the filter returns false will be ignored by
- * android_native_app_glue. If filter is set to NULL, no filtering is done.
- *
- * The default key filter will filter out volume and camera button presses.
- */
-void android_app_set_key_event_filter(struct android_app* app,
-                                      android_key_event_filter filter);
-
-/**
- * Set the filter to use when processing touch and motion events.
- * Any events for which the filter returns false will be ignored by
- * android_native_app_glue. If filter is set to NULL, no filtering is done.
- *
- * Note that the default motion event filter will only allow touchscreen events
- * through, in order to mimic NativeActivity's behaviour, so for controller
- * events to be passed to the app, set the filter to NULL.
- */
-void android_app_set_motion_event_filter(struct android_app* app,
-                                         android_motion_event_filter filter);
-
-#ifdef __cplusplus
-}
-#endif
-
-/** @} */
diff --git a/third_party/android_game_activity/include/game-text-input/gamecommon.h b/third_party/android_game_activity/include/game-text-input/gamecommon.h
deleted file mode 100644
index 38bffff..0000000
--- a/third_party/android_game_activity/include/game-text-input/gamecommon.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @defgroup game_common Game Common
- * Common structures and functions used within AGDK
- * @{
- */
-
-#pragma once
-
-/**
- * The type of a component for which to retrieve insets. See
- * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
- */
-typedef enum GameCommonInsetsType {
-    GAMECOMMON_INSETS_TYPE_CAPTION_BAR = 0,
-    GAMECOMMON_INSETS_TYPE_DISPLAY_CUTOUT,
-    GAMECOMMON_INSETS_TYPE_IME,
-    GAMECOMMON_INSETS_TYPE_MANDATORY_SYSTEM_GESTURES,
-    GAMECOMMON_INSETS_TYPE_NAVIGATION_BARS,
-    GAMECOMMON_INSETS_TYPE_STATUS_BARS,
-    GAMECOMMON_INSETS_TYPE_SYSTEM_BARS,
-    GAMECOMMON_INSETS_TYPE_SYSTEM_GESTURES,
-    GAMECOMMON_INSETS_TYPE_TAPABLE_ELEMENT,
-    GAMECOMMON_INSETS_TYPE_WATERFALL,
-    GAMECOMMON_INSETS_TYPE_COUNT
-} GameCommonInsetsType;
diff --git a/third_party/android_game_activity/include/game-text-input/gametextinput.cpp b/third_party/android_game_activity/include/game-text-input/gametextinput.cpp
deleted file mode 100644
index 6a39943..0000000
--- a/third_party/android_game_activity/include/game-text-input/gametextinput.cpp
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-#include "game-text-input/gametextinput.h"
-
-#include <android/log.h>
-#include <jni.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <algorithm>
-#include <memory>
-#include <vector>
-
-#define LOG_TAG "GameTextInput"
-
-static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16;
-
-// Cache of field ids in the Java GameTextInputState class
-struct StateClassInfo {
-    jfieldID text;
-    jfieldID selectionStart;
-    jfieldID selectionEnd;
-    jfieldID composingRegionStart;
-    jfieldID composingRegionEnd;
-};
-
-// Main GameTextInput object.
-struct GameTextInput {
-   public:
-    GameTextInput(JNIEnv *env, uint32_t max_string_size);
-    ~GameTextInput();
-    void setState(const GameTextInputState &state);
-    const GameTextInputState &getState() const { return currentState_; }
-    void setInputConnection(jobject inputConnection);
-    void processEvent(jobject textInputEvent);
-    void showIme(uint32_t flags);
-    void hideIme(uint32_t flags);
-    void setEventCallback(GameTextInputEventCallback callback, void *context);
-    jobject stateToJava(const GameTextInputState &state) const;
-    void stateFromJava(jobject textInputEvent,
-                       GameTextInputGetStateCallback callback,
-                       void *context) const;
-    void setImeInsetsCallback(GameTextInputImeInsetsCallback callback,
-                              void *context);
-    void processImeInsets(const ARect *insets);
-    const ARect &getImeInsets() const { return currentInsets_; }
-
-   private:
-    // Copy string and set other fields
-    void setStateInner(const GameTextInputState &state);
-    static void processCallback(void *context, const GameTextInputState *state);
-    JNIEnv *env_ = nullptr;
-    // Cached at initialization from
-    // com/google/androidgamesdk/gametextinput/State.
-    jclass stateJavaClass_ = nullptr;
-    // The latest text input update.
-    GameTextInputState currentState_ = {};
-    // An instance of gametextinput.InputConnection.
-    jclass inputConnectionClass_ = nullptr;
-    jobject inputConnection_ = nullptr;
-    jmethodID inputConnectionSetStateMethod_;
-    jmethodID setSoftKeyboardActiveMethod_;
-    void (*eventCallback_)(void *context,
-                           const struct GameTextInputState *state) = nullptr;
-    void *eventCallbackContext_ = nullptr;
-    void (*insetsCallback_)(void *context,
-                            const struct ARect *insets) = nullptr;
-    ARect currentInsets_ = {};
-    void *insetsCallbackContext_ = nullptr;
-    StateClassInfo stateClassInfo_ = {};
-    // Constant-sized buffer used to store state text.
-    std::vector<char> stateStringBuffer_;
-};
-
-std::unique_ptr<GameTextInput> s_gameTextInput;
-
-extern "C" {
-
-///////////////////////////////////////////////////////////
-/// GameTextInputState C Functions
-///////////////////////////////////////////////////////////
-
-// Convert to a Java structure.
-jobject currentState_toJava(const GameTextInput *gameTextInput,
-                            const GameTextInputState *state) {
-    if (state == nullptr) return NULL;
-    return gameTextInput->stateToJava(*state);
-}
-
-// Convert from Java structure.
-void currentState_fromJava(const GameTextInput *gameTextInput,
-                           jobject textInputEvent,
-                           GameTextInputGetStateCallback callback,
-                           void *context) {
-    gameTextInput->stateFromJava(textInputEvent, callback, context);
-}
-
-///////////////////////////////////////////////////////////
-/// GameTextInput C Functions
-///////////////////////////////////////////////////////////
-
-struct GameTextInput *GameTextInput_init(JNIEnv *env,
-                                         uint32_t max_string_size) {
-    if (s_gameTextInput.get() != nullptr) {
-        __android_log_print(ANDROID_LOG_WARN, LOG_TAG,
-                            "Warning: called GameTextInput_init twice without "
-                            "calling GameTextInput_destroy");
-        return s_gameTextInput.get();
-    }
-    // Don't use make_unique, for C++11 compatibility
-    s_gameTextInput =
-        std::unique_ptr<GameTextInput>(new GameTextInput(env, max_string_size));
-    return s_gameTextInput.get();
-}
-
-void GameTextInput_destroy(GameTextInput *input) {
-    if (input == nullptr || s_gameTextInput.get() == nullptr) return;
-    s_gameTextInput.reset();
-}
-
-void GameTextInput_setState(GameTextInput *input,
-                            const GameTextInputState *state) {
-    if (state == nullptr) return;
-    input->setState(*state);
-}
-
-void GameTextInput_getState(GameTextInput *input,
-                            GameTextInputGetStateCallback callback,
-                            void *context) {
-    callback(context, &input->getState());
-}
-
-void GameTextInput_setInputConnection(GameTextInput *input,
-                                      jobject inputConnection) {
-    input->setInputConnection(inputConnection);
-}
-
-void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) {
-    input->processEvent(textInputEvent);
-}
-
-void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) {
-    input->processImeInsets(insets);
-}
-
-void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) {
-    input->showIme(flags);
-}
-
-void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
-    input->hideIme(flags);
-}
-
-void GameTextInput_setEventCallback(struct GameTextInput *input,
-                                    GameTextInputEventCallback callback,
-                                    void *context) {
-    input->setEventCallback(callback, context);
-}
-
-void GameTextInput_setImeInsetsCallback(struct GameTextInput *input,
-                                        GameTextInputImeInsetsCallback callback,
-                                        void *context) {
-    input->setImeInsetsCallback(callback, context);
-}
-
-void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) {
-    *insets = input->getImeInsets();
-}
-
-}  // extern "C"
-
-///////////////////////////////////////////////////////////
-/// GameTextInput C++ class Implementation
-///////////////////////////////////////////////////////////
-
-GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size)
-    : env_(env),
-      stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE
-                                              : max_string_size) {
-    stateJavaClass_ = (jclass)env_->NewGlobalRef(
-        env_->FindClass("com/google/androidgamesdk/gametextinput/State"));
-    inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass(
-        "com/google/androidgamesdk/gametextinput/InputConnection"));
-    inputConnectionSetStateMethod_ =
-        env_->GetMethodID(inputConnectionClass_, "setState",
-                          "(Lcom/google/androidgamesdk/gametextinput/State;)V");
-    setSoftKeyboardActiveMethod_ = env_->GetMethodID(
-        inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V");
-
-    stateClassInfo_.text =
-        env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;");
-    stateClassInfo_.selectionStart =
-        env_->GetFieldID(stateJavaClass_, "selectionStart", "I");
-    stateClassInfo_.selectionEnd =
-        env_->GetFieldID(stateJavaClass_, "selectionEnd", "I");
-    stateClassInfo_.composingRegionStart =
-        env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
-    stateClassInfo_.composingRegionEnd =
-        env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
-}
-
-GameTextInput::~GameTextInput() {
-    if (stateJavaClass_ != NULL) {
-        env_->DeleteGlobalRef(stateJavaClass_);
-        stateJavaClass_ = NULL;
-    }
-    if (inputConnectionClass_ != NULL) {
-        env_->DeleteGlobalRef(inputConnectionClass_);
-        inputConnectionClass_ = NULL;
-    }
-    if (inputConnection_ != NULL) {
-        env_->DeleteGlobalRef(inputConnection_);
-        inputConnection_ = NULL;
-    }
-}
-
-void GameTextInput::setState(const GameTextInputState &state) {
-    if (inputConnection_ == nullptr) return;
-    jobject jstate = stateToJava(state);
-    env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_,
-                         jstate);
-    env_->DeleteLocalRef(jstate);
-    setStateInner(state);
-}
-
-void GameTextInput::setStateInner(const GameTextInputState &state) {
-    // Check if we're setting using our own string (other parts may be
-    // different)
-    if (state.text_UTF8 == currentState_.text_UTF8) {
-        currentState_ = state;
-        return;
-    }
-    // Otherwise, copy across the string.
-    auto bytes_needed =
-        std::min(static_cast<uint32_t>(state.text_length + 1),
-                 static_cast<uint32_t>(stateStringBuffer_.size()));
-    currentState_.text_UTF8 = stateStringBuffer_.data();
-    std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1,
-              stateStringBuffer_.data());
-    currentState_.text_length = state.text_length;
-    currentState_.selection = state.selection;
-    currentState_.composingRegion = state.composingRegion;
-    stateStringBuffer_[bytes_needed - 1] = 0;
-}
-
-void GameTextInput::setInputConnection(jobject inputConnection) {
-    if (inputConnection_ != NULL) {
-        env_->DeleteGlobalRef(inputConnection_);
-    }
-    inputConnection_ = env_->NewGlobalRef(inputConnection);
-}
-
-/*static*/ void GameTextInput::processCallback(
-    void *context, const GameTextInputState *state) {
-    auto thiz = static_cast<GameTextInput *>(context);
-    if (state != nullptr) thiz->setStateInner(*state);
-}
-
-void GameTextInput::processEvent(jobject textInputEvent) {
-    stateFromJava(textInputEvent, processCallback, this);
-    if (eventCallback_) {
-        eventCallback_(eventCallbackContext_, &currentState_);
-    }
-}
-
-void GameTextInput::showIme(uint32_t flags) {
-    if (inputConnection_ == nullptr) return;
-    env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true,
-                         flags);
-}
-
-void GameTextInput::setEventCallback(GameTextInputEventCallback callback,
-                                     void *context) {
-    eventCallback_ = callback;
-    eventCallbackContext_ = context;
-}
-
-void GameTextInput::setImeInsetsCallback(
-    GameTextInputImeInsetsCallback callback, void *context) {
-    insetsCallback_ = callback;
-    insetsCallbackContext_ = context;
-}
-
-void GameTextInput::processImeInsets(const ARect *insets) {
-    currentInsets_ = *insets;
-    if (insetsCallback_) {
-        insetsCallback_(insetsCallbackContext_, &currentInsets_);
-    }
-}
-
-void GameTextInput::hideIme(uint32_t flags) {
-    if (inputConnection_ == nullptr) return;
-    env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false,
-                         flags);
-}
-
-jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
-    static jmethodID constructor = nullptr;
-    if (constructor == nullptr) {
-        constructor = env_->GetMethodID(stateJavaClass_, "<init>",
-                                        "(Ljava/lang/String;IIII)V");
-        if (constructor == nullptr) {
-            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
-                                "Can't find gametextinput.State constructor");
-            return nullptr;
-        }
-    }
-    const char *text = state.text_UTF8;
-    if (text == nullptr) {
-        static char empty_string[] = "";
-        text = empty_string;
-    }
-    // Note that this expects 'modified' UTF-8 which is not the same as UTF-8
-    // https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
-    jstring jtext = env_->NewStringUTF(text);
-    jobject jobj =
-        env_->NewObject(stateJavaClass_, constructor, jtext,
-                        state.selection.start, state.selection.end,
-                        state.composingRegion.start, state.composingRegion.end);
-    env_->DeleteLocalRef(jtext);
-    return jobj;
-}
-
-void GameTextInput::stateFromJava(jobject textInputEvent,
-                                  GameTextInputGetStateCallback callback,
-                                  void *context) const {
-    jstring text =
-        (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text);
-    // Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it,
-    // except at the end. It's actually not specified whether the value returned
-    // by GetStringUTFChars includes a null at the end, but it *seems to* on
-    // Android.
-    const char *text_chars = env_->GetStringUTFChars(text, NULL);
-    int text_len = env_->GetStringUTFLength(
-        text);  // Length in bytes, *not* including the null.
-    int selectionStart =
-        env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart);
-    int selectionEnd =
-        env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd);
-    int composingRegionStart =
-        env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
-    int composingRegionEnd =
-        env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
-    GameTextInputState state{text_chars,
-                             text_len,
-                             {selectionStart, selectionEnd},
-                             {composingRegionStart, composingRegionEnd}};
-    callback(context, &state);
-    env_->ReleaseStringUTFChars(text, text_chars);
-    env_->DeleteLocalRef(text);
-}
diff --git a/third_party/android_game_activity/include/game-text-input/gametextinput.h b/third_party/android_game_activity/include/game-text-input/gametextinput.h
deleted file mode 100644
index a85265e..0000000
--- a/third_party/android_game_activity/include/game-text-input/gametextinput.h
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @defgroup game_text_input Game Text Input
- * The interface to use GameTextInput.
- * @{
- */
-
-#pragma once
-
-#include <android/rect.h>
-#include <jni.h>
-#include <stdint.h>
-
-#include "gamecommon.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * This struct holds a span within a region of text from start (inclusive) to
- * end (exclusive). An empty span or cursor position is specified with
- * start==end. An undefined span is specified with start = end = SPAN_UNDEFINED.
- */
-typedef struct GameTextInputSpan {
-    /** The start of the region (inclusive). */
-    int32_t start;
-    /** The end of the region (exclusive). */
-    int32_t end;
-} GameTextInputSpan;
-
-/**
- * Values with special meaning in a GameTextInputSpan.
- */
-enum GameTextInputSpanFlag { SPAN_UNDEFINED = -1 };
-
-/**
- * This struct holds the state of an editable section of text.
- * The text can have a selection and a composing region defined on it.
- * A composing region is used by IMEs that allow input using multiple steps to
- * compose a glyph or word. Use functions GameTextInput_getState and
- * GameTextInput_setState to read and modify the state that an IME is editing.
- */
-typedef struct GameTextInputState {
-    /**
-     * Text owned by the state, as a modified UTF-8 string. Null-terminated.
-     * https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
-     */
-    const char *text_UTF8;
-    /**
-     * Length in bytes of text_UTF8, *not* including the null at end.
-     */
-    int32_t text_length;
-    /**
-     * A selection defined on the text.
-     */
-    GameTextInputSpan selection;
-    /**
-     * A composing region defined on the text.
-     */
-    GameTextInputSpan composingRegion;
-} GameTextInputState;
-
-/**
- * A callback called by GameTextInput_getState.
- * @param context User-defined context.
- * @param state State, owned by the library, that will be valid for the duration
- * of the callback.
- */
-typedef void (*GameTextInputGetStateCallback)(
-    void *context, const struct GameTextInputState *state);
-
-/**
- * Opaque handle to the GameTextInput API.
- */
-typedef struct GameTextInput GameTextInput;
-
-/**
- * Initialize the GameTextInput library.
- * If called twice without GameTextInput_destroy being called, the same pointer
- * will be returned and a warning will be issued.
- * @param env A JNI env valid on the calling thread.
- * @param max_string_size The maximum length of a string that can be edited. If
- * zero, the maximum defaults to 65536 bytes. A buffer of this size is allocated
- * at initialization.
- * @return A handle to the library.
- */
-GameTextInput *GameTextInput_init(JNIEnv *env, uint32_t max_string_size);
-
-/**
- * When using GameTextInput, you need to create a gametextinput.InputConnection
- * on the Java side and pass it using this function to the library, unless using
- * GameActivity in which case this will be done for you. See the GameActivity
- * source code or GameTextInput samples for examples of usage.
- * @param input A valid GameTextInput library handle.
- * @param inputConnection A gametextinput.InputConnection object.
- */
-void GameTextInput_setInputConnection(GameTextInput *input,
-                                      jobject inputConnection);
-
-/**
- * Unless using GameActivity, it is required to call this function from your
- * Java gametextinput.Listener.stateChanged method to convert eventState and
- * trigger any event callbacks. When using GameActivity, this does not need to
- * be called as event processing is handled by the Activity.
- * @param input A valid GameTextInput library handle.
- * @param eventState A Java gametextinput.State object.
- */
-void GameTextInput_processEvent(GameTextInput *input, jobject eventState);
-
-/**
- * Free any resources owned by the GameTextInput library.
- * Any subsequent calls to the library will fail until GameTextInput_init is
- * called again.
- * @param input A valid GameTextInput library handle.
- */
-void GameTextInput_destroy(GameTextInput *input);
-
-/**
- * Flags to be passed to GameTextInput_showIme.
- */
-enum ShowImeFlags {
-    SHOW_IME_UNDEFINED = 0,  // Default value.
-    SHOW_IMPLICIT =
-        1,  // Indicates that the user has forced the input method open so it
-            // should not be closed until they explicitly do so.
-    SHOW_FORCED = 2  // Indicates that this is an implicit request to show the
-                     // input window, not as the result of a direct request by
-                     // the user. The window may not be shown in this case.
-};
-
-/**
- * Show the IME. Calls InputMethodManager.showSoftInput().
- * @param input A valid GameTextInput library handle.
- * @param flags Defined in ShowImeFlags above. For more information see:
- * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
- */
-void GameTextInput_showIme(GameTextInput *input, uint32_t flags);
-
-/**
- * Flags to be passed to GameTextInput_hideIme.
- */
-enum HideImeFlags {
-    HIDE_IME_UNDEFINED = 0,  // Default value.
-    HIDE_IMPLICIT_ONLY =
-        1,  // Indicates that the soft input window should only be hidden if it
-            // was not explicitly shown by the user.
-    HIDE_NOT_ALWAYS =
-        2,  // Indicates that the soft input window should normally be hidden,
-            // unless it was originally shown with SHOW_FORCED.
-};
-
-/**
- * Show the IME. Calls InputMethodManager.hideSoftInputFromWindow().
- * @param input A valid GameTextInput library handle.
- * @param flags Defined in HideImeFlags above. For more information see:
- * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
- */
-void GameTextInput_hideIme(GameTextInput *input, uint32_t flags);
-
-/**
- * Call a callback with the current GameTextInput state, which may have been
- * modified by changes in the IME and calls to GameTextInput_setState. We use a
- * callback rather than returning the state in order to simplify ownership of
- * text_UTF8 strings. These strings are only valid during the calling of the
- * callback.
- * @param input A valid GameTextInput library handle.
- * @param callback A function that will be called with valid state.
- * @param context Context used by the callback.
- */
-void GameTextInput_getState(GameTextInput *input,
-                            GameTextInputGetStateCallback callback,
-                            void *context);
-
-/**
- * Set the current GameTextInput state. This state is reflected to any active
- * IME.
- * @param input A valid GameTextInput library handle.
- * @param state The state to set. Ownership is maintained by the caller and must
- * remain valid for the duration of the call.
- */
-void GameTextInput_setState(GameTextInput *input,
-                            const GameTextInputState *state);
-
-/**
- * Type of the callback needed by GameTextInput_setEventCallback that will be
- * called every time the IME state changes.
- * @param context User-defined context set in GameTextInput_setEventCallback.
- * @param current_state Current IME state, owned by the library and valid during
- * the callback.
- */
-typedef void (*GameTextInputEventCallback)(
-    void *context, const GameTextInputState *current_state);
-
-/**
- * Optionally set a callback to be called whenever the IME state changes.
- * Not necessary if you are using GameActivity, which handles these callbacks
- * for you.
- * @param input A valid GameTextInput library handle.
- * @param callback Called by the library when the IME state changes.
- * @param context Context passed as first argument to the callback.
- */
-void GameTextInput_setEventCallback(GameTextInput *input,
-                                    GameTextInputEventCallback callback,
-                                    void *context);
-
-/**
- * Type of the callback needed by GameTextInput_setImeInsetsCallback that will
- * be called every time the IME window insets change.
- * @param context User-defined context set in
- * GameTextInput_setImeWIndowInsetsCallback.
- * @param current_insets Current IME insets, owned by the library and valid
- * during the callback.
- */
-typedef void (*GameTextInputImeInsetsCallback)(void *context,
-                                               const ARect *current_insets);
-
-/**
- * Optionally set a callback to be called whenever the IME insets change.
- * Not necessary if you are using GameActivity, which handles these callbacks
- * for you.
- * @param input A valid GameTextInput library handle.
- * @param callback Called by the library when the IME insets change.
- * @param context Context passed as first argument to the callback.
- */
-void GameTextInput_setImeInsetsCallback(GameTextInput *input,
-                                        GameTextInputImeInsetsCallback callback,
-                                        void *context);
-
-/**
- * Get the current window insets for the IME.
- * @param input A valid GameTextInput library handle.
- * @param insets Filled with the current insets by this function.
- */
-void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets);
-
-/**
- * Unless using GameActivity, it is required to call this function from your
- * Java gametextinput.Listener.onImeInsetsChanged method to
- * trigger any event callbacks. When using GameActivity, this does not need to
- * be called as insets processing is handled by the Activity.
- * @param input A valid GameTextInput library handle.
- * @param eventState A Java gametextinput.State object.
- */
-void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets);
-
-/**
- * Convert a GameTextInputState struct to a Java gametextinput.State object.
- * Don't forget to delete the returned Java local ref when you're done.
- * @param input A valid GameTextInput library handle.
- * @param state Input state to convert.
- * @return A Java object of class gametextinput.State. The caller is required to
- * delete this local reference.
- */
-jobject GameTextInputState_toJava(const GameTextInput *input,
-                                  const GameTextInputState *state);
-
-/**
- * Convert from a Java gametextinput.State object into a C GameTextInputState
- * struct.
- * @param input A valid GameTextInput library handle.
- * @param state A Java gametextinput.State object.
- * @param callback A function called with the C struct, valid for the duration
- * of the call.
- * @param context Context passed to the callback.
- */
-void GameTextInputState_fromJava(const GameTextInput *input, jobject state,
-                                 GameTextInputGetStateCallback callback,
-                                 void *context);
-
-#ifdef __cplusplus
-}
-#endif
-
-/** @} */
diff --git a/third_party/android_game_activity/module.json b/third_party/android_game_activity/module.json
deleted file mode 100644
index b571794..0000000
--- a/third_party/android_game_activity/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"export_libraries":[],"library_name":null,"android":{"export_libraries":null,"library_name":null}}
\ No newline at end of file
diff --git a/third_party/angle/BUILD.gn b/third_party/angle/BUILD.gn
index 35f6dd7..bbc067f 100644
--- a/third_party/angle/BUILD.gn
+++ b/third_party/angle/BUILD.gn
@@ -284,7 +284,10 @@
 if (is_starboard) {
   config("starboard_angle_config") {
     if (is_win) {
-      cflags = ["/wd4200"]
+      configs = [ "//build/config/win:visual_studio_version_compat" ]
+      cflags = [
+        "/wd4200",
+      ]
     }
   }
 }
diff --git a/third_party/angle/src/compiler/translator/TranslatorGLSL.cpp b/third_party/angle/src/compiler/translator/TranslatorGLSL.cpp
index 246754a..e8ef008 100644
--- a/third_party/angle/src/compiler/translator/TranslatorGLSL.cpp
+++ b/third_party/angle/src/compiler/translator/TranslatorGLSL.cpp
@@ -318,6 +318,7 @@
         // on drivers that don't have the extension at all as it would break WebGL 1 for
         // some users.
         sink << "#extension GL_ARB_gpu_shader5 : enable\n";
+        sink << "#extension GL_EXT_gpu_shader5 : enable\n";
     }
 
     TExtensionGLSL extensionGLSL(getOutputType());
diff --git a/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
index 8268fba..5ff2eaa 100644
--- a/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
+++ b/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
@@ -428,12 +428,9 @@
 
 
 #if defined(STARBOARD)
-        // Only allow feature level 10 on starboard by default.
-#if defined(ENABLE_D3D11_FEATURE_LEVEL_11)
+        // D3D11CreateDevice will choose proper feature level from this list.
         mAvailableFeatureLevels.push_back(D3D_FEATURE_LEVEL_11_0);
-#else
         mAvailableFeatureLevels.push_back(D3D_FEATURE_LEVEL_10_0);
-#endif // defined(ENABLE_D3D11_FEATURE_LEVEL_11)
 #else
         if (requestedMajorVersion == EGL_DONT_CARE || requestedMajorVersion >= 11)
         {
diff --git a/third_party/angle/src/libANGLE/renderer/gl/RendererGL.cpp b/third_party/angle/src/libANGLE/renderer/gl/RendererGL.cpp
index 75270b6..f3c47dd 100644
--- a/third_party/angle/src/libANGLE/renderer/gl/RendererGL.cpp
+++ b/third_party/angle/src/libANGLE/renderer/gl/RendererGL.cpp
@@ -78,6 +78,15 @@
     "share_context to eglCreateContext. Results are undefined.",
 };
 #endif  // defined(ANGLE_PLATFORM_ANDROID)
+
+const char *kIgnoredWarnings[] = {
+    // We always request GL_ARB_gpu_shader5 and GL_EXT_gpu_shader5 when compiling shaders but some
+    // drivers warn when it is not present. This ends up spamming the console on every shader
+    // compile.
+    "extension `GL_ARB_gpu_shader5' unsupported in",
+    "extension `GL_EXT_gpu_shader5' unsupported in",
+};
+
 }  // namespace
 
 static void INTERNAL_GL_APIENTRY LogGLDebugMessage(GLenum source,
@@ -192,6 +201,14 @@
         // Don't print performance warnings. They tend to be very spammy in the dEQP test suite and
         // there is very little we can do about them.
 
+        for (const char *&warn : kIgnoredWarnings)
+        {
+            if (strstr(message, warn) != nullptr)
+            {
+                return;
+            }
+        }
+
         // TODO(ynovikov): filter into WARN and INFO if INFO is ever implemented
         WARN() << std::endl
                << "\tSource: " << sourceText << std::endl
diff --git a/third_party/chromium/media/base/decoder_buffer.cc b/third_party/chromium/media/base/decoder_buffer.cc
index 6060c9b..d242506 100644
--- a/third_party/chromium/media/base/decoder_buffer.cc
+++ b/third_party/chromium/media/base/decoder_buffer.cc
@@ -7,9 +7,6 @@
 #include <sstream>
 
 #include "base/debug/alias.h"
-#if defined(STARBOARD)
-#include "starboard/media.h"
-#endif  // defined(STARBOARD)
 
 namespace media {
 
@@ -20,6 +17,12 @@
 }  // namespace
 
 // static
+DecoderBuffer::Allocator* DecoderBuffer::Allocator::GetInstance() {
+  DCHECK(s_allocator);
+  return s_allocator;
+}
+
+// static
 void DecoderBuffer::Allocator::Set(Allocator* allocator) {
   s_allocator = allocator;
 }
@@ -73,8 +76,7 @@
       is_key_frame_(false) {}
 
 DecoderBuffer::DecoderBuffer(
-    std::unique_ptr<ReadOnlyUnalignedMapping> shared_mem_mapping,
-    size_t size)
+    std::unique_ptr<ReadOnlyUnalignedMapping> shared_mem_mapping, size_t size)
     : size_(size),
       side_data_size_(0),
       shared_mem_mapping_(std::move(shared_mem_mapping)),
@@ -96,15 +98,8 @@
   DCHECK(s_allocator);
   DCHECK(!data_);
 
-#if SB_API_VERSION >= 14
-  int alignment = SbMediaGetBufferAlignment();
-  int padding = SbMediaGetBufferPadding();
-#else  // SB_API_VERSION >= 14
-  int alignment = std::max(SbMediaGetBufferAlignment(kSbMediaTypeAudio),
-                           SbMediaGetBufferAlignment(kSbMediaTypeVideo));
-  int padding = std::max(SbMediaGetBufferPadding(kSbMediaTypeAudio),
-                         SbMediaGetBufferPadding(kSbMediaTypeVideo));
-#endif  // SB_API_VERSION >= 14
+  int alignment = s_allocator->GetBufferAlignment();
+  int padding = s_allocator->GetBufferPadding();
   allocated_size_ = size_ + padding;
   data_ = static_cast<uint8_t*>(s_allocator->Allocate(allocated_size_,
                                                       alignment));
diff --git a/third_party/chromium/media/base/decoder_buffer.h b/third_party/chromium/media/base/decoder_buffer.h
index ed7c01e..5892e26 100644
--- a/third_party/chromium/media/base/decoder_buffer.h
+++ b/third_party/chromium/media/base/decoder_buffer.h
@@ -21,9 +21,11 @@
 #include "media/base/decrypt_config.h"
 #include "media/base/media_export.h"
 #include "media/base/timestamp_constants.h"
-#if !defined(STARBOARD)
+#if defined(STARBOARD)
+#include "starboard/media.h"
+#else  // defined(STARBOARD)
 #include "media/base/unaligned_shared_memory.h"
-#endif  // !defined(STARBOARD)
+#endif  // defined(STARBOARD)
 
 namespace media {
 
@@ -46,16 +48,31 @@
 
 #if defined(STARBOARD)
   class Allocator {
-    public:
-      // The function should never return nullptr.  It may terminate the app on
-      // allocation failure.
-      virtual void* Allocate(size_t size, size_t alignment) = 0;
-      virtual void Free(void* p, size_t size) = 0;
+   public:
+    static Allocator* GetInstance();
 
-    protected:
-      ~Allocator() {}
+    // The function should never return nullptr.  It may terminate the app on
+    // allocation failure.
+    virtual void* Allocate(size_t size, size_t alignment) = 0;
+    virtual void Free(void* p, size_t size) = 0;
 
-      static void Set(Allocator* allocator);
+    virtual int GetAudioBufferBudget() const = 0;
+    virtual int GetBufferAlignment() const = 0;
+    virtual int GetBufferPadding() const = 0;
+    virtual SbTime GetBufferGarbageCollectionDurationThreshold() const = 0;
+    virtual int GetProgressiveBufferBudget(SbMediaVideoCodec codec,
+                                           int resolution_width,
+                                           int resolution_height,
+                                           int bits_per_pixel) const = 0;
+    virtual int GetVideoBufferBudget(SbMediaVideoCodec codec,
+                                     int resolution_width,
+                                     int resolution_height,
+                                     int bits_per_pixel) const = 0;
+
+   protected:
+    ~Allocator() {}
+
+    static void Set(Allocator* allocator);
   };
 #endif  // defined(STARBOARD)
 
diff --git a/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc b/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
index 19fca6b..d8305f0 100644
--- a/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
+++ b/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
@@ -14,16 +14,16 @@
 
 #include "media/base/demuxer_memory_limit.h"
 
+#include "media/base/decoder_buffer.h"
 #include "media/base/starboard_utils.h"
 #include "media/base/video_codecs.h"
 #include "base/logging.h"
-#include "starboard/media.h"
 
 namespace media {
 
 size_t GetDemuxerStreamAudioMemoryLimit(
     const AudioDecoderConfig* /*audio_config*/) {
-  return SbMediaGetAudioBufferBudget();
+  return DecoderBuffer::Allocator::GetInstance()->GetAudioBufferBudget();
 }
 
 size_t GetDemuxerStreamVideoMemoryLimit(
diff --git a/third_party/chromium/media/base/demuxer_stream.h b/third_party/chromium/media/base/demuxer_stream.h
index 9a31c24..67769b3 100644
--- a/third_party/chromium/media/base/demuxer_stream.h
+++ b/third_party/chromium/media/base/demuxer_stream.h
@@ -5,6 +5,10 @@
 #ifndef MEDIA_BASE_DEMUXER_STREAM_H_
 #define MEDIA_BASE_DEMUXER_STREAM_H_
 
+#if defined(STARBOARD)
+#include <vector>
+#endif  // defined(STARBOARD)
+
 #include "base/callback.h"
 #include "base/memory/ref_counted.h"
 #include "media/base/media_export.h"
@@ -73,8 +77,13 @@
   // The first parameter indicates the status of the read.
   // The second parameter is non-NULL and contains media data
   // or the end of the stream if the first parameter is kOk. NULL otherwise.
+#if defined(STARBOARD)
+  typedef base::OnceCallback<void(Status, const std::vector<scoped_refptr<DecoderBuffer>>&)> ReadCB;
+  virtual void Read(int max_number_of_buffers_to_read, ReadCB read_cb) = 0;
+#else  // defined (STARBOARD)
   typedef base::OnceCallback<void(Status, scoped_refptr<DecoderBuffer>)> ReadCB;
   virtual void Read(ReadCB read_cb) = 0;
+#endif  // defined (STARBOARD)
 
   // Returns the audio/video decoder configuration. It is an error to call the
   // audio method on a video stream and vice versa. After |kConfigChanged| is
diff --git a/third_party/chromium/media/base/starboard_utils.cc b/third_party/chromium/media/base/starboard_utils.cc
index 687e5af..96fb524 100644
--- a/third_party/chromium/media/base/starboard_utils.cc
+++ b/third_party/chromium/media/base/starboard_utils.cc
@@ -19,6 +19,7 @@
 #include "base/logging.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
+#include "media/base/decoder_buffer.h"
 #include "media/base/decrypt_config.h"
 #include "starboard/common/media.h"
 #include "starboard/configuration.h"
@@ -382,18 +383,19 @@
 
   return sb_media_color_metadata;
 }
-
 int GetSbMediaVideoBufferBudget(const VideoDecoderConfig* video_config,
                                 const std::string& mime_type) {
   if (!video_config) {
-    return SbMediaGetVideoBufferBudget(kSbMediaVideoCodecH264, 1920, 1080, 8);
+    return DecoderBuffer::Allocator::GetInstance()->GetVideoBufferBudget(
+        kSbMediaVideoCodecH264, 1920, 1080, 8);
   }
 
   auto width = video_config->visible_rect().size().width();
   auto height = video_config->visible_rect().size().height();
   auto bits_per_pixel = GetBitsPerPixel(mime_type);
   auto codec = MediaVideoCodecToSbMediaVideoCodec(video_config->codec());
-  return SbMediaGetVideoBufferBudget(codec, width, height, bits_per_pixel);
+  return DecoderBuffer::Allocator::GetInstance()->GetVideoBufferBudget(
+      codec, width, height, bits_per_pixel);
 }
 
 std::string ExtractCodecs(const std::string& mime_type) {
diff --git a/third_party/chromium/media/filters/chunk_demuxer.cc b/third_party/chromium/media/filters/chunk_demuxer.cc
index e738be9..1d6e475 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.cc
+++ b/third_party/chromium/media/filters/chunk_demuxer.cc
@@ -127,8 +127,15 @@
   DVLOG(1) << "ChunkDemuxerStream::AbortReads()";
   base::AutoLock auto_lock(lock_);
   ChangeState_Locked(RETURNING_ABORT_FOR_READS);
-  if (read_cb_)
+  pending_config_change_ = false;
+
+  if (read_cb_) {
+#if defined(STARBOARD)
+    std::move(read_cb_).Run(kAborted, {});
+#else // defined (STARBOARD)
     std::move(read_cb_).Run(kAborted, nullptr);
+#endif // defined (STARBOARD)
+  }
 }
 
 void ChunkDemuxerStream::CompletePendingReadIfPossible() {
@@ -147,8 +154,12 @@
   // Pass an end of stream buffer to the pending callback to signal that no more
   // data will be sent.
   if (read_cb_) {
+#if defined(STARBOARD)
+    std::move(read_cb_).Run(DemuxerStream::kOk, {StreamParserBuffer::CreateEOSBuffer()});
+#else // defined (STARBOARD)
     std::move(read_cb_).Run(DemuxerStream::kOk,
                             StreamParserBuffer::CreateEOSBuffer());
+#endif // defined (STARBOARD)
   }
 }
 
@@ -348,18 +359,33 @@
 }
 
 // DemuxerStream methods.
+#if defined(STARBOARD)
+void ChunkDemuxerStream::Read(int max_number_of_buffers_to_read, ReadCB read_cb) {
+#else  // defined(STARBOARD)
 void ChunkDemuxerStream::Read(ReadCB read_cb) {
+#endif  // defined(STARBOARD)
   base::AutoLock auto_lock(lock_);
   DCHECK_NE(state_, UNINITIALIZED);
   DCHECK(!read_cb_);
 
   read_cb_ = BindToCurrentLoop(std::move(read_cb));
 
+#if defined(STARBOARD)
+  max_number_of_buffers_to_read_ = max_number_of_buffers_to_read;
+
+  if (!is_enabled_) {
+    DVLOG(1) << "Read from disabled stream, returning EOS";
+    std::move(read_cb_).Run(kOk, {StreamParserBuffer::CreateEOSBuffer()});
+    return;
+  }
+#else   // defined(STARBOARD)
+
   if (!is_enabled_) {
     DVLOG(1) << "Read from disabled stream, returning EOS";
     std::move(read_cb_).Run(kOk, StreamParserBuffer::CreateEOSBuffer());
     return;
   }
+#endif  // defined(STARBOARD)
 
   CompletePendingReadIfPossible_Locked();
 }
@@ -406,7 +432,11 @@
     stream_->Seek(timestamp);
   } else if (read_cb_) {
     DVLOG(1) << "Read from disabled stream, returning EOS";
+#if defined(STARBOARD)
+    std::move(read_cb_).Run(kOk, {StreamParserBuffer::CreateEOSBuffer()});
+#else // defined(STARBOARD)
     std::move(read_cb_).Run(kOk, StreamParserBuffer::CreateEOSBuffer());
+#endif // defined(STARBOARD)
   }
 }
 
@@ -436,6 +466,72 @@
 
 ChunkDemuxerStream::~ChunkDemuxerStream() = default;
 
+#if defined(STARBOARD)
+void ChunkDemuxerStream::CompletePendingReadIfPossible_Locked() {
+  lock_.AssertAcquired();
+  DCHECK(read_cb_);
+
+  DemuxerStream::Status status = DemuxerStream::kAborted;
+  std::vector<scoped_refptr<DecoderBuffer>> buffers;
+
+  if (pending_config_change_) {
+    status = kConfigChanged;
+    std::move(read_cb_).Run(status, buffers);
+    pending_config_change_ = false;
+    return;
+  }
+
+  if (state_ == RETURNING_DATA_FOR_READS) {
+    for (int i = 0; i < max_number_of_buffers_to_read_; i++) {
+      scoped_refptr<StreamParserBuffer> buffer;
+      SourceBufferStreamStatus stream_status = stream_->GetNextBuffer(&buffer);
+      if (stream_status == SourceBufferStreamStatus::kSuccess) {
+        DVLOG(2) << __func__ << ": found kOk, type " << type_ << ", dts "
+                 << buffer->GetDecodeTimestamp().InSecondsF() << ", pts "
+                 << buffer->timestamp().InSecondsF() << ", dur "
+                 << buffer->duration().InSecondsF() << ", key "
+                 << buffer->is_key_frame();
+        buffers.push_back(std::move(buffer));
+        status = DemuxerStream::kOk;
+      } else if (stream_status == SourceBufferStreamStatus::kEndOfStream) {
+        buffer = StreamParserBuffer::CreateEOSBuffer();
+        DVLOG(2) << __func__ << ": found kOk with EOS buffer, type "
+                 << type_;
+        buffers.push_back(std::move(buffer));
+        status = DemuxerStream::kOk;
+        break;
+      } else if (stream_status == SourceBufferStreamStatus::kConfigChange) {
+        DVLOG(2) << __func__ << ": returning kConfigChange, type " << type_;
+        status = kConfigChanged;
+        break;
+      } else if (stream_status == SourceBufferStreamStatus::kNeedBuffer) {
+        if (buffers.empty())
+          return;
+        else
+          break;
+      }
+    }
+
+    if (status == kConfigChanged && !buffers.empty()) {
+      pending_config_change_ = true;
+      status = kOk;
+    }
+
+  } else if (state_ == RETURNING_ABORT_FOR_READS) {
+    status = DemuxerStream::kAborted;
+    DVLOG(2) << __func__ << ": returning kAborted, type " << type_;
+  } else if (state_ == SHUTDOWN) {
+    status = DemuxerStream::kOk;
+    buffers.push_back(std::move(StreamParserBuffer::CreateEOSBuffer()));
+    DVLOG(2) << __func__ << ": returning kOk with EOS buffer, type " << type_;
+  } else if (state_ == UNINITIALIZED) {
+    NOTREACHED();
+    return;
+  }
+
+  std::move(read_cb_).Run(status, buffers);
+}
+#else // defined(STARBOARD)
 void ChunkDemuxerStream::CompletePendingReadIfPossible_Locked() {
   lock_.AssertAcquired();
   DCHECK(read_cb_);
@@ -492,6 +588,7 @@
 
   std::move(read_cb_).Run(status, buffer);
 }
+#endif // defined(STARBOARD)
 
 ChunkDemuxer::ChunkDemuxer(
     base::OnceClosure open_cb,
diff --git a/third_party/chromium/media/filters/chunk_demuxer.h b/third_party/chromium/media/filters/chunk_demuxer.h
index e59fcfb..fd67e6f 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.h
+++ b/third_party/chromium/media/filters/chunk_demuxer.h
@@ -130,7 +130,12 @@
   std::string mime_type() const override { return mime_type_; }
 #endif  // defined (STARBOARD)
 
+#if defined(STARBOARD)
+  void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
+#else  // defined (STARBOARD)
   void Read(ReadCB read_cb) override;
+#endif  // defined (STARBOARD)
+
   Type type() const override;
   Liveness liveness() const override;
   AudioDecoderConfig audio_decoder_config() override;
@@ -180,6 +185,8 @@
 
 #if defined(STARBOARD)
   const std::string mime_type_;
+  int max_number_of_buffers_to_read_{1};
+  bool pending_config_change_ {false};
 #endif  // defined (STARBOARD)
 
   // Specifies the type of the stream.
diff --git a/third_party/chromium/media/filters/source_buffer_stream.cc b/third_party/chromium/media/filters/source_buffer_stream.cc
index b6d4bbc..a1e9e2e 100644
--- a/third_party/chromium/media/filters/source_buffer_stream.cc
+++ b/third_party/chromium/media/filters/source_buffer_stream.cc
@@ -14,12 +14,12 @@
 #include "base/logging.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/trace_event/trace_event.h"
+#if defined(STARBOARD)
+#include "media/base/decoder_buffer.h"
+#endif  // defined(STARBOARD)
 #include "media/base/demuxer_memory_limit.h"
 #include "media/base/media_switches.h"
 #include "media/base/timestamp_constants.h"
-#if defined(STARBOARD)
-#include "starboard/media.h"
-#endif  // defined(STARBOARD)
 
 namespace media {
 
@@ -849,7 +849,8 @@
   // Address duration based GC.
   base::TimeDelta duration = GetBufferedDurationForGarbageCollection();
   const SbTime duration_gc_threadold =
-      SbMediaGetBufferGarbageCollectionDurationThreshold();
+      DecoderBuffer::Allocator::GetInstance()
+          ->GetBufferGarbageCollectionDurationThreshold();
   if (duration.ToSbTime() > duration_gc_threadold) {
     effective_memory_limit = ranges_size * duration_gc_threadold /
                              duration.ToSbTime();
diff --git a/third_party/crashpad/handler/BUILD.gn b/third_party/crashpad/handler/BUILD.gn
index 10582a8..c91280d 100644
--- a/third_party/crashpad/handler/BUILD.gn
+++ b/third_party/crashpad/handler/BUILD.gn
@@ -153,7 +153,8 @@
   }
 }
 
-if (!crashpad_is_ios) {
+# TODO: b/256660693 Disabled crashpad_handler executables due to build issues on Android
+if (!crashpad_is_ios && !crashpad_is_android) {
   if (crashpad_is_in_starboard) {
     config("crashpad_handler_starboard_config") {
       cflags = [
@@ -207,27 +208,28 @@
 # It is normal to package native code as a loadable module but Android's APK
 # installer will ignore files not named like a shared object, so give the
 # handler executable an acceptable name.
-if (crashpad_is_android) {
-  copy("crashpad_handler_named_as_so") {
-    deps = [ ":crashpad_handler" ]
-
-    sources = [ "$root_out_dir/crashpad_handler" ]
-
-    outputs = [ "$root_out_dir/libcrashpad_handler.so" ]
-  }
-
-  crashpad_executable("crashpad_handler_trampoline") {
-    output_name = "libcrashpad_handler_trampoline.so"
-
-    sources = [ "linux/handler_trampoline.cc" ]
-
-    ldflags = [ "-llog" ]
-
-    if (crashpad_is_in_chromium) {
-      no_default_deps = true
-    }
-  }
-}
+# TODO: b/256660693 Disabled crashpad_handler executables due to build issues on Android
+# if (crashpad_is_android) {
+#   copy("crashpad_handler_named_as_so") {
+#     deps = [ ":crashpad_handler" ]
+# 
+#     sources = [ "$root_out_dir/crashpad_handler" ]
+# 
+#     outputs = [ "$root_out_dir/libcrashpad_handler.so" ]
+#   }
+# 
+#   crashpad_executable("crashpad_handler_trampoline") {
+#     output_name = "libcrashpad_handler_trampoline.so"
+# 
+#     sources = [ "linux/handler_trampoline.cc" ]
+# 
+#     ldflags = [ "-llog" ]
+# 
+#     if (crashpad_is_in_chromium) {
+#       no_default_deps = true
+#     }
+#   }
+# }
 
 if (!crashpad_is_ios && !crashpad_is_in_starboard) {
   crashpad_executable("crashpad_handler_test_extended_handler") {
diff --git a/third_party/crashpad/test/gtest_main.cc b/third_party/crashpad/test/gtest_main.cc
index ad3a095..23b416d 100644
--- a/third_party/crashpad/test/gtest_main.cc
+++ b/third_party/crashpad/test/gtest_main.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "base/base_wrapper.h"
 #include "build/build_config.h"
 #include "gtest/gtest.h"
 #include "test/main_arguments.h"
diff --git a/third_party/icu/source/common/umapfile.cpp b/third_party/icu/source/common/umapfile.cpp
index b2aad14..cfab828 100644
--- a/third_party/icu/source/common/umapfile.cpp
+++ b/third_party/icu/source/common/umapfile.cpp
@@ -26,6 +26,10 @@
 #include "udatamem.h"
 #include "umapfile.h"
 
+#if MAP_IMPLEMENTATION==MAP_STARBOARD
+#include "starboard/extension/memory_mapped_file.h"
+#endif
+
 /* memory-mapping base definitions ------------------------------------------ */
 
 #if MAP_IMPLEMENTATION==MAP_WIN32
@@ -351,6 +355,10 @@
 #elif MAP_IMPLEMENTATION==MAP_STARBOARD
     U_CFUNC UBool
     uprv_mapFile(UDataMemory *pData, const char *path, UErrorCode *status) {
+        if (U_FAILURE(*status)) {
+            return FALSE;
+        }
+
         UDataMemory_init(pData); /* Clear the output struct.        */
 
         /* open the input file */
@@ -372,6 +380,28 @@
             return FALSE;
         }
 
+        pData->heapAllocated = false;
+
+        const auto* memory_mapped_file_extension =
+            reinterpret_cast<const CobaltExtensionMemoryMappedFileApi*>(
+                SbSystemGetExtension(kCobaltExtensionMemoryMappedFileName));
+
+        if(memory_mapped_file_extension &&
+           strcmp(memory_mapped_file_extension->name,
+                  kCobaltExtensionMemoryMappedFileName) == 0 &&
+           memory_mapped_file_extension->version >= 1) {
+                void *p = memory_mapped_file_extension->MemoryMapFile(
+                        NULL, path, kSbMemoryMapProtectRead, 0,fileLength);
+                if(p) {
+                    pData->map=p;
+                    pData->pHeader=(const DataHeader *)p;
+                    pData->mapAddr=p;
+                    SbFileClose(file);
+                    return TRUE;
+                }
+            // If mmap extension didn't work, fall back to allocating
+        }
+
         /* allocate the memory to hold the file data */
         void *p = uprv_malloc(fileLength);
         if (!p) {
@@ -390,13 +420,19 @@
         pData->map=p;
         pData->pHeader=(const DataHeader *)p;
         pData->mapAddr=p;
+        pData->heapAllocated = true;
         return TRUE;
     }
 
     U_CFUNC void
     uprv_unmapFile(UDataMemory *pData) {
         if(pData!=NULL && pData->map!=NULL) {
-            uprv_free(pData->map);
+            if(pData->heapAllocated) {
+                uprv_free(pData->map);
+            } else {
+                size_t dataLen = (char *)pData->map - (char *)pData->mapAddr;
+                SbMemoryUnmap(pData->mapAddr, dataLen);
+            }
             pData->map     = NULL;
             pData->mapAddr = NULL;
             pData->pHeader = NULL;
diff --git a/third_party/libfdkaac/LICENSE b/third_party/libfdkaac/LICENSE
new file mode 100644
index 0000000..796c2a2
--- /dev/null
+++ b/third_party/libfdkaac/LICENSE
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+© Copyright  1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+  All rights reserved.
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+of the MPEG specifications.
+Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+individually for the purpose of encoding or decoding bit streams in products that are compliant with
+the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+software may already be covered under those patent licenses when it is used for those licensed purposes only.
+Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+applications information and documentation.
+2.    COPYRIGHT LICENSE
+Redistribution and use in source and binary forms, with or without modification, are permitted without
+payment of copyright license fees provided that you satisfy the following conditions:
+You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+your modifications thereto in source code form.
+You must retain the complete text of this software license in the documentation and/or other materials
+provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+prior written permission.
+You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+software or your modifications thereto.
+Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+and the date of any change. For modified versions of the FDK AAC Codec, the term
+"Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+"Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+3.    NO PATENT LICENSE
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+respect to this software.
+You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+by appropriate patent licenses.
+4.    DISCLAIMER
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+"AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+or business interruption, however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of this software, even if
+advised of the possibility of such damage.
+5.    CONTACT INFORMATION
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------------------------------------- */
diff --git a/third_party/libfdkaac/METADATA b/third_party/libfdkaac/METADATA
new file mode 100644
index 0000000..527f8da
--- /dev/null
+++ b/third_party/libfdkaac/METADATA
@@ -0,0 +1,14 @@
+# Format: google3/devtools/metadata/metadata.proto (go/google3metadata)
+name: "fdk-aac"
+description:
+  "A standalone library of the Fraunhofer FDK AAC code from Android."
+third_party {
+  url {
+    type: GIT
+    value: "https://github.com/mstorsjo/fdk-aac"
+  }
+  version: "2.0.2"
+  last_upgrade_date { year: 2022 month: 10 day: 17 }
+  license_note: "Software License for The Fraunhofer FDK AAC Codec Library for Android"
+  license_type: BY_EXCEPTION_ONLY
+}
diff --git a/third_party/libfdkaac/include/FDK_audio.h b/third_party/libfdkaac/include/FDK_audio.h
new file mode 100644
index 0000000..0e440c9
--- /dev/null
+++ b/third_party/libfdkaac/include/FDK_audio.h
@@ -0,0 +1,813 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright  1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2.    COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3.    NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4.    DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5.    CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+   Author(s):   Manuel Jander
+
+   Description:
+
+*******************************************************************************/
+
+/** \file   FDK_audio.h
+ *  \brief  Global audio struct and constant definitions.
+ */
+
+#ifndef FDK_AUDIO_H
+#define FDK_AUDIO_H
+
+#include "machine_type.h"
+#include "genericStds.h"
+#include "syslib_channelMapDescr.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * File format identifiers.
+ */
+typedef enum {
+  FF_UNKNOWN = -1, /**< Unknown format.        */
+  FF_RAW = 0,      /**< No container, bit stream data conveyed "as is". */
+
+  FF_MP4_3GPP = 3, /**< 3GPP file format.      */
+  FF_MP4_MP4F = 4, /**< MPEG-4 File format.     */
+
+  FF_RAWPACKETS = 5 /**< Proprietary raw packet file. */
+
+} FILE_FORMAT;
+
+/**
+ * Transport type identifiers.
+ */
+typedef enum {
+  TT_UNKNOWN = -1, /**< Unknown format.            */
+  TT_MP4_RAW = 0,  /**< "as is" access units (packet based since there is
+                      obviously no sync layer) */
+  TT_MP4_ADIF = 1, /**< ADIF bitstream format.     */
+  TT_MP4_ADTS = 2, /**< ADTS bitstream format.     */
+
+  TT_MP4_LATM_MCP1 = 6, /**< Audio Mux Elements with muxConfigPresent = 1 */
+  TT_MP4_LATM_MCP0 = 7, /**< Audio Mux Elements with muxConfigPresent = 0, out
+                           of band StreamMuxConfig */
+
+  TT_MP4_LOAS = 10, /**< Audio Sync Stream.         */
+
+  TT_DRM = 12 /**< Digital Radio Mondial (DRM30/DRM+) bitstream format. */
+
+} TRANSPORT_TYPE;
+
+#define TT_IS_PACKET(x)                                                   \
+  (((x) == TT_MP4_RAW) || ((x) == TT_DRM) || ((x) == TT_MP4_LATM_MCP0) || \
+   ((x) == TT_MP4_LATM_MCP1))
+
+/**
+ * Audio Object Type definitions.
+ */
+typedef enum {
+  AOT_NONE = -1,
+  AOT_NULL_OBJECT = 0,
+  AOT_AAC_MAIN = 1, /**< Main profile                              */
+  AOT_AAC_LC = 2,   /**< Low Complexity object                     */
+  AOT_AAC_SSR = 3,
+  AOT_AAC_LTP = 4,
+  AOT_SBR = 5,
+  AOT_AAC_SCAL = 6,
+  AOT_TWIN_VQ = 7,
+  AOT_CELP = 8,
+  AOT_HVXC = 9,
+  AOT_RSVD_10 = 10,          /**< (reserved)                                */
+  AOT_RSVD_11 = 11,          /**< (reserved)                                */
+  AOT_TTSI = 12,             /**< TTSI Object                               */
+  AOT_MAIN_SYNTH = 13,       /**< Main Synthetic object                     */
+  AOT_WAV_TAB_SYNTH = 14,    /**< Wavetable Synthesis object                */
+  AOT_GEN_MIDI = 15,         /**< General MIDI object                       */
+  AOT_ALG_SYNTH_AUD_FX = 16, /**< Algorithmic Synthesis and Audio FX object */
+  AOT_ER_AAC_LC = 17,        /**< Error Resilient(ER) AAC Low Complexity    */
+  AOT_RSVD_18 = 18,          /**< (reserved)                                */
+  AOT_ER_AAC_LTP = 19,       /**< Error Resilient(ER) AAC LTP object        */
+  AOT_ER_AAC_SCAL = 20,      /**< Error Resilient(ER) AAC Scalable object   */
+  AOT_ER_TWIN_VQ = 21,       /**< Error Resilient(ER) TwinVQ object         */
+  AOT_ER_BSAC = 22,          /**< Error Resilient(ER) BSAC object           */
+  AOT_ER_AAC_LD = 23,        /**< Error Resilient(ER) AAC LowDelay object   */
+  AOT_ER_CELP = 24,          /**< Error Resilient(ER) CELP object           */
+  AOT_ER_HVXC = 25,          /**< Error Resilient(ER) HVXC object           */
+  AOT_ER_HILN = 26,          /**< Error Resilient(ER) HILN object           */
+  AOT_ER_PARA = 27,          /**< Error Resilient(ER) Parametric object     */
+  AOT_RSVD_28 = 28,          /**< might become SSC                          */
+  AOT_PS = 29,               /**< PS, Parametric Stereo (includes SBR)      */
+  AOT_MPEGS = 30,            /**< MPEG Surround                             */
+
+  AOT_ESCAPE = 31, /**< Signal AOT uses more than 5 bits          */
+
+  AOT_MP3ONMP4_L1 = 32, /**< MPEG-Layer1 in mp4                        */
+  AOT_MP3ONMP4_L2 = 33, /**< MPEG-Layer2 in mp4                        */
+  AOT_MP3ONMP4_L3 = 34, /**< MPEG-Layer3 in mp4                        */
+  AOT_RSVD_35 = 35,     /**< might become DST                          */
+  AOT_RSVD_36 = 36,     /**< might become ALS                          */
+  AOT_AAC_SLS = 37,     /**< AAC + SLS                                 */
+  AOT_SLS = 38,         /**< SLS                                       */
+  AOT_ER_AAC_ELD = 39,  /**< AAC Enhanced Low Delay                    */
+
+  AOT_USAC = 42,     /**< USAC                                      */
+  AOT_SAOC = 43,     /**< SAOC                                      */
+  AOT_LD_MPEGS = 44, /**< Low Delay MPEG Surround                   */
+
+  /* Pseudo AOTs */
+  AOT_MP2_AAC_LC = 129, /**< Virtual AOT MP2 Low Complexity profile */
+  AOT_MP2_SBR = 132, /**< Virtual AOT MP2 Low Complexity Profile with SBR    */
+
+  AOT_DRM_AAC = 143, /**< Virtual AOT for DRM (ER-AAC-SCAL without SBR) */
+  AOT_DRM_SBR = 144, /**< Virtual AOT for DRM (ER-AAC-SCAL with SBR) */
+  AOT_DRM_MPEG_PS =
+      145, /**< Virtual AOT for DRM (ER-AAC-SCAL with SBR and MPEG-PS) */
+  AOT_DRM_SURROUND =
+      146, /**< Virtual AOT for DRM Surround (ER-AAC-SCAL (+SBR) +MPS) */
+  AOT_DRM_USAC = 147 /**< Virtual AOT for DRM with USAC */
+
+} AUDIO_OBJECT_TYPE;
+
+#define CAN_DO_PS(aot)                                           \
+  ((aot) == AOT_AAC_LC || (aot) == AOT_SBR || (aot) == AOT_PS || \
+   (aot) == AOT_ER_BSAC || (aot) == AOT_DRM_AAC)
+
+#define IS_USAC(aot) ((aot) == AOT_USAC)
+
+#define IS_LOWDELAY(aot) ((aot) == AOT_ER_AAC_LD || (aot) == AOT_ER_AAC_ELD)
+
+/** Channel Mode ( 1-7 equals MPEG channel configurations, others are
+ * arbitrary). */
+typedef enum {
+  MODE_INVALID = -1,
+  MODE_UNKNOWN = 0,
+  MODE_1 = 1,         /**< C */
+  MODE_2 = 2,         /**< L+R */
+  MODE_1_2 = 3,       /**< C, L+R */
+  MODE_1_2_1 = 4,     /**< C, L+R, Rear */
+  MODE_1_2_2 = 5,     /**< C, L+R, LS+RS */
+  MODE_1_2_2_1 = 6,   /**< C, L+R, LS+RS, LFE */
+  MODE_1_2_2_2_1 = 7, /**< C, LC+RC, L+R, LS+RS, LFE */
+
+  MODE_6_1 = 11,           /**< C, L+R, LS+RS, Crear, LFE */
+  MODE_7_1_BACK = 12,      /**< C, L+R, LS+RS, Lrear+Rrear, LFE */
+  MODE_7_1_TOP_FRONT = 14, /**< C, L+R, LS+RS, LFE, Ltop+Rtop */
+
+  MODE_7_1_REAR_SURROUND = 33, /**< C, L+R, LS+RS, Lrear+Rrear, LFE */
+  MODE_7_1_FRONT_CENTER = 34,  /**< C, LC+RC, L+R, LS+RS, LFE */
+
+  MODE_212 = 128 /**< 212 configuration, used in ELDv2 */
+
+} CHANNEL_MODE;
+
+/**
+ * Speaker description tags.
+ * Do not change the enumeration values unless it keeps the following
+ * segmentation:
+ * - Bit 0-3: Horizontal postion (0: none, 1: front, 2: side, 3: back, 4: lfe)
+ * - Bit 4-7: Vertical position (0: normal, 1: top, 2: bottom)
+ */
+typedef enum {
+  ACT_NONE = 0x00,
+  ACT_FRONT = 0x01, /*!< Front speaker position (at normal height) */
+  ACT_SIDE = 0x02,  /*!< Side speaker position (at normal height) */
+  ACT_BACK = 0x03,  /*!< Back speaker position (at normal height) */
+  ACT_LFE = 0x04,   /*!< Low frequency effect speaker postion (front) */
+
+  ACT_TOP =
+      0x10, /*!< Top speaker area (for combination with speaker positions) */
+  ACT_FRONT_TOP = 0x11, /*!< Top front speaker = (ACT_FRONT|ACT_TOP) */
+  ACT_SIDE_TOP = 0x12,  /*!< Top side speaker  = (ACT_SIDE |ACT_TOP) */
+  ACT_BACK_TOP = 0x13,  /*!< Top back speaker  = (ACT_BACK |ACT_TOP) */
+
+  ACT_BOTTOM =
+      0x20, /*!< Bottom speaker area (for combination with speaker positions) */
+  ACT_FRONT_BOTTOM = 0x21, /*!< Bottom front speaker = (ACT_FRONT|ACT_BOTTOM) */
+  ACT_SIDE_BOTTOM = 0x22,  /*!< Bottom side speaker  = (ACT_SIDE |ACT_BOTTOM) */
+  ACT_BACK_BOTTOM = 0x23   /*!< Bottom back speaker  = (ACT_BACK |ACT_BOTTOM) */
+
+} AUDIO_CHANNEL_TYPE;
+
+typedef enum {
+  SIG_UNKNOWN = -1,
+  SIG_IMPLICIT = 0,
+  SIG_EXPLICIT_BW_COMPATIBLE = 1,
+  SIG_EXPLICIT_HIERARCHICAL = 2
+
+} SBR_PS_SIGNALING;
+
+/**
+ * Audio Codec flags.
+ */
+#define AC_ER_VCB11                                                           \
+  0x000001 /*!< aacSectionDataResilienceFlag     flag (from ASC): 1 means use \
+              virtual codebooks  */
+#define AC_ER_RVLC                                                             \
+  0x000002 /*!< aacSpectralDataResilienceFlag     flag (from ASC): 1 means use \
+              huffman codeword reordering */
+#define AC_ER_HCR                                                             \
+  0x000004 /*!< aacSectionDataResilienceFlag     flag (from ASC): 1 means use \
+              virtual codebooks  */
+#define AC_SCALABLE 0x000008    /*!< AAC Scalable*/
+#define AC_ELD 0x000010         /*!< AAC-ELD */
+#define AC_LD 0x000020          /*!< AAC-LD */
+#define AC_ER 0x000040          /*!< ER syntax */
+#define AC_BSAC 0x000080        /*!< BSAC */
+#define AC_USAC 0x000100        /*!< USAC */
+#define AC_RSV603DA 0x000200    /*!< RSVD60 3D audio */
+#define AC_HDAAC 0x000400       /*!< HD-AAC */
+#define AC_RSVD50 0x004000      /*!< Rsvd50 */
+#define AC_SBR_PRESENT 0x008000 /*!< SBR present flag (from ASC) */
+#define AC_SBRCRC \
+  0x010000 /*!< SBR CRC present flag. Only relevant for AAC-ELD for now. */
+#define AC_PS_PRESENT 0x020000 /*!< PS present flag (from ASC or implicit)  */
+#define AC_MPS_PRESENT                                                     \
+  0x040000                    /*!< MPS present flag (from ASC or implicit) \
+                               */
+#define AC_DRM 0x080000       /*!< DRM bit stream syntax */
+#define AC_INDEP 0x100000     /*!< Independency flag */
+#define AC_MPEGD_RES 0x200000 /*!< MPEG-D residual individual channel data. */
+#define AC_SAOC_PRESENT 0x400000   /*!< SAOC Present Flag */
+#define AC_DAB 0x800000            /*!< DAB bit stream syntax */
+#define AC_ELD_DOWNSCALE 0x1000000 /*!< ELD Downscaled playout */
+#define AC_LD_MPS 0x2000000        /*!< Low Delay MPS. */
+#define AC_DRC_PRESENT                                   \
+  0x4000000 /*!< Dynamic Range Control (DRC) data found. \
+             */
+#define AC_USAC_SCFGI3 \
+  0x8000000 /*!< USAC flag: If stereoConfigIndex is 3 the flag is set. */
+/**
+ * Audio Codec flags (reconfiguration).
+ */
+#define AC_CM_DET_CFG_CHANGE                                                 \
+  0x000001 /*!< Config mode signalizes the callback to work in config change \
+              detection mode */
+#define AC_CM_ALLOC_MEM                                               \
+  0x000002 /*!< Config mode signalizes the callback to work in memory \
+              allocation mode */
+
+/**
+ * Audio Codec flags (element specific).
+ */
+#define AC_EL_USAC_TW 0x000001    /*!< USAC time warped filter bank is active */
+#define AC_EL_USAC_NOISE 0x000002 /*!< USAC noise filling is active */
+#define AC_EL_USAC_ITES 0x000004  /*!< USAC SBR inter-TES tool is active */
+#define AC_EL_USAC_PVC \
+  0x000008 /*!< USAC SBR predictive vector coding tool is active */
+#define AC_EL_USAC_MPS212 0x000010 /*!< USAC MPS212 tool is active */
+#define AC_EL_USAC_LFE 0x000020    /*!< USAC element is LFE */
+#define AC_EL_USAC_CP_POSSIBLE                                                 \
+  0x000040 /*!< USAC may use Complex Stereo Prediction in this channel element \
+            */
+#define AC_EL_ENHANCED_NOISE 0x000080   /*!< Enhanced noise filling*/
+#define AC_EL_IGF_AFTER_TNS 0x000100    /*!< IGF after TNS */
+#define AC_EL_IGF_INDEP_TILING 0x000200 /*!< IGF independent tiling */
+#define AC_EL_IGF_USE_ENF 0x000400      /*!< IGF use enhanced noise filling */
+#define AC_EL_FULLBANDLPD 0x000800      /*!< enable fullband LPD tools */
+#define AC_EL_LPDSTEREOIDX 0x001000     /*!< LPD-stereo-tool stereo index */
+#define AC_EL_LFE 0x002000              /*!< The element is of type LFE. */
+
+/* CODER_CONFIG::flags */
+#define CC_MPEG_ID 0x00100000
+#define CC_IS_BASELAYER 0x00200000
+#define CC_PROTECTION 0x00400000
+#define CC_SBR 0x00800000
+#define CC_SBRCRC 0x00010000
+#define CC_SAC 0x00020000
+#define CC_RVLC 0x01000000
+#define CC_VCB11 0x02000000
+#define CC_HCR 0x04000000
+#define CC_PSEUDO_SURROUND 0x08000000
+#define CC_USAC_NOISE 0x10000000
+#define CC_USAC_TW 0x20000000
+#define CC_USAC_HBE 0x40000000
+
+/** Generic audio coder configuration structure. */
+typedef struct {
+  AUDIO_OBJECT_TYPE aot;     /**< Audio Object Type (AOT).           */
+  AUDIO_OBJECT_TYPE extAOT;  /**< Extension Audio Object Type (SBR). */
+  CHANNEL_MODE channelMode;  /**< Channel mode.                      */
+  UCHAR channelConfigZero;   /**< Use channel config zero + pce although a
+                                standard channel config could be signaled. */
+  INT samplingRate;          /**< Sampling rate.                     */
+  INT extSamplingRate;       /**< Extended samplerate (SBR).         */
+  INT downscaleSamplingRate; /**< Downscale sampling rate (ELD downscaled mode)
+                              */
+  INT bitRate;               /**< Average bitrate.                   */
+  int samplesPerFrame; /**< Number of PCM samples per codec frame and audio
+                          channel. */
+  int noChannels;      /**< Number of audio channels.          */
+  int bitsFrame;
+  int nSubFrames; /**< Amount of encoder subframes. 1 means no subframing. */
+  int BSACnumOfSubFrame; /**< The number of the sub-frames which are grouped and
+                            transmitted in a super-frame (BSAC). */
+  int BSAClayerLength; /**< The average length of the large-step layers in bytes
+                          (BSAC).                            */
+  UINT flags;          /**< flags */
+  UCHAR matrixMixdownA; /**< Matrix mixdown index to put into PCE. Default value
+                           0 means no mixdown coefficient, valid values are 1-4
+                           which correspond to matrix_mixdown_idx 0-3. */
+  UCHAR headerPeriod;   /**< Frame period for sending in band configuration
+                           buffers in the transport layer. */
+
+  UCHAR stereoConfigIndex;       /**< USAC MPS stereo mode */
+  UCHAR sbrMode;                 /**< USAC SBR mode */
+  SBR_PS_SIGNALING sbrSignaling; /**< 0: implicit signaling, 1: backwards
+                                    compatible explicit signaling, 2:
+                                    hierarcical explicit signaling */
+
+  UCHAR rawConfig[64]; /**< raw codec specific config as bit stream */
+  int rawConfigBits;   /**< Size of rawConfig in bits */
+
+  UCHAR sbrPresent;
+  UCHAR psPresent;
+} CODER_CONFIG;
+
+#define USAC_ID_BIT 16 /** USAC element IDs start at USAC_ID_BIT */
+
+/** MP4 Element IDs. */
+typedef enum {
+  /* mp4 element IDs */
+  ID_NONE = -1, /**< Invalid Element helper ID.             */
+  ID_SCE = 0,   /**< Single Channel Element.                */
+  ID_CPE = 1,   /**< Channel Pair Element.                  */
+  ID_CCE = 2,   /**< Coupling Channel Element.              */
+  ID_LFE = 3,   /**< LFE Channel Element.                   */
+  ID_DSE = 4,   /**< Currently one Data Stream Element for ancillary data is
+                   supported. */
+  ID_PCE = 5,   /**< Program Config Element.                */
+  ID_FIL = 6,   /**< Fill Element.                          */
+  ID_END = 7,   /**< Arnie (End Element = Terminator).      */
+  ID_EXT = 8,   /**< Extension Payload (ER only).           */
+  ID_SCAL = 9,  /**< AAC scalable element (ER only).        */
+  /* USAC element IDs */
+  ID_USAC_SCE = 0 + USAC_ID_BIT, /**< Single Channel Element.                */
+  ID_USAC_CPE = 1 + USAC_ID_BIT, /**< Channel Pair Element.                  */
+  ID_USAC_LFE = 2 + USAC_ID_BIT, /**< LFE Channel Element.                   */
+  ID_USAC_EXT = 3 + USAC_ID_BIT, /**< Extension Element.                     */
+  ID_USAC_END = 4 + USAC_ID_BIT, /**< Arnie (End Element = Terminator).      */
+  ID_LAST
+} MP4_ELEMENT_ID;
+
+/* usacConfigExtType q.v. ISO/IEC DIS 23008-3 Table 52  and  ISO/IEC FDIS
+ * 23003-3:2011(E) Table 74*/
+typedef enum {
+  /* USAC and RSVD60 3DA */
+  ID_CONFIG_EXT_FILL = 0,
+  /* RSVD60 3DA */
+  ID_CONFIG_EXT_DOWNMIX = 1,
+  ID_CONFIG_EXT_LOUDNESS_INFO = 2,
+  ID_CONFIG_EXT_AUDIOSCENE_INFO = 3,
+  ID_CONFIG_EXT_HOA_MATRIX = 4,
+  ID_CONFIG_EXT_SIG_GROUP_INFO = 6
+  /* 5-127 => reserved for ISO use */
+  /* > 128 => reserved for use outside of ISO scope */
+} CONFIG_EXT_ID;
+
+#define IS_CHANNEL_ELEMENT(elementId)                                         \
+  ((elementId) == ID_SCE || (elementId) == ID_CPE || (elementId) == ID_LFE || \
+   (elementId) == ID_USAC_SCE || (elementId) == ID_USAC_CPE ||                \
+   (elementId) == ID_USAC_LFE)
+
+#define IS_MP4_CHANNEL_ELEMENT(elementId) \
+  ((elementId) == ID_SCE || (elementId) == ID_CPE || (elementId) == ID_LFE)
+
+#define EXT_ID_BITS 4 /**< Size in bits of extension payload type tags. */
+
+/** Extension payload types. */
+typedef enum {
+  EXT_FIL = 0x00,
+  EXT_FILL_DATA = 0x01,
+  EXT_DATA_ELEMENT = 0x02,
+  EXT_DATA_LENGTH = 0x03,
+  EXT_UNI_DRC = 0x04,
+  EXT_LDSAC_DATA = 0x09,
+  EXT_SAOC_DATA = 0x0a,
+  EXT_DYNAMIC_RANGE = 0x0b,
+  EXT_SAC_DATA = 0x0c,
+  EXT_SBR_DATA = 0x0d,
+  EXT_SBR_DATA_CRC = 0x0e
+} EXT_PAYLOAD_TYPE;
+
+#define IS_USAC_CHANNEL_ELEMENT(elementId)                     \
+  ((elementId) == ID_USAC_SCE || (elementId) == ID_USAC_CPE || \
+   (elementId) == ID_USAC_LFE)
+
+/** MPEG-D USAC & RSVD60 3D audio Extension Element Types. */
+typedef enum {
+  /* usac */
+  ID_EXT_ELE_FILL = 0x00,
+  ID_EXT_ELE_MPEGS = 0x01,
+  ID_EXT_ELE_SAOC = 0x02,
+  ID_EXT_ELE_AUDIOPREROLL = 0x03,
+  ID_EXT_ELE_UNI_DRC = 0x04,
+  /* rsv603da */
+  ID_EXT_ELE_OBJ_METADATA = 0x05,
+  ID_EXT_ELE_SAOC_3D = 0x06,
+  ID_EXT_ELE_HOA = 0x07,
+  ID_EXT_ELE_FMT_CNVRTR = 0x08,
+  ID_EXT_ELE_MCT = 0x09,
+  ID_EXT_ELE_ENHANCED_OBJ_METADATA = 0x0d,
+  /* reserved for use outside of ISO scope */
+  ID_EXT_ELE_VR_METADATA = 0x81,
+  ID_EXT_ELE_UNKNOWN = 0xFF
+} USAC_EXT_ELEMENT_TYPE;
+
+/**
+ * Proprietary raw packet file configuration data type identifier.
+ */
+typedef enum {
+  TC_NOTHING = 0,  /* No configuration available -> in-band configuration.   */
+  TC_RAW_ADTS = 2, /* Transfer type is ADTS. */
+  TC_RAW_LATM_MCP1 = 6, /* Transfer type is LATM with SMC present.    */
+  TC_RAW_SDC = 21       /* Configuration data field is Drm SDC.             */
+
+} TP_CONFIG_TYPE;
+
+/*
+ * ##############################################################################################
+ * Library identification and error handling
+ * ##############################################################################################
+ */
+/* \cond */
+
+typedef enum {
+  FDK_NONE = 0,
+  FDK_TOOLS = 1,
+  FDK_SYSLIB = 2,
+  FDK_AACDEC = 3,
+  FDK_AACENC = 4,
+  FDK_SBRDEC = 5,
+  FDK_SBRENC = 6,
+  FDK_TPDEC = 7,
+  FDK_TPENC = 8,
+  FDK_MPSDEC = 9,
+  FDK_MPEGFILEREAD = 10,
+  FDK_MPEGFILEWRITE = 11,
+  FDK_PCMDMX = 31,
+  FDK_MPSENC = 34,
+  FDK_TDLIMIT = 35,
+  FDK_UNIDRCDEC = 38,
+
+  FDK_MODULE_LAST
+
+} FDK_MODULE_ID;
+
+/* AAC capability flags */
+#define CAPF_AAC_LC 0x00000001 /**< Support flag for AAC Low Complexity. */
+#define CAPF_ER_AAC_LD                                                        \
+  0x00000002 /**< Support flag for AAC Low Delay with Error Resilience tools. \
+              */
+#define CAPF_ER_AAC_SCAL 0x00000004 /**< Support flag for AAC Scalable. */
+#define CAPF_ER_AAC_LC                                                      \
+  0x00000008 /**< Support flag for AAC Low Complexity with Error Resilience \
+                tools. */
+#define CAPF_AAC_480 \
+  0x00000010 /**< Support flag for AAC with 480 framelength.  */
+#define CAPF_AAC_512 \
+  0x00000020 /**< Support flag for AAC with 512 framelength.  */
+#define CAPF_AAC_960 \
+  0x00000040 /**< Support flag for AAC with 960 framelength.  */
+#define CAPF_AAC_1024 \
+  0x00000080 /**< Support flag for AAC with 1024 framelength. */
+#define CAPF_AAC_HCR \
+  0x00000100 /**< Support flag for AAC with Huffman Codeword Reordering.    */
+#define CAPF_AAC_VCB11 \
+  0x00000200 /**< Support flag for AAC Virtual Codebook 11.    */
+#define CAPF_AAC_RVLC \
+  0x00000400 /**< Support flag for AAC Reversible Variable Length Coding.   */
+#define CAPF_AAC_MPEG4 0x00000800 /**< Support flag for MPEG file format. */
+#define CAPF_AAC_DRC \
+  0x00001000 /**< Support flag for AAC Dynamic Range Control. */
+#define CAPF_AAC_CONCEALMENT \
+  0x00002000 /**< Support flag for AAC concealment.           */
+#define CAPF_AAC_DRM_BSFORMAT \
+  0x00004000 /**< Support flag for AAC DRM bistream format. */
+#define CAPF_ER_AAC_ELD                                              \
+  0x00008000 /**< Support flag for AAC Enhanced Low Delay with Error \
+                Resilience tools.  */
+#define CAPF_ER_AAC_BSAC \
+  0x00010000 /**< Support flag for AAC BSAC.                           */
+#define CAPF_AAC_ELD_DOWNSCALE \
+  0x00040000 /**< Support flag for AAC-ELD Downscaling           */
+#define CAPF_AAC_USAC_LP \
+  0x00100000 /**< Support flag for USAC low power mode. */
+#define CAPF_AAC_USAC \
+  0x00200000 /**< Support flag for Unified Speech and Audio Coding (USAC). */
+#define CAPF_ER_AAC_ELDV2 \
+  0x00800000 /**< Support flag for AAC Enhanced Low Delay with MPS 212.  */
+#define CAPF_AAC_UNIDRC \
+  0x01000000 /**< Support flag for MPEG-D Dynamic Range Control (uniDrc). */
+
+/* Transport capability flags */
+#define CAPF_ADTS \
+  0x00000001 /**< Support flag for ADTS transport format.        */
+#define CAPF_ADIF \
+  0x00000002 /**< Support flag for ADIF transport format.        */
+#define CAPF_LATM \
+  0x00000004 /**< Support flag for LATM transport format.        */
+#define CAPF_LOAS \
+  0x00000008 /**< Support flag for LOAS transport format.        */
+#define CAPF_RAWPACKETS \
+  0x00000010 /**< Support flag for RAW PACKETS transport format. */
+#define CAPF_DRM \
+  0x00000020 /**< Support flag for DRM/DRM+ transport format.    */
+#define CAPF_RSVD50 \
+  0x00000040 /**< Support flag for RSVD50 transport format       */
+
+/* SBR capability flags */
+#define CAPF_SBR_LP \
+  0x00000001 /**< Support flag for SBR Low Power mode.           */
+#define CAPF_SBR_HQ \
+  0x00000002 /**< Support flag for SBR High Quality mode.        */
+#define CAPF_SBR_DRM_BS \
+  0x00000004 /**< Support flag for                               */
+#define CAPF_SBR_CONCEALMENT \
+  0x00000008 /**< Support flag for SBR concealment.              */
+#define CAPF_SBR_DRC \
+  0x00000010 /**< Support flag for SBR Dynamic Range Control.    */
+#define CAPF_SBR_PS_MPEG \
+  0x00000020 /**< Support flag for MPEG Parametric Stereo.       */
+#define CAPF_SBR_PS_DRM \
+  0x00000040 /**< Support flag for DRM Parametric Stereo.        */
+#define CAPF_SBR_ELD_DOWNSCALE \
+  0x00000080 /**< Support flag for ELD reduced delay mode        */
+#define CAPF_SBR_HBEHQ \
+  0x00000100 /**< Support flag for HQ HBE                        */
+
+/* PCM utils capability flags */
+#define CAPF_DMX_BLIND \
+  0x00000001 /**< Support flag for blind downmixing.             */
+#define CAPF_DMX_PCE                                                      \
+  0x00000002 /**< Support flag for guided downmix with data from MPEG-2/4 \
+                Program Config Elements (PCE). */
+#define CAPF_DMX_ARIB                                                         \
+  0x00000004 /**< Support flag for PCE guided downmix with slightly different \
+                equations and levels to fulfill ARIB standard. */
+#define CAPF_DMX_DVB                                                           \
+  0x00000008 /**< Support flag for guided downmix with data from DVB ancillary \
+                data fields. */
+#define CAPF_DMX_CH_EXP                                                       \
+  0x00000010 /**< Support flag for simple upmixing by dublicating channels or \
+                adding zero channels. */
+#define CAPF_DMX_6_CH                                                   \
+  0x00000020 /**< Support flag for 5.1 channel configuration (input and \
+                output). */
+#define CAPF_DMX_8_CH                                                          \
+  0x00000040 /**< Support flag for 6 and 7.1 channel configurations (input and \
+                output). */
+#define CAPF_DMX_24_CH                                                   \
+  0x00000080 /**< Support flag for 22.2 channel configuration (input and \
+                output). */
+#define CAPF_LIMITER                                      \
+  0x00002000 /**< Support flag for signal level limiting. \
+              */
+
+/* MPEG Surround capability flags */
+#define CAPF_MPS_STD \
+  0x00000001 /**< Support flag for MPEG Surround.           */
+#define CAPF_MPS_LD                                         \
+  0x00000002 /**< Support flag for Low Delay MPEG Surround. \
+              */
+#define CAPF_MPS_USAC \
+  0x00000004 /**< Support flag for USAC MPEG Surround.      */
+#define CAPF_MPS_HQ                                                     \
+  0x00000010 /**< Support flag indicating if high quality processing is \
+                supported */
+#define CAPF_MPS_LP                                                        \
+  0x00000020 /**< Support flag indicating if partially complex (low power) \
+                processing is supported */
+#define CAPF_MPS_BLIND \
+  0x00000040 /**< Support flag indicating if blind processing is supported */
+#define CAPF_MPS_BINAURAL \
+  0x00000080 /**< Support flag indicating if binaural output is possible */
+#define CAPF_MPS_2CH_OUT \
+  0x00000100 /**< Support flag indicating if 2ch output is possible      */
+#define CAPF_MPS_6CH_OUT \
+  0x00000200 /**< Support flag indicating if 6ch output is possible      */
+#define CAPF_MPS_8CH_OUT \
+  0x00000400 /**< Support flag indicating if 8ch output is possible      */
+#define CAPF_MPS_1CH_IN \
+  0x00001000 /**< Support flag indicating if 1ch dmx input is possible   */
+#define CAPF_MPS_2CH_IN \
+  0x00002000 /**< Support flag indicating if 2ch dmx input is possible   */
+#define CAPF_MPS_6CH_IN \
+  0x00004000 /**< Support flag indicating if 5ch dmx input is possible   */
+
+/* \endcond */
+
+/*
+ * ##############################################################################################
+ * Library versioning
+ * ##############################################################################################
+ */
+
+/**
+ * Convert each member of version numbers to one single numeric version
+ * representation.
+ * \param lev0  1st level of version number.
+ * \param lev1  2nd level of version number.
+ * \param lev2  3rd level of version number.
+ */
+#define LIB_VERSION(lev0, lev1, lev2)                      \
+  ((lev0 << 24 & 0xff000000) | (lev1 << 16 & 0x00ff0000) | \
+   (lev2 << 8 & 0x0000ff00))
+
+/**
+ *  Build text string of version.
+ */
+#define LIB_VERSION_STRING(info)                                               \
+  FDKsprintf((info)->versionStr, "%d.%d.%d", (((info)->version >> 24) & 0xff), \
+             (((info)->version >> 16) & 0xff),                                 \
+             (((info)->version >> 8) & 0xff))
+
+/**
+ *  Library information.
+ */
+typedef struct LIB_INFO {
+  const char* title;
+  const char* build_date;
+  const char* build_time;
+  FDK_MODULE_ID module_id;
+  INT version;
+  UINT flags;
+  char versionStr[32];
+} LIB_INFO;
+
+#ifdef __cplusplus
+#define FDK_AUDIO_INLINE inline
+#else
+#define FDK_AUDIO_INLINE
+#endif
+
+/** Initialize library info. */
+static FDK_AUDIO_INLINE void FDKinitLibInfo(LIB_INFO* info) {
+  int i;
+
+  for (i = 0; i < FDK_MODULE_LAST; i++) {
+    info[i].module_id = FDK_NONE;
+  }
+}
+
+/** Aquire supported features of library. */
+static FDK_AUDIO_INLINE UINT
+FDKlibInfo_getCapabilities(const LIB_INFO* info, FDK_MODULE_ID module_id) {
+  int i;
+
+  for (i = 0; i < FDK_MODULE_LAST; i++) {
+    if (info[i].module_id == module_id) {
+      return info[i].flags;
+    }
+  }
+  return 0;
+}
+
+/** Search for next free tab. */
+static FDK_AUDIO_INLINE INT FDKlibInfo_lookup(const LIB_INFO* info,
+                                              FDK_MODULE_ID module_id) {
+  int i = -1;
+
+  for (i = 0; i < FDK_MODULE_LAST; i++) {
+    if (info[i].module_id == module_id) return -1;
+    if (info[i].module_id == FDK_NONE) break;
+  }
+  if (i == FDK_MODULE_LAST) return -1;
+
+  return i;
+}
+
+/*
+ * ##############################################################################################
+ * Buffer description
+ * ##############################################################################################
+ */
+
+/**
+ *  I/O buffer descriptor.
+ */
+typedef struct FDK_bufDescr {
+  void** ppBase;  /*!< Pointer to an array containing buffer base addresses.
+                       Set to NULL for buffer requirement info. */
+  UINT* pBufSize; /*!< Pointer to an array containing the number of elements
+                     that can be placed in the specific buffer. */
+  UINT* pEleSize; /*!< Pointer to an array containing the element size for each
+                     buffer in bytes. That is mostly the number returned by the
+                     sizeof() operator for the data type used for the specific
+                     buffer. */
+  UINT*
+      pBufType; /*!< Pointer to an array of bit fields containing a description
+                     for each buffer. See XXX below for more details.  */
+  UINT numBufs; /*!< Total number of buffers. */
+
+} FDK_bufDescr;
+
+/**
+ * Buffer type description field.
+ */
+#define FDK_BUF_TYPE_MASK_IO ((UINT)0x03 << 30)
+#define FDK_BUF_TYPE_MASK_DESCR ((UINT)0x3F << 16)
+#define FDK_BUF_TYPE_MASK_ID ((UINT)0xFF)
+
+#define FDK_BUF_TYPE_INPUT ((UINT)0x1 << 30)
+#define FDK_BUF_TYPE_OUTPUT ((UINT)0x2 << 30)
+
+#define FDK_BUF_TYPE_PCM_DATA ((UINT)0x1 << 16)
+#define FDK_BUF_TYPE_ANC_DATA ((UINT)0x2 << 16)
+#define FDK_BUF_TYPE_BS_DATA ((UINT)0x4 << 16)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FDK_AUDIO_H */
diff --git a/third_party/libfdkaac/include/aacdecoder_lib.h b/third_party/libfdkaac/include/aacdecoder_lib.h
new file mode 100644
index 0000000..e64ae70
--- /dev/null
+++ b/third_party/libfdkaac/include/aacdecoder_lib.h
@@ -0,0 +1,1083 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright  1995 - 2019 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2.    COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3.    NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4.    DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5.    CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/**************************** AAC decoder library ******************************
+
+   Author(s):   Manuel Jander
+
+   Description:
+
+*******************************************************************************/
+
+#ifndef AACDECODER_LIB_H
+#define AACDECODER_LIB_H
+
+/**
+ * \file   aacdecoder_lib.h
+ * \brief  FDK AAC decoder library interface header file.
+ *
+
+\page INTRO Introduction
+
+
+\section SCOPE Scope
+
+This document describes the high-level application interface and usage of the
+ISO/MPEG-2/4 AAC Decoder library developed by the Fraunhofer Institute for
+Integrated Circuits (IIS). Depending on the library configuration, decoding of
+AAC-LC (Low-Complexity), HE-AAC (High-Efficiency AAC v1 and v2), AAC-LD
+(Low-Delay) and AAC-ELD (Enhanced Low-Delay) is implemented.
+
+All references to SBR (Spectral Band Replication) are only applicable to HE-AAC
+and AAC-ELD configurations of the FDK library. All references to PS (Parametric
+Stereo) are only applicable to HE-AAC v2 decoder configuration of the library.
+
+\section DecoderBasics Decoder Basics
+
+This document can only give a rough overview about the ISO/MPEG-2, ISO/MPEG-4
+AAC audio and MPEG-D USAC coding standards. To understand all details referenced
+in this document, you are encouraged to read the following documents.
+
+- ISO/IEC 13818-7 (MPEG-2 AAC) Standard, defines the syntax of MPEG-2 AAC audio
+bitstreams.
+- ISO/IEC 14496-3 (MPEG-4 AAC, subpart 1 and 4) Standard, defines the syntax of
+MPEG-4 AAC audio bitstreams.
+- ISO/IEC 23003-3 (MPEG-D USAC), defines MPEG-D USAC unified speech and audio
+codec.
+- Lutzky, Schuller, Gayer, Kr&auml;mer, Wabnik, "A guideline to audio codec
+delay", 116th AES Convention, May 8, 2004
+
+In short, MPEG Advanced Audio Coding is based on a time-to-frequency mapping of
+the signal. The signal is partitioned into overlapping time portions and
+transformed into frequency domain. The spectral components are then quantized
+and coded using a highly efficient coding scheme.\n Encoded MPEG-2 and MPEG-4
+AAC audio bitstreams are composed of frames. Contrary to MPEG-1/2 Layer-3 (mp3),
+the length of individual frames is not restricted to a fixed number of bytes,
+but can take any length between 1 and 768 bytes.
+
+In addition to the above mentioned frequency domain coding mode, MPEG-D USAC
+also employs a time domain Algebraic Code-Excited Linear Prediction (ACELP)
+speech coder core. This operating mode is selected by the encoder in order to
+achieve the optimum audio quality for different content type. Several
+enhancements allow achieving higher quality at lower bit rates compared to
+MPEG-4 HE-AAC.
+
+
+\page LIBUSE Library Usage
+
+
+\section InterfaceDescritpion API Description
+
+All API header files are located in the folder /include of the release package.
+The contents of each file is described in detail in this document. All header
+files are provided for usage in specific C/C++ programs. The main AAC decoder
+library API functions are located in aacdecoder_lib.h header file.
+
+
+\section Calling_Sequence Calling Sequence
+
+The following sequence is necessary for proper decoding of ISO/MPEG-2/4 AAC,
+HE-AAC v2, or MPEG-D USAC bitstreams. In the following description, input stream
+read and output write function details are left out, since they may be
+implemented in a variety of configurations depending on the user's specific
+requirements.
+
+
+-# Call aacDecoder_Open() to open and retrieve a handle to a new AAC decoder
+instance. \code aacDecoderInfo = aacDecoder_Open(transportType, nrOfLayers);
+\endcode
+-# If out-of-band config data (Audio Specific Config (ASC) or Stream Mux Config
+(SMC)) is available, call aacDecoder_ConfigRaw() to pass this data to the
+decoder before beginning the decoding process. If this data is not available in
+advance, the decoder will configure itself while decoding, during the
+aacDecoder_DecodeFrame() function call.
+-# Begin decoding loop.
+\code
+do {
+\endcode
+-# Read data from bitstream file or stream buffer in to the driver program
+working memory (a client-supplied input buffer "inBuffer" in framework). This
+buffer will be used to load AAC bitstream data to the decoder.  Only when all
+data in this buffer has been processed will the decoder signal an empty buffer.
+-# Call aacDecoder_Fill() to fill the decoder's internal bitstream input buffer
+with the client-supplied bitstream input buffer. Note, if the data loaded in to
+the internal buffer is not sufficient to decode a frame,
+aacDecoder_DecodeFrame() will return ::AAC_DEC_NOT_ENOUGH_BITS until a
+sufficient amount of data is loaded in to the internal buffer. For streaming
+formats (ADTS, LOAS), it is acceptable to load more than one frame to the
+decoder. However, for packed based formats, only one frame may be loaded to the
+decoder per aacDecoder_DecodeFrame() call. For least amount of communication
+delay, fill and decode should be performed on a frame by frame basis. \code
+    ErrorStatus = aacDecoder_Fill(aacDecoderInfo, inBuffer, bytesRead,
+bytesValid); \endcode
+-# Call aacDecoder_DecodeFrame(). This function decodes one frame and writes
+decoded PCM audio data to a client-supplied buffer. It is the client's
+responsibility to allocate a buffer which is large enough to hold the decoded
+output data. \code ErrorStatus = aacDecoder_DecodeFrame(aacDecoderInfo,
+TimeData, OUT_BUF_SIZE, flags); \endcode If the bitstream configuration (number
+of channels, sample rate, frame size) is not known a priori, you may call
+aacDecoder_GetStreamInfo() to retrieve a structure that contains this
+information. You may use this data to initialize an audio output device. \code
+    p_si = aacDecoder_GetStreamInfo(aacDecoderInfo);
+\endcode
+-# Repeat steps 5 to 7 until no data is available to decode any more, or in case
+of error. \code } while (bytesRead[0] > 0 || doFlush || doBsFlush ||
+forceContinue); \endcode
+-# Call aacDecoder_Close() to de-allocate all AAC decoder and transport layer
+structures. \code aacDecoder_Close(aacDecoderInfo); \endcode
+
+\image latex decode.png "Decode calling sequence" width=11cm
+
+\image latex change_source.png "Change data source sequence" width=5cm
+
+\image latex conceal.png "Error concealment sequence" width=14cm
+
+\subsection Error_Concealment_Sequence Error Concealment Sequence
+
+There are different strategies to handle bit stream errors. Depending on the
+system properties the product designer might choose to take different actions in
+case a bit error occurs. In many cases the decoder might be able to do
+reasonable error concealment without the need of any additional actions from the
+system. But in some cases its not even possible to know how many decoded PCM
+output samples are required to fill the gap due to the data error, then the
+software surrounding the decoder must deal with the situation. The most simple
+way would be to just stop audio playback and resume once enough bit stream data
+and/or buffered output samples are available. More sophisticated designs might
+also be able to deal with sender/receiver clock drifts or data drop outs by
+using a closed loop control of FIFO fulness levels. The chosen strategy depends
+on the final product requirements.
+
+The error concealment sequence diagram illustrates the general execution paths
+for error handling.
+
+The macro IS_OUTPUT_VALID(err) can be used to identify if the audio output
+buffer contains valid audio either from error free bit stream data or successful
+error concealment. In case the result is false, the decoder output buffer does
+not contain meaningful audio samples and should not be passed to any output as
+it is. Most likely in case that a continuous audio output PCM stream is
+required, the output buffer must be filled with audio data from the calling
+framework. This might be e.g. an appropriate number of samples all zero.
+
+If error code ::AAC_DEC_TRANSPORT_SYNC_ERROR is returned by the decoder, under
+some particular conditions it is possible to estimate lost frames due to the bit
+stream error. In that case the bit stream is required to have a constant
+bitrate, and compatible transport type. Audio samples for the lost frames can be
+obtained by calling aacDecoder_DecodeFrame() with flag ::AACDEC_CONCEAL set
+n-times where n is the count of lost frames. Please note that the decoder has to
+have encountered valid configuration data at least once to be able to generate
+concealed data, because at the minimum the sampling rate, frame size and amount
+of audio channels needs to be known.
+
+If it is not possible to get an estimation of lost frames then a constant
+fullness of the audio output buffer can be achieved by implementing different
+FIFO control techniques e.g. just stop taking of samples from the buffer to
+avoid underflow or stop filling new data to the buffer to avoid overflow. But
+this techniques are out of scope of this document.
+
+For a detailed description of a specific error code please refer also to
+::AAC_DECODER_ERROR.
+
+\section BufferSystem Buffer System
+
+There are three main buffers in an AAC decoder application. One external input
+buffer to hold bitstream data from file I/O or elsewhere, one decoder-internal
+input buffer, and one to hold the decoded output PCM sample data. In resource
+limited applications, the output buffer may be reused as an external input
+buffer prior to the subsequence aacDecoder_Fill() function call.
+
+To feed the data to the decoder-internal input buffer, use the
+function aacDecoder_Fill(). This function returns important information
+regarding the number of bytes in the external input buffer that have not yet
+been copied into the internal input buffer (variable bytesValid). Once the
+external buffer has been fully copied, it can be completely re-filled again. In
+case you wish to refill the buffer while there are unprocessed bytes (bytesValid
+is unequal 0), you should preserve the unconsumed data. However, we recommend to
+refill the buffer only when bytesValid returns 0.
+
+The bytesValid parameter is an input and output parameter to the FDK decoder. As
+an input, it signals how many valid bytes are available in the external buffer.
+After consumption of the external buffer using aacDecoder_Fill() function, the
+bytesValid parameter indicates if any of the bytes in the external buffer were
+not consumed.
+
+\image latex dec_buffer.png "Life cycle of the external input buffer" width=9cm
+
+\page OutputFormat Decoder audio output
+
+\section OutputFormatObtaining Obtaining channel mapping information
+
+The decoded audio output format is indicated by a set of variables of the
+CStreamInfo structure. While the struct members sampleRate, frameSize and
+numChannels might be self explanatory, pChannelType and pChannelIndices require
+some further explanation.
+
+These two arrays indicate the configuration of channel data within the output
+buffer. Both arrays have CStreamInfo::numChannels number of cells. Each cell of
+pChannelType indicates the channel type, which is described in the enum
+::AUDIO_CHANNEL_TYPE (defined in FDK_audio.h). The cells of pChannelIndices
+indicate the sub index among the channels starting with 0 among channels of the
+same audio channel type.
+
+The indexing scheme is structured as defined in MPEG-2/4 Standards. Indices
+start from the front direction (a center channel if available, will always be
+index 0) and increment, starting with the left side, pairwise (e.g. L, R) and
+from front to back (Front L, Front R, Surround L, Surround R). For detailed
+explanation, please refer to ISO/IEC 13818-7:2005(E), chapter 8.5.3.2.
+
+In case a Program Config is included in the audio configuration, the channel
+mapping described within it will be adopted.
+
+The examples below explain these aspects in detail.
+
+\section OutputFormatChange Changing the audio output format
+
+For MPEG-4 audio the channel order can be changed at runtime through the
+parameter
+::AAC_PCM_OUTPUT_CHANNEL_MAPPING. See the description of those
+parameters and the decoder library function aacDecoder_SetParam() for more
+detail.
+
+\section OutputFormatExample Channel mapping examples
+
+The following examples illustrate the location of individual audio samples in
+the audio buffer that is passed to aacDecoder_DecodeFrame() and the expected
+data in the CStreamInfo structure which can be obtained by calling
+aacDecoder_GetStreamInfo().
+
+\subsection ExamplesStereo Stereo
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+a AAC-LC bit stream which has channelConfiguration = 2 in its audio specific
+config would lead to the following values in CStreamInfo:
+
+CStreamInfo::numChannels = 2
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT }
+
+CStreamInfo::pChannelIndices = { 0, 1 }
+
+The output buffer will be formatted as follows:
+
+\verbatim
+  <left sample 0>  <left sample 1>  <left sample 2>  ... <left sample N>
+  <right sample 0> <right sample 1> <right sample 2> ... <right sample N>
+\endverbatim
+
+Where N equals to CStreamInfo::frameSize .
+
+\subsection ExamplesSurround Surround 5.1
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+a AAC-LC bit stream which has channelConfiguration = 6 in its audio specific
+config, would lead to the following values in CStreamInfo:
+
+CStreamInfo::numChannels = 6
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT, ::ACT_FRONT, ::ACT_LFE,
+::ACT_BACK, ::ACT_BACK }
+
+CStreamInfo::pChannelIndices = { 1, 2, 0, 0, 0, 1 }
+
+Since ::AAC_PCM_OUTPUT_CHANNEL_MAPPING is 1, WAV file channel ordering will be
+used. For a 5.1 channel scheme, thus the channels would be: front left, front
+right, center, LFE, surround left, surround right. Thus the third channel is the
+center channel, receiving the index 0. The other front channels are front left,
+front right being placed as first and second channels with indices 1 and 2
+correspondingly. There is only one LFE, placed as the fourth channel and index
+0. Finally both surround channels get the type definition ACT_BACK, and the
+indices 0 and 1.
+
+The output buffer will be formatted as follows:
+
+\verbatim
+<front left sample 0> <front right sample 0>
+<center sample 0> <LFE sample 0>
+<surround left sample 0> <surround right sample 0>
+
+<front left sample 1> <front right sample 1>
+<center sample 1> <LFE sample 1>
+<surround left sample 1> <surround right sample 1>
+
+...
+
+<front left sample N> <front right sample N>
+<center sample N> <LFE sample N>
+<surround left sample N> <surround right sample N>
+\endverbatim
+
+Where N equals to CStreamInfo::frameSize .
+
+\subsection ExamplesArib ARIB coding mode 2/1
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+in case of a ARIB bit stream using coding mode 2/1 as described in ARIB STD-B32
+Part 2 Version 2.1-E1, page 61, would lead to the following values in
+CStreamInfo:
+
+CStreamInfo::numChannels = 3
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT, ::ACT_BACK }
+
+CStreamInfo::pChannelIndices = { 0, 1, 0 }
+
+The audio channels will be placed as follows in the audio output buffer:
+
+\verbatim
+<front left sample 0> <front right sample 0>  <mid surround sample 0>
+
+<front left sample 1> <front right sample 1> <mid surround sample 1>
+
+...
+
+<front left sample N> <front right sample N> <mid surround sample N>
+
+Where N equals to CStreamInfo::frameSize .
+
+\endverbatim
+
+*/
+
+#include "machine_type.h"
+#include "FDK_audio.h"
+
+#include "genericStds.h"
+
+#define AACDECODER_LIB_VL0 3
+#define AACDECODER_LIB_VL1 2
+#define AACDECODER_LIB_VL2 0
+
+/**
+ * \brief  AAC decoder error codes.
+ */
+typedef enum {
+  AAC_DEC_OK =
+      0x0000, /*!< No error occurred. Output buffer is valid and error free. */
+  AAC_DEC_OUT_OF_MEMORY =
+      0x0002, /*!< Heap returned NULL pointer. Output buffer is invalid. */
+  AAC_DEC_UNKNOWN =
+      0x0005, /*!< Error condition is of unknown reason, or from a another
+                 module. Output buffer is invalid. */
+
+  /* Synchronization errors. Output buffer is invalid. */
+  aac_dec_sync_error_start = 0x1000,
+  AAC_DEC_TRANSPORT_SYNC_ERROR = 0x1001, /*!< The transport decoder had
+                                            synchronization problems. Do not
+                                            exit decoding. Just feed new
+                                              bitstream data. */
+  AAC_DEC_NOT_ENOUGH_BITS = 0x1002, /*!< The input buffer ran out of bits. */
+  aac_dec_sync_error_end = 0x1FFF,
+
+  /* Initialization errors. Output buffer is invalid. */
+  aac_dec_init_error_start = 0x2000,
+  AAC_DEC_INVALID_HANDLE =
+      0x2001, /*!< The handle passed to the function call was invalid (NULL). */
+  AAC_DEC_UNSUPPORTED_AOT =
+      0x2002, /*!< The AOT found in the configuration is not supported. */
+  AAC_DEC_UNSUPPORTED_FORMAT =
+      0x2003, /*!< The bitstream format is not supported.  */
+  AAC_DEC_UNSUPPORTED_ER_FORMAT =
+      0x2004, /*!< The error resilience tool format is not supported. */
+  AAC_DEC_UNSUPPORTED_EPCONFIG =
+      0x2005, /*!< The error protection format is not supported. */
+  AAC_DEC_UNSUPPORTED_MULTILAYER =
+      0x2006, /*!< More than one layer for AAC scalable is not supported. */
+  AAC_DEC_UNSUPPORTED_CHANNELCONFIG =
+      0x2007, /*!< The channel configuration (either number or arrangement) is
+                 not supported. */
+  AAC_DEC_UNSUPPORTED_SAMPLINGRATE = 0x2008, /*!< The sample rate specified in
+                                                the configuration is not
+                                                supported. */
+  AAC_DEC_INVALID_SBR_CONFIG =
+      0x2009, /*!< The SBR configuration is not supported. */
+  AAC_DEC_SET_PARAM_FAIL = 0x200A,  /*!< The parameter could not be set. Either
+                                       the value was out of range or the
+                                       parameter does  not exist. */
+  AAC_DEC_NEED_TO_RESTART = 0x200B, /*!< The decoder needs to be restarted,
+                                       since the required configuration change
+                                       cannot be performed. */
+  AAC_DEC_OUTPUT_BUFFER_TOO_SMALL =
+      0x200C, /*!< The provided output buffer is too small. */
+  aac_dec_init_error_end = 0x2FFF,
+
+  /* Decode errors. Output buffer is valid but concealed. */
+  aac_dec_decode_error_start = 0x4000,
+  AAC_DEC_TRANSPORT_ERROR =
+      0x4001, /*!< The transport decoder encountered an unexpected error. */
+  AAC_DEC_PARSE_ERROR = 0x4002, /*!< Error while parsing the bitstream. Most
+                                   probably it is corrupted, or the system
+                                   crashed. */
+  AAC_DEC_UNSUPPORTED_EXTENSION_PAYLOAD =
+      0x4003, /*!< Error while parsing the extension payload of the bitstream.
+                 The extension payload type found is not supported. */
+  AAC_DEC_DECODE_FRAME_ERROR = 0x4004, /*!< The parsed bitstream value is out of
+                                          range. Most probably the bitstream is
+                                          corrupt, or the system crashed. */
+  AAC_DEC_CRC_ERROR = 0x4005,          /*!< The embedded CRC did not match. */
+  AAC_DEC_INVALID_CODE_BOOK = 0x4006,  /*!< An invalid codebook was signaled.
+                                          Most probably the bitstream is corrupt,
+                                          or the system  crashed. */
+  AAC_DEC_UNSUPPORTED_PREDICTION =
+      0x4007, /*!< Predictor found, but not supported in the AAC Low Complexity
+                 profile. Most probably the bitstream is corrupt, or has a wrong
+                 format. */
+  AAC_DEC_UNSUPPORTED_CCE = 0x4008, /*!< A CCE element was found which is not
+                                       supported. Most probably the bitstream is
+                                       corrupt, or has a wrong format. */
+  AAC_DEC_UNSUPPORTED_LFE = 0x4009, /*!< A LFE element was found which is not
+                                       supported. Most probably the bitstream is
+                                       corrupt, or has a wrong format. */
+  AAC_DEC_UNSUPPORTED_GAIN_CONTROL_DATA =
+      0x400A, /*!< Gain control data found but not supported. Most probably the
+                 bitstream is corrupt, or has a wrong format. */
+  AAC_DEC_UNSUPPORTED_SBA =
+      0x400B, /*!< SBA found, but currently not supported in the BSAC profile.
+               */
+  AAC_DEC_TNS_READ_ERROR = 0x400C, /*!< Error while reading TNS data. Most
+                                      probably the bitstream is corrupt or the
+                                      system crashed. */
+  AAC_DEC_RVLC_ERROR =
+      0x400D, /*!< Error while decoding error resilient data. */
+  aac_dec_decode_error_end = 0x4FFF,
+  /* Ancillary data errors. Output buffer is valid. */
+  aac_dec_anc_data_error_start = 0x8000,
+  AAC_DEC_ANC_DATA_ERROR =
+      0x8001, /*!< Non severe error concerning the ancillary data handling. */
+  AAC_DEC_TOO_SMALL_ANC_BUFFER = 0x8002,  /*!< The registered ancillary data
+                                             buffer is too small to receive the
+                                             parsed data. */
+  AAC_DEC_TOO_MANY_ANC_ELEMENTS = 0x8003, /*!< More than the allowed number of
+                                             ancillary data elements should be
+                                             written to buffer. */
+  aac_dec_anc_data_error_end = 0x8FFF
+
+} AAC_DECODER_ERROR;
+
+/** Macro to identify initialization errors. Output buffer is invalid. */
+#define IS_INIT_ERROR(err)                                                    \
+  ((((err) >= aac_dec_init_error_start) && ((err) <= aac_dec_init_error_end)) \
+       ? 1                                                                    \
+       : 0)
+/** Macro to identify decode errors. Output buffer is valid but concealed. */
+#define IS_DECODE_ERROR(err)                 \
+  ((((err) >= aac_dec_decode_error_start) && \
+    ((err) <= aac_dec_decode_error_end))     \
+       ? 1                                   \
+       : 0)
+/**
+ * Macro to identify if the audio output buffer contains valid samples after
+ * calling aacDecoder_DecodeFrame(). Output buffer is valid but can be
+ * concealed.
+ */
+#define IS_OUTPUT_VALID(err) (((err) == AAC_DEC_OK) || IS_DECODE_ERROR(err))
+
+/*! \enum  AAC_MD_PROFILE
+ *  \brief The available metadata profiles which are mostly related to downmixing. The values define the arguments
+ *         for the use with parameter ::AAC_METADATA_PROFILE.
+ */
+typedef enum {
+  AAC_MD_PROFILE_MPEG_STANDARD =
+      0, /*!< The standard profile creates a mixdown signal based on the
+            advanced downmix metadata (from a DSE). The equations and default
+            values are defined in ISO/IEC 14496:3 Ammendment 4. Any other
+            (legacy) downmix metadata will be ignored. No other parameter will
+            be modified.         */
+  AAC_MD_PROFILE_MPEG_LEGACY =
+      1, /*!< This profile behaves identical to the standard profile if advanced
+              downmix metadata (from a DSE) is available. If not, the
+            matrix_mixdown information embedded in the program configuration
+            element (PCE) will be applied. If neither is the case, the module
+            creates a mixdown using the default coefficients as defined in
+            ISO/IEC 14496:3 AMD 4. The profile can be used to support legacy
+            digital TV (e.g. DVB) streams.           */
+  AAC_MD_PROFILE_MPEG_LEGACY_PRIO =
+      2, /*!< Similar to the ::AAC_MD_PROFILE_MPEG_LEGACY profile but if both
+            the advanced (ISO/IEC 14496:3 AMD 4) and the legacy (PCE) MPEG
+            downmix metadata are available the latter will be applied.
+          */
+  AAC_MD_PROFILE_ARIB_JAPAN =
+      3 /*!< Downmix creation as described in ABNT NBR 15602-2. But if advanced
+             downmix metadata (ISO/IEC 14496:3 AMD 4) is available it will be
+             preferred because of the higher resolutions. In addition the
+           metadata expiry time will be set to the value defined in the ARIB
+           standard (see ::AAC_METADATA_EXPIRY_TIME).
+         */
+} AAC_MD_PROFILE;
+
+/*! \enum  AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS
+ *  \brief Options for handling of DRC parameters, if presentation mode is not indicated in bitstream
+ */
+typedef enum {
+  AAC_DRC_PARAMETER_HANDLING_DISABLED = -1, /*!< DRC parameter handling
+                                               disabled, all parameters are
+                                               applied as requested. */
+  AAC_DRC_PARAMETER_HANDLING_ENABLED =
+      0, /*!< Apply changes to requested DRC parameters to prevent clipping. */
+  AAC_DRC_PRESENTATION_MODE_1_DEFAULT =
+      1, /*!< Use DRC presentation mode 1 as default (e.g. for Nordig) */
+  AAC_DRC_PRESENTATION_MODE_2_DEFAULT =
+      2 /*!< Use DRC presentation mode 2 as default (e.g. for DTG DBook) */
+} AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS;
+
+/**
+ * \brief AAC decoder setting parameters
+ */
+typedef enum {
+  AAC_PCM_DUAL_CHANNEL_OUTPUT_MODE =
+      0x0002, /*!< Defines how the decoder processes two channel signals: \n
+                   0: Leave both signals as they are (default). \n
+                   1: Create a dual mono output signal from channel 1. \n
+                   2: Create a dual mono output signal from channel 2. \n
+                   3: Create a dual mono output signal by mixing both channels
+                 (L' = R' = 0.5*Ch1 + 0.5*Ch2). */
+  AAC_PCM_OUTPUT_CHANNEL_MAPPING =
+      0x0003, /*!< Output buffer channel ordering. 0: MPEG PCE style order, 1:
+                 WAV file channel order (default). */
+  AAC_PCM_LIMITER_ENABLE =
+      0x0004,                           /*!< Enable signal level limiting. \n
+                                             -1: Auto-config. Enable limiter for all
+                                           non-lowdelay configurations by default. \n
+                                              0: Disable limiter in general. \n
+                                              1: Enable limiter always.
+                                             It is recommended to call the decoder
+                                           with a AACDEC_CLRHIST flag to reset all
+                                           states when      the limiter switch is changed
+                                           explicitly. */
+  AAC_PCM_LIMITER_ATTACK_TIME = 0x0005, /*!< Signal level limiting attack time
+                                           in ms. Default configuration is 15
+                                           ms. Adjustable range from 1 ms to 15
+                                           ms. */
+  AAC_PCM_LIMITER_RELEAS_TIME = 0x0006, /*!< Signal level limiting release time
+                                           in ms. Default configuration is 50
+                                           ms. Adjustable time must be larger
+                                           than 0 ms. */
+  AAC_PCM_MIN_OUTPUT_CHANNELS =
+      0x0011, /*!< Minimum number of PCM output channels. If higher than the
+                 number of encoded audio channels, a simple channel extension is
+                 applied (see note 4 for exceptions). \n -1, 0: Disable channel
+                 extension feature. The decoder output contains the same number
+                 of channels as the encoded bitstream. \n 1:    This value is
+                 currently needed only together with the mix-down feature. See
+                          ::AAC_PCM_MAX_OUTPUT_CHANNELS and note 2 below. \n
+                    2:    Encoded mono signals will be duplicated to achieve a
+                 2/0/0.0 channel output configuration. \n 6:    The decoder
+                 tries to reorder encoded signals with less than six channels to
+                 achieve a 3/0/2.1 channel output signal. Missing channels will
+                 be filled with a zero signal. If reordering is not possible the
+                 empty channels will simply be appended. Only available if
+                 instance is configured to support multichannel output. \n 8:
+                 The decoder tries to reorder encoded signals with less than
+                 eight channels to achieve a 3/0/4.1 channel output signal.
+                 Missing channels will be filled with a zero signal. If
+                 reordering is not possible the empty channels will simply be
+                          appended. Only available if instance is configured to
+                 support multichannel output.\n NOTE: \n
+                     1. The channel signaling (CStreamInfo::pChannelType and
+                 CStreamInfo::pChannelIndices) will not be modified. Added empty
+                 channels will be signaled with channel type
+                        AUDIO_CHANNEL_TYPE::ACT_NONE. \n
+                     2. If the parameter value is greater than that of
+                 ::AAC_PCM_MAX_OUTPUT_CHANNELS both will be set to the same
+                 value. \n
+                     3. This parameter will be ignored if the number of encoded
+                 audio channels is greater than 8. */
+  AAC_PCM_MAX_OUTPUT_CHANNELS =
+      0x0012, /*!< Maximum number of PCM output channels. If lower than the
+                 number of encoded audio channels, downmixing is applied
+                 accordingly (see note 5 for exceptions). If dedicated metadata
+                 is available in the stream it will be used to achieve better
+                 mixing results. \n -1, 0: Disable downmixing feature. The
+                 decoder output contains the same number of channels as the
+                 encoded bitstream. \n 1:    All encoded audio configurations
+                 with more than one channel will be mixed down to one mono
+                 output signal. \n 2:    The decoder performs a stereo mix-down
+                 if the number encoded audio channels is greater than two. \n 6:
+                 If the number of encoded audio channels is greater than six the
+                 decoder performs a mix-down to meet the target output
+                 configuration of 3/0/2.1 channels. Only available if instance
+                 is configured to support multichannel output. \n 8:    This
+                 value is currently needed only together with the channel
+                 extension feature. See ::AAC_PCM_MIN_OUTPUT_CHANNELS and note 2
+                 below. Only available if instance is configured to support
+                 multichannel output. \n NOTE: \n
+                     1. Down-mixing of any seven or eight channel configuration
+                 not defined in ISO/IEC 14496-3 PDAM 4 is not supported by this
+                 software version. \n
+                     2. If the parameter value is greater than zero but smaller
+                 than ::AAC_PCM_MIN_OUTPUT_CHANNELS both will be set to same
+                 value. \n
+                     3. This parameter will be ignored if the number of encoded
+                 audio channels is greater than 8. */
+  AAC_METADATA_PROFILE =
+      0x0020, /*!< See ::AAC_MD_PROFILE for all available values. */
+  AAC_METADATA_EXPIRY_TIME = 0x0021, /*!< Defines the time in ms after which all
+                                        the bitstream associated meta-data (DRC,
+                                        downmix coefficients, ...) will be reset
+                                        to default if no update has been
+                                        received. Negative values disable the
+                                        feature. */
+
+  AAC_CONCEAL_METHOD = 0x0100, /*!< Error concealment: Processing method. \n
+                                    0: Spectral muting. \n
+                                    1: Noise substitution (see ::CONCEAL_NOISE).
+                                  \n 2: Energy interpolation (adds additional
+                                  signal delay of one frame, see
+                                  ::CONCEAL_INTER. only some AOTs are
+                                  supported). \n */
+  AAC_DRC_BOOST_FACTOR =
+      0x0200, /*!< MPEG-4 / MPEG-D Dynamic Range Control (DRC): Scaling factor
+                 for boosting gain values. Defines how the boosting DRC factors
+                 (conveyed in the bitstream) will be applied to the decoded
+                 signal. The valid values range from 0 (don't apply boost
+                 factors) to 127 (fully apply boost factors). Default value is 0
+                 for MPEG-4 DRC and 127 for MPEG-D DRC. */
+  AAC_DRC_ATTENUATION_FACTOR = 0x0201, /*!< MPEG-4 / MPEG-D DRC: Scaling factor
+                                          for attenuating gain values. Same as
+                                            ::AAC_DRC_BOOST_FACTOR but for
+                                          attenuating DRC factors. */
+  AAC_DRC_REFERENCE_LEVEL =
+      0x0202, /*!< MPEG-4 / MPEG-D DRC: Target reference level / decoder target
+                 loudness.\n Defines the level below full-scale (quantized in
+                 steps of 0.25dB) to which the output audio signal will be
+                 normalized to by the DRC module.\n The parameter controls
+                 loudness normalization for both MPEG-4 DRC and MPEG-D DRC. The
+                 valid values range from 40 (-10 dBFS) to 127 (-31.75 dBFS).\n
+                   Example values:\n
+                   124 (-31 dBFS) for audio/video receivers (AVR) or other
+                 devices allowing audio playback with high dynamic range,\n 96
+                 (-24 dBFS) for TV sets or equivalent devices (default),\n 64
+                 (-16 dBFS) for mobile devices where the dynamic range of audio
+                 playback is restricted.\n Any value smaller than 0 switches off
+                 loudness normalization and MPEG-4 DRC. */
+  AAC_DRC_HEAVY_COMPRESSION =
+      0x0203, /*!< MPEG-4 DRC: En-/Disable DVB specific heavy compression (aka
+                 RF mode). If set to 1, the decoder will apply the compression
+                 values from the DVB specific ancillary data field. At the same
+                 time the MPEG-4 Dynamic Range Control tool will be disabled. By
+                   default, heavy compression is disabled. */
+  AAC_DRC_DEFAULT_PRESENTATION_MODE =
+      0x0204, /*!< MPEG-4 DRC: Default presentation mode (DRC parameter
+                 handling). \n Defines the handling of the DRC parameters boost
+                 factor, attenuation factor and heavy compression, if no
+                 presentation mode is indicated in the bitstream.\n For options,
+                 see ::AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS.\n Default:
+                 ::AAC_DRC_PARAMETER_HANDLING_DISABLED */
+  AAC_DRC_ENC_TARGET_LEVEL =
+      0x0205, /*!< MPEG-4 DRC: Encoder target level for light (i.e. not heavy)
+                 compression.\n If known, this declares the target reference
+                 level that was assumed at the encoder for calculation of
+                 limiting gains. The valid values range from 0 (full-scale) to
+                 127 (31.75 dB below full-scale). This parameter is used only
+                 with ::AAC_DRC_PARAMETER_HANDLING_ENABLED and ignored
+                 otherwise.\n Default: 127 (worst-case assumption).\n */
+  AAC_UNIDRC_SET_EFFECT = 0x0206, /*!< MPEG-D DRC: Request a DRC effect type for
+                                     selection of a DRC set.\n Supported indices
+                                     are:\n -1: DRC off. Completely disables
+                                     MPEG-D DRC.\n 0: None (default). Disables
+                                     MPEG-D DRC, but automatically enables DRC
+                                     if necessary to prevent clipping.\n 1: Late
+                                     night\n 2: Noisy environment\n 3: Limited
+                                     playback range\n 4: Low playback level\n 5:
+                                     Dialog enhancement\n 6: General
+                                     compression. Used for generally enabling
+                                     MPEG-D DRC without particular request.\n */
+  AAC_UNIDRC_ALBUM_MODE =
+      0x0207, /*!<  MPEG-D DRC: Enable album mode. 0: Disabled (default), 1:
+                 Enabled.\n Disabled album mode leads to application of gain
+                 sequences for fading in and out, if provided in the
+                 bitstream.\n Enabled album mode makes use of dedicated album
+                 loudness information, if provided in the bitstream.\n */
+  AAC_QMF_LOWPOWER =
+      0x0300, /*!< Quadrature Mirror Filter (QMF) Bank processing mode. \n
+                   -1: Use internal default. \n
+                    0: Use complex QMF data mode. \n
+                    1: Use real (low power) QMF data mode. \n */
+  AAC_TPDEC_CLEAR_BUFFER =
+      0x0603 /*!< Clear internal bit stream buffer of transport layers. The
+                decoder will start decoding at new data passed after this event
+                and any previous data is discarded. */
+
+} AACDEC_PARAM;
+
+/**
+ * \brief This structure gives information about the currently decoded audio
+ * data. All fields are read-only.
+ */
+typedef struct {
+  /* These five members are the only really relevant ones for the user. */
+  INT sampleRate; /*!< The sample rate in Hz of the decoded PCM audio signal. */
+  INT frameSize;  /*!< The frame size of the decoded PCM audio signal. \n
+                       Typically this is: \n
+                       1024 or 960 for AAC-LC \n
+                       2048 or 1920 for HE-AAC (v2) \n
+                       512 or 480 for AAC-LD and AAC-ELD \n
+                       768, 1024, 2048 or 4096 for USAC  */
+  INT numChannels; /*!< The number of output audio channels before the rendering
+                      module, i.e. the original channel configuration. */
+  AUDIO_CHANNEL_TYPE
+  *pChannelType; /*!< Audio channel type of each output audio channel. */
+  UCHAR *pChannelIndices; /*!< Audio channel index for each output audio
+                             channel. See ISO/IEC 13818-7:2005(E), 8.5.3.2
+                             Explicit channel mapping using a
+                             program_config_element() */
+  /* Decoder internal members. */
+  INT aacSampleRate; /*!< Sampling rate in Hz without SBR (from configuration
+                        info) divided by a (ELD) downscale factor if present. */
+  INT profile; /*!< MPEG-2 profile (from file header) (-1: not applicable (e. g.
+                  MPEG-4)).               */
+  AUDIO_OBJECT_TYPE
+  aot; /*!< Audio Object Type (from ASC): is set to the appropriate value
+          for MPEG-2 bitstreams (e. g. 2 for AAC-LC). */
+  INT channelConfig; /*!< Channel configuration (0: PCE defined, 1: mono, 2:
+                        stereo, ...                       */
+  INT bitRate;       /*!< Instantaneous bit rate.                   */
+  INT aacSamplesPerFrame;   /*!< Samples per frame for the AAC core (from ASC)
+                               divided by a (ELD) downscale factor if present. \n
+                                 Typically this is (with a downscale factor of 1):
+                               \n   1024 or 960 for AAC-LC \n   512 or 480 for
+                               AAC-LD   and AAC-ELD         */
+  INT aacNumChannels;       /*!< The number of audio channels after AAC core
+                               processing (before PS or MPS processing).       CAUTION: This
+                               are not the final number of output channels! */
+  AUDIO_OBJECT_TYPE extAot; /*!< Extension Audio Object Type (from ASC)   */
+  INT extSamplingRate; /*!< Extension sampling rate in Hz (from ASC) divided by
+                          a (ELD) downscale factor if present. */
+
+  UINT outputDelay; /*!< The number of samples the output is additionally
+                       delayed by.the decoder. */
+  UINT flags; /*!< Copy of internal flags. Only to be written by the decoder,
+                 and only to be read externally. */
+
+  SCHAR epConfig; /*!< epConfig level (from ASC): only level 0 supported, -1
+                     means no ER (e. g. AOT=2, MPEG-2 AAC, etc.)  */
+  /* Statistics */
+  INT numLostAccessUnits; /*!< This integer will reflect the estimated amount of
+                             lost access units in case aacDecoder_DecodeFrame()
+                               returns AAC_DEC_TRANSPORT_SYNC_ERROR. It will be
+                             < 0 if the estimation failed. */
+
+  INT64 numTotalBytes; /*!< This is the number of total bytes that have passed
+                          through the decoder. */
+  INT64
+  numBadBytes; /*!< This is the number of total bytes that were considered
+                  with errors from numTotalBytes. */
+  INT64
+  numTotalAccessUnits;     /*!< This is the number of total access units that
+                              have passed through the decoder. */
+  INT64 numBadAccessUnits; /*!< This is the number of total access units that
+                              were considered with errors from numTotalBytes. */
+
+  /* Metadata */
+  SCHAR drcProgRefLev; /*!< DRC program reference level. Defines the reference
+                          level below full-scale. It is quantized in steps of
+                          0.25dB. The valid values range from 0 (0 dBFS) to 127
+                          (-31.75 dBFS). It is used to reflect the average
+                          loudness of the audio in LKFS according to ITU-R BS
+                          1770. If no level has been found in the bitstream the
+                          value is -1. */
+  SCHAR
+  drcPresMode;        /*!< DRC presentation mode. According to ETSI TS 101 154,
+                         this field indicates whether   light (MPEG-4 Dynamic Range
+                         Control tool) or heavy compression (DVB heavy
+                         compression)   dynamic range control shall take priority
+                         on the outputs.   For details, see ETSI TS 101 154, table
+                         C.33. Possible values are: \n   -1: No corresponding
+                         metadata found in the bitstream \n   0: DRC presentation
+                         mode not indicated \n   1: DRC presentation mode 1 \n   2:
+                         DRC presentation mode 2 \n   3: Reserved */
+  INT outputLoudness; /*!< Audio output loudness in steps of -0.25 dB. Range: 0
+                         (0 dBFS) to 231 (-57.75 dBFS).\n  A value of -1
+                         indicates that no loudness metadata is present.\n  If
+                         loudness normalization is active, the value corresponds
+                         to the target loudness value set with
+                         ::AAC_DRC_REFERENCE_LEVEL.\n  If loudness normalization
+                         is not active, the output loudness value corresponds to
+                         the loudness metadata given in the bitstream.\n
+                           Loudness metadata can originate from MPEG-4 DRC or
+                         MPEG-D DRC. */
+
+} CStreamInfo;
+
+typedef struct AAC_DECODER_INSTANCE
+    *HANDLE_AACDECODER; /*!< Pointer to a AAC decoder instance. */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \brief Initialize ancillary data buffer.
+ *
+ * \param self    AAC decoder handle.
+ * \param buffer  Pointer to (external) ancillary data buffer.
+ * \param size    Size of the buffer pointed to by buffer.
+ * \return        Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_AncDataInit(HANDLE_AACDECODER self,
+                                                    UCHAR *buffer, int size);
+
+/**
+ * \brief Get one ancillary data element.
+ *
+ * \param self   AAC decoder handle.
+ * \param index  Index of the ancillary data element to get.
+ * \param ptr    Pointer to a buffer receiving a pointer to the requested
+ * ancillary data element.
+ * \param size   Pointer to a buffer receiving the length of the requested
+ * ancillary data element.
+ * \return       Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_AncDataGet(HANDLE_AACDECODER self,
+                                                   int index, UCHAR **ptr,
+                                                   int *size);
+
+/**
+ * \brief Set one single decoder parameter.
+ *
+ * \param self   AAC decoder handle.
+ * \param param  Parameter to be set.
+ * \param value  Parameter value.
+ * \return       Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_SetParam(const HANDLE_AACDECODER self,
+                                                 const AACDEC_PARAM param,
+                                                 const INT value);
+
+/**
+ * \brief              Get free bytes inside decoder internal buffer.
+ * \param self         Handle of AAC decoder instance.
+ * \param pFreeBytes   Pointer to variable receiving amount of free bytes inside
+ * decoder internal buffer.
+ * \return             Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR
+aacDecoder_GetFreeBytes(const HANDLE_AACDECODER self, UINT *pFreeBytes);
+
+/**
+ * \brief               Open an AAC decoder instance.
+ * \param transportFmt  The transport type to be used.
+ * \param nrOfLayers    Number of transport layers.
+ * \return              AAC decoder handle.
+ */
+LINKSPEC_H HANDLE_AACDECODER aacDecoder_Open(TRANSPORT_TYPE transportFmt,
+                                             UINT nrOfLayers);
+
+/**
+ * \brief Explicitly configure the decoder by passing a raw AudioSpecificConfig
+ * (ASC) or a StreamMuxConfig (SMC), contained in a binary buffer. This is
+ * required for MPEG-4 and Raw Packets file format bitstreams as well as for
+ * LATM bitstreams with no in-band SMC. If the transport format is LATM with or
+ * without LOAS, configuration is assumed to be an SMC, for all other file
+ * formats an ASC.
+ *
+ * \param self    AAC decoder handle.
+ * \param conf    Pointer to an unsigned char buffer containing the binary
+ * configuration buffer (either ASC or SMC).
+ * \param length  Length of the configuration buffer in bytes.
+ * \return        Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_ConfigRaw(HANDLE_AACDECODER self,
+                                                  UCHAR *conf[],
+                                                  const UINT length[]);
+
+/**
+ * \brief Submit raw ISO base media file format boxes to decoder for parsing
+ * (only some box types are recognized).
+ *
+ * \param self    AAC decoder handle.
+ * \param buffer  Pointer to an unsigned char buffer containing the binary box
+ * data (including size and type, can be a sequence of multiple boxes).
+ * \param length  Length of the data in bytes.
+ * \return        Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_RawISOBMFFData(HANDLE_AACDECODER self,
+                                                       UCHAR *buffer,
+                                                       UINT length);
+
+/**
+ * \brief Fill AAC decoder's internal input buffer with bitstream data from the
+ * external input buffer. The function only copies such data as long as the
+ * decoder-internal input buffer is not full. So it grabs whatever it can from
+ * pBuffer and returns information (bytesValid) so that at a subsequent call of
+ * %aacDecoder_Fill(), the right position in pBuffer can be determined to grab
+ * the next data.
+ *
+ * \param self        AAC decoder handle.
+ * \param pBuffer     Pointer to external input buffer.
+ * \param bufferSize  Size of external input buffer. This argument is required
+ * because decoder-internally we need the information to calculate the offset to
+ * pBuffer, where the next available data is, which is then
+ * fed into the decoder-internal buffer (as much as
+ * possible). Our example framework implementation fills the
+ * buffer at pBuffer again, once it contains no available valid bytes anymore
+ * (meaning bytesValid equal 0).
+ * \param bytesValid  Number of bitstream bytes in the external bitstream buffer
+ * that have not yet been copied into the decoder's internal bitstream buffer by
+ * calling this function. The value is updated according to
+ * the amount of newly copied bytes.
+ * \return            Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_Fill(HANDLE_AACDECODER self,
+                                             UCHAR *pBuffer[],
+                                             const UINT bufferSize[],
+                                             UINT *bytesValid);
+
+/** Flag for aacDecoder_DecodeFrame(): Trigger the built-in error concealment
+ * module to generate a substitute signal for one lost frame. New input data
+ * will not be considered.
+ */
+#define AACDEC_CONCEAL 1
+/** Flag for aacDecoder_DecodeFrame(): Flush all filterbanks to get all delayed
+ * audio without having new input data. Thus new input data will not be
+ * considered.
+ */
+#define AACDEC_FLUSH 2
+/** Flag for aacDecoder_DecodeFrame(): Signal an input bit stream data
+ * discontinuity. Resync any internals as necessary.
+ */
+#define AACDEC_INTR 4
+/** Flag for aacDecoder_DecodeFrame(): Clear all signal delay lines and history
+ * buffers. CAUTION: This can cause discontinuities in the output signal.
+ */
+#define AACDEC_CLRHIST 8
+
+/**
+ * \brief               Decode one audio frame
+ *
+ * \param self          AAC decoder handle.
+ * \param pTimeData     Pointer to external output buffer where the decoded PCM
+ * samples will be stored into.
+ * \param timeDataSize  Size of external output buffer in PCM samples.
+ * \param flags         Bit field with flags for the decoder: \n
+ *                      (flags & AACDEC_CONCEAL) == 1: Do concealment. \n
+ *                      (flags & AACDEC_FLUSH) == 2: Discard input data. Flush
+ * filter banks (output delayed audio). \n (flags & AACDEC_INTR) == 4: Input
+ * data is discontinuous. Resynchronize any internals as
+ * necessary. \n (flags & AACDEC_CLRHIST) == 8: Clear all signal delay lines and
+ * history buffers.
+ * \return              Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_DecodeFrame(HANDLE_AACDECODER self,
+                                                    INT_PCM *pTimeData,
+                                                    const INT timeDataSize,
+                                                    const UINT flags);
+
+/**
+ * \brief       De-allocate all resources of an AAC decoder instance.
+ *
+ * \param self  AAC decoder handle.
+ * \return      void.
+ */
+LINKSPEC_H void aacDecoder_Close(HANDLE_AACDECODER self);
+
+/**
+ * \brief       Get CStreamInfo handle from decoder.
+ *
+ * \param self  AAC decoder handle.
+ * \return      Reference to requested CStreamInfo.
+ */
+LINKSPEC_H CStreamInfo *aacDecoder_GetStreamInfo(HANDLE_AACDECODER self);
+
+/**
+ * \brief       Get decoder library info.
+ *
+ * \param info  Pointer to an allocated LIB_INFO structure.
+ * \return      0 on success.
+ */
+LINKSPEC_H INT aacDecoder_GetLibInfo(LIB_INFO *info);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AACDECODER_LIB_H */
diff --git a/third_party/libfdkaac/include/genericStds.h b/third_party/libfdkaac/include/genericStds.h
new file mode 100644
index 0000000..8828ba7
--- /dev/null
+++ b/third_party/libfdkaac/include/genericStds.h
@@ -0,0 +1,584 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright  1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2.    COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3.    NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4.    DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5.    CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+   Author(s):
+
+   Description:
+
+*******************************************************************************/
+
+/** \file   genericStds.h
+    \brief  Generic Run-Time Support function wrappers and heap allocation
+   monitoring.
+ */
+
+#if !defined(GENERICSTDS_H)
+#define GENERICSTDS_H
+
+#include "machine_type.h"
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846 /*!< Pi. Only used in example projects. */
+#endif
+
+/**
+ * Identifiers for various memory locations. They are used along with memory
+ * allocation functions like FDKcalloc_L() to specify the requested memory's
+ * location.
+ */
+typedef enum {
+  /* Internal */
+  SECT_DATA_L1 = 0x2000,
+  SECT_DATA_L2,
+  SECT_DATA_L1_A,
+  SECT_DATA_L1_B,
+  SECT_CONSTDATA_L1,
+
+  /* External */
+  SECT_DATA_EXTERN = 0x4000,
+  SECT_CONSTDATA_EXTERN
+
+} MEMORY_SECTION;
+
+/*! \addtogroup SYSLIB_MEMORY_MACROS FDK memory macros
+ *
+ * The \c H_ prefix indicates that the macro is to be used in a header file, the
+ * \c C_ prefix indicates that the macro is to be used in a source file.
+ *
+ * Declaring memory areas requires to specify a unique name and a data type.
+ *
+ * For defining a memory area you require additionally one or two sizes,
+ * depending if the memory should be organized into one or two dimensions.
+ *
+ * The macros containing the keyword \c AALLOC instead of \c ALLOC additionally
+ * take care of returning aligned memory addresses (beyond the natural alignment
+ * of its type). The preprocesor macro
+ * ::ALIGNMENT_DEFAULT indicates the aligment to be used (this is hardware
+ * specific).
+ *
+ * The \c _L suffix indicates that the memory will be located in a specific
+ * section. This is useful to allocate critical memory section into fast
+ * internal SRAM for example.
+ *
+ * @{
+ */
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define H_ALLOC_MEM(name, type) \
+  type *Get##name(int n = 0);   \
+  void Free##name(type **p);    \
+  UINT GetRequiredMem##name(void);
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define H_ALLOC_MEM_OVERLAY(name, type) \
+  type *Get##name(int n = 0);           \
+  void Free##name(type **p);            \
+  UINT GetRequiredMem##name(void);
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM(name, type, num)               \
+  type *Get##name(int n) {                         \
+    FDK_ASSERT((n) == 0);                          \
+    return ((type *)FDKcalloc(num, sizeof(type))); \
+  }                                                \
+  void Free##name(type **p) {                      \
+    if (p != NULL) {                               \
+      FDKfree(*p);                                 \
+      *p = NULL;                                   \
+    }                                              \
+  }                                                \
+  UINT GetRequiredMem##name(void) {                \
+    return ALGN_SIZE_EXTRES((num) * sizeof(type)); \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM2(name, type, n1, n2)                 \
+  type *Get##name(int n) {                               \
+    FDK_ASSERT((n) < (n2));                              \
+    return ((type *)FDKcalloc(n1, sizeof(type)));        \
+  }                                                      \
+  void Free##name(type **p) {                            \
+    if (p != NULL) {                                     \
+      FDKfree(*p);                                       \
+      *p = NULL;                                         \
+    }                                                    \
+  }                                                      \
+  UINT GetRequiredMem##name(void) {                      \
+    return ALGN_SIZE_EXTRES((n1) * sizeof(type)) * (n2); \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM(name, type, num)                                  \
+  type *Get##name(int n) {                                             \
+    type *ap;                                                          \
+    FDK_ASSERT((n) == 0);                                              \
+    ap = ((type *)FDKaalloc((num) * sizeof(type), ALIGNMENT_DEFAULT)); \
+    return ap;                                                         \
+  }                                                                    \
+  void Free##name(type **p) {                                          \
+    if (p != NULL) {                                                   \
+      FDKafree(*p);                                                    \
+      *p = NULL;                                                       \
+    }                                                                  \
+  }                                                                    \
+  UINT GetRequiredMem##name(void) {                                    \
+    return ALGN_SIZE_EXTRES((num) * sizeof(type) + ALIGNMENT_DEFAULT + \
+                            sizeof(void *));                           \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM2(name, type, n1, n2)                             \
+  type *Get##name(int n) {                                            \
+    type *ap;                                                         \
+    FDK_ASSERT((n) < (n2));                                           \
+    ap = ((type *)FDKaalloc((n1) * sizeof(type), ALIGNMENT_DEFAULT)); \
+    return ap;                                                        \
+  }                                                                   \
+  void Free##name(type **p) {                                         \
+    if (p != NULL) {                                                  \
+      FDKafree(*p);                                                   \
+      *p = NULL;                                                      \
+    }                                                                 \
+  }                                                                   \
+  UINT GetRequiredMem##name(void) {                                   \
+    return ALGN_SIZE_EXTRES((n1) * sizeof(type) + ALIGNMENT_DEFAULT + \
+                            sizeof(void *)) *                         \
+           (n2);                                                      \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM_L(name, type, num, s)               \
+  type *Get##name(int n) {                              \
+    FDK_ASSERT((n) == 0);                               \
+    return ((type *)FDKcalloc_L(num, sizeof(type), s)); \
+  }                                                     \
+  void Free##name(type **p) {                           \
+    if (p != NULL) {                                    \
+      FDKfree_L(*p);                                    \
+      *p = NULL;                                        \
+    }                                                   \
+  }                                                     \
+  UINT GetRequiredMem##name(void) {                     \
+    return ALGN_SIZE_EXTRES((num) * sizeof(type));      \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM2_L(name, type, n1, n2, s)            \
+  type *Get##name(int n) {                               \
+    FDK_ASSERT((n) < (n2));                              \
+    return (type *)FDKcalloc_L(n1, sizeof(type), s);     \
+  }                                                      \
+  void Free##name(type **p) {                            \
+    if (p != NULL) {                                     \
+      FDKfree_L(*p);                                     \
+      *p = NULL;                                         \
+    }                                                    \
+  }                                                      \
+  UINT GetRequiredMem##name(void) {                      \
+    return ALGN_SIZE_EXTRES((n1) * sizeof(type)) * (n2); \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM_L(name, type, num, s)                                  \
+  type *Get##name(int n) {                                                  \
+    type *ap;                                                               \
+    FDK_ASSERT((n) == 0);                                                   \
+    ap = ((type *)FDKaalloc_L((num) * sizeof(type), ALIGNMENT_DEFAULT, s)); \
+    return ap;                                                              \
+  }                                                                         \
+  void Free##name(type **p) {                                               \
+    if (p != NULL) {                                                        \
+      FDKafree_L(*p);                                                       \
+      *p = NULL;                                                            \
+    }                                                                       \
+  }                                                                         \
+  UINT GetRequiredMem##name(void) {                                         \
+    return ALGN_SIZE_EXTRES((num) * sizeof(type) + ALIGNMENT_DEFAULT +      \
+                            sizeof(void *));                                \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM2_L(name, type, n1, n2, s)                             \
+  type *Get##name(int n) {                                                 \
+    type *ap;                                                              \
+    FDK_ASSERT((n) < (n2));                                                \
+    ap = ((type *)FDKaalloc_L((n1) * sizeof(type), ALIGNMENT_DEFAULT, s)); \
+    return ap;                                                             \
+  }                                                                        \
+  void Free##name(type **p) {                                              \
+    if (p != NULL) {                                                       \
+      FDKafree_L(*p);                                                      \
+      *p = NULL;                                                           \
+    }                                                                      \
+  }                                                                        \
+  UINT GetRequiredMem##name(void) {                                        \
+    return ALGN_SIZE_EXTRES((n1) * sizeof(type) + ALIGNMENT_DEFAULT +      \
+                            sizeof(void *)) *                              \
+           (n2);                                                           \
+  }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM_OVERLAY(name, type, num, sect, tag) \
+  C_AALLOC_MEM_L(name, type, num, sect)
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_SCRATCH_START(name, type, n)                 \
+  type _##name[(n) + (ALIGNMENT_DEFAULT + sizeof(type) - 1)]; \
+  type *name = (type *)ALIGN_PTR(_##name);                    \
+  C_ALLOC_ALIGNED_REGISTER(name, (n) * sizeof(type));
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_SCRATCH_START(name, type, n) type name[n];
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_SCRATCH_END(name, type, n) C_ALLOC_ALIGNED_UNREGISTER(name);
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_SCRATCH_END(name, type, n)
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_STACK_START(name, type, n)                   \
+  type _##name[(n) + (ALIGNMENT_DEFAULT + sizeof(type) - 1)]; \
+  type *name = (type *)ALIGN_PTR(_##name);                    \
+  C_ALLOC_ALIGNED_REGISTER(name, (n) * sizeof(type));
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_STACK_END(name, type, n) C_ALLOC_ALIGNED_UNREGISTER(name);
+
+/*! @} */
+
+#define C_ALLOC_ALIGNED_REGISTER(x, size)
+#define C_ALLOC_ALIGNED_UNREGISTER(x)
+#define C_ALLOC_ALIGNED_CHECK(x)
+#define C_ALLOC_ALIGNED_CHECK2(x, y)
+#define FDK_showBacktrace(a, b)
+
+/*! \addtogroup SYSLIB_EXITCODES Unified exit codes
+ *  Exit codes to be used as return values of FDK software test and
+ * demonstration applications. Not as return values of product modules and/or
+ * libraries.
+ *  @{
+ */
+#define FDK_EXITCODE_OK 0 /*!< Successful termination. No errors. */
+#define FDK_EXITCODE_USAGE                                                  \
+  64 /*!< The command/application was used incorrectly, e.g. with the wrong \
+        number of arguments, a bad flag, a bad syntax in a parameter, or    \
+        whatever. */
+#define FDK_EXITCODE_DATAERROR                                               \
+  65 /*!< The input data was incorrect in some way. This should only be used \
+        for user data and not system files. */
+#define FDK_EXITCODE_NOINPUT                                                   \
+  66 /*!< An input file (not a system file) did not exist or was not readable. \
+      */
+#define FDK_EXITCODE_UNAVAILABLE                                              \
+  69 /*!< A service is unavailable. This can occur if a support program or    \
+        file does not exist. This can also be used as a catchall message when \
+        something you wanted to do doesn't work, but you don't know why. */
+#define FDK_EXITCODE_SOFTWARE                                                  \
+  70 /*!< An internal software error has been detected. This should be limited \
+        to non- operating system related errors as possible. */
+#define FDK_EXITCODE_CANTCREATE \
+  73 /*!< A (user specified) output file cannot be created. */
+#define FDK_EXITCODE_IOERROR \
+  74 /*!< An error occurred while doing I/O on some file. */
+/*! @} */
+
+/*--------------------------------------------
+ * Runtime support declarations
+ *---------------------------------------------*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void FDKprintf(const char *szFmt, ...);
+
+void FDKprintfErr(const char *szFmt, ...);
+
+/** Wrapper for <stdio.h>'s getchar(). */
+int FDKgetchar(void);
+
+INT FDKfprintf(void *stream, const char *format, ...);
+INT FDKsprintf(char *str, const char *format, ...);
+
+char *FDKstrchr(char *s, INT c);
+const char *FDKstrstr(const char *haystack, const char *needle);
+char *FDKstrcpy(char *dest, const char *src);
+char *FDKstrncpy(char *dest, const char *src, const UINT n);
+
+#define FDK_MAX_OVERLAYS 8 /**< Maximum number of memory overlays. */
+
+void *FDKcalloc(const UINT n, const UINT size);
+void *FDKmalloc(const UINT size);
+void FDKfree(void *ptr);
+
+/**
+ *  Allocate and clear an aligned memory area. Use FDKafree() instead of
+ * FDKfree() for these memory areas.
+ *
+ * \param size       Size of requested memory in bytes.
+ * \param alignment  Alignment of requested memory in bytes.
+ * \return           Pointer to allocated memory.
+ */
+void *FDKaalloc(const UINT size, const UINT alignment);
+
+/**
+ *  Free an aligned memory area.
+ *
+ * \param ptr  Pointer to be freed.
+ */
+void FDKafree(void *ptr);
+
+/**
+ *  Allocate memory in a specific memory section.
+ *  Requests can be made for internal or external memory. If internal memory is
+ *  requested, FDKcalloc_L() first tries to use L1 memory, which sizes are
+ * defined by ::DATA_L1_A_SIZE and ::DATA_L1_B_SIZE. If no L1 memory is
+ * available, then FDKcalloc_L() tries to use L2 memory. If that fails as well,
+ * the requested memory is allocated at an extern location using the fallback
+ * FDKcalloc().
+ *
+ * \param n     See MSDN documentation on calloc().
+ * \param size  See MSDN documentation on calloc().
+ * \param s     Memory section.
+ * \return      See MSDN documentation on calloc().
+ */
+void *FDKcalloc_L(const UINT n, const UINT size, MEMORY_SECTION s);
+
+/**
+ *  Allocate aligned memory in a specific memory section.
+ *  See FDKcalloc_L() description for details - same applies here.
+ */
+void *FDKaalloc_L(const UINT size, const UINT alignment, MEMORY_SECTION s);
+
+/**
+ *  Free memory that was allocated in a specific memory section.
+ */
+void FDKfree_L(void *ptr);
+
+/**
+ *  Free aligned memory that was allocated in a specific memory section.
+ */
+void FDKafree_L(void *ptr);
+
+/**
+ * Copy memory. Source and destination memory must not overlap.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memcpy(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemcpy(void *dst, const void *src, const UINT size);
+
+/**
+ * Copy memory. Source and destination memory are allowed to overlap.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memmove(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemmove(void *dst, const void *src, const UINT size);
+
+/**
+ * Clear memory.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memclear(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemclear(void *memPtr, const UINT size);
+
+/**
+ * Fill memory with values.
+ * The function arguments correspond to the standard memset(). Please see MSDN
+ * documentation for details on how to use it.
+ */
+void FDKmemset(void *memPtr, const INT value, const UINT size);
+
+/* Compare function wrappers */
+INT FDKmemcmp(const void *s1, const void *s2, const UINT size);
+INT FDKstrcmp(const char *s1, const char *s2);
+INT FDKstrncmp(const char *s1, const char *s2, const UINT size);
+
+UINT FDKstrlen(const char *s);
+
+#define FDKmax(a, b) ((a) > (b) ? (a) : (b))
+#define FDKmin(a, b) ((a) < (b) ? (a) : (b))
+
+#define FDK_INT_MAX ((INT)0x7FFFFFFF)
+#define FDK_INT_MIN ((INT)0x80000000)
+
+/* FILE I/O */
+
+/*!
+ *  Check platform for endianess.
+ *
+ * \return  1 if platform is little endian, non-1 if platform is big endian.
+ */
+int IS_LITTLE_ENDIAN(void);
+
+/*!
+ *  Convert input value to little endian format.
+ *
+ * \param val  Value to be converted. It may be in both big or little endian.
+ * \return     Value in little endian format.
+ */
+UINT TO_LITTLE_ENDIAN(UINT val);
+
+/*!
+ * \fn     FDKFILE *FDKfopen(const char *filename, const char *mode);
+ *         Standard fopen() wrapper.
+ * \fn     INT FDKfclose(FDKFILE *FP);
+ *         Standard fclose() wrapper.
+ * \fn     INT FDKfseek(FDKFILE *FP, LONG OFFSET, int WHENCE);
+ *         Standard fseek() wrapper.
+ * \fn     INT FDKftell(FDKFILE *FP);
+ *         Standard ftell() wrapper.
+ * \fn     INT FDKfflush(FDKFILE *fp);
+ *         Standard fflush() wrapper.
+ * \fn     UINT FDKfwrite(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+ *         Standard fwrite() wrapper.
+ * \fn     UINT FDKfread(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+ *         Standard fread() wrapper.
+ */
+typedef void FDKFILE;
+extern const INT FDKSEEK_SET, FDKSEEK_CUR, FDKSEEK_END;
+
+FDKFILE *FDKfopen(const char *filename, const char *mode);
+INT FDKfclose(FDKFILE *FP);
+INT FDKfseek(FDKFILE *FP, LONG OFFSET, int WHENCE);
+INT FDKftell(FDKFILE *FP);
+INT FDKfflush(FDKFILE *fp);
+UINT FDKfwrite(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+UINT FDKfread(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+char *FDKfgets(void *dst, INT size, FDKFILE *fp);
+void FDKrewind(FDKFILE *fp);
+INT FDKfeof(FDKFILE *fp);
+
+/**
+ * \brief        Write each member in little endian order. Convert automatically
+ * to host endianess.
+ * \param ptrf   Pointer to memory where to read data from.
+ * \param size   Size of each item to be written.
+ * \param nmemb  Number of items to be written.
+ * \param fp     File pointer of type FDKFILE.
+ * \return       Number of items read on success and fread() error on failure.
+ */
+UINT FDKfwrite_EL(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+
+/**
+ * \brief        Read variable of size "size" as little endian. Convert
+ * automatically to host endianess. 4-byte alignment is enforced for 24 bit
+ * data, at 32 bit full scale.
+ * \param dst    Pointer to memory where to store data into.
+ * \param size   Size of each item to be read.
+ * \param nmemb  Number of items to be read.
+ * \param fp     File pointer of type FDKFILE.
+ * \return       Number of items read on success and fread() error on failure.
+ */
+UINT FDKfread_EL(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+
+/**
+ * \brief  Print FDK software disclaimer.
+ */
+void FDKprintDisclaimer(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GENERICSTDS_H */
diff --git a/third_party/libfdkaac/include/machine_type.h b/third_party/libfdkaac/include/machine_type.h
new file mode 100644
index 0000000..bd97669
--- /dev/null
+++ b/third_party/libfdkaac/include/machine_type.h
@@ -0,0 +1,411 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright  1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2.    COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3.    NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4.    DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5.    CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+   Author(s):
+
+   Description:
+
+*******************************************************************************/
+
+/** \file   machine_type.h
+ *  \brief  Type defines for various processors and compiler tools.
+ */
+
+#if !defined(MACHINE_TYPE_H)
+#define MACHINE_TYPE_H
+
+#include <stddef.h> /* Needed to define size_t */
+
+#if defined(__ANDROID__) && (__GNUC__ == 4) && (__GNUC_MINOR__ == 4) && \
+    (__GNUC_GNU_INLINE__ == 1)
+typedef unsigned long long uint64_t;
+#include <sys/types.h>
+#endif
+
+/* Library calling convention spec. __cdecl and friends might be added here as
+ * required. */
+#define LINKSPEC_H
+#define LINKSPEC_CPP
+
+/* for doxygen the following docu parts must be separated */
+/** \var  SCHAR
+ *        Data type representing at least 1 byte signed integer on all supported
+ * platforms.
+ */
+/** \var  UCHAR
+ *        Data type representing at least 1 byte unsigned integer on all
+ * supported platforms.
+ */
+/** \var  INT
+ *        Data type representing at least 4 byte signed integer on all supported
+ * platforms.
+ */
+/** \var  UINT
+ *        Data type representing at least 4 byte unsigned integer on all
+ * supported platforms.
+ */
+/** \var  LONG
+ *        Data type representing 4 byte signed integer on all supported
+ * platforms.
+ */
+/** \var  ULONG
+ *        Data type representing 4 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \var  SHORT
+ *        Data type representing 2 byte signed integer on all supported
+ * platforms.
+ */
+/** \var  USHORT
+ *        Data type representing 2 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \var  INT64
+ *        Data type representing 8 byte signed integer on all supported
+ * platforms.
+ */
+/** \var  UINT64
+ *        Data type representing 8 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \def  SHORT_BITS
+ *        Number of bits the data type short represents. sizeof() is not suited
+ * to get this info, because a byte is not always defined as 8 bits.
+ */
+/** \def  CHAR_BITS
+ *        Number of bits the data type char represents. sizeof() is not suited
+ * to get this info, because a byte is not always defined as 8 bits.
+ */
+/** \var  INT_PCM
+ *        Data type representing the width of input and output PCM samples.
+ */
+
+typedef signed int INT;
+typedef unsigned int UINT;
+#ifdef __LP64__
+/* force FDK long-datatypes to 4 byte  */
+/* Use defines to avoid type alias problems on 64 bit machines. */
+#define LONG INT
+#define ULONG UINT
+#else  /* __LP64__ */
+typedef signed long LONG;
+typedef unsigned long ULONG;
+#endif /* __LP64__ */
+typedef signed short SHORT;
+typedef unsigned short USHORT;
+typedef signed char SCHAR;
+typedef unsigned char UCHAR;
+
+#define SHORT_BITS 16
+#define CHAR_BITS 8
+
+/* Define 64 bit base integer type. */
+#ifdef _MSC_VER
+typedef __int64 INT64;
+typedef unsigned __int64 UINT64;
+#else
+typedef long long INT64;
+typedef unsigned long long UINT64;
+#endif
+
+#ifndef NULL
+#ifdef __cplusplus
+#define NULL 0
+#else
+#define NULL ((void *)0)
+#endif
+#endif
+
+#if ((defined(__i686__) || defined(__i586__) || defined(__i386__) ||  \
+      defined(__x86_64__)) ||                                         \
+     (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)))) && \
+    !defined(FDK_ASSERT_ENABLE)
+#define FDK_ASSERT_ENABLE
+#endif
+
+#if defined(FDK_ASSERT_ENABLE)
+#include <assert.h>
+#define FDK_ASSERT(x) assert(x)
+#else
+#define FDK_ASSERT(ignore)
+#endif
+
+typedef SHORT INT_PCM;
+#define MAXVAL_PCM MAXVAL_SGL
+#define MINVAL_PCM MINVAL_SGL
+#define WAV_BITS 16
+#define SAMPLE_BITS 16
+#define SAMPLE_MAX ((INT_PCM)(((ULONG)1 << (SAMPLE_BITS - 1)) - 1))
+#define SAMPLE_MIN (~SAMPLE_MAX)
+
+/*!
+* \def    RAM_ALIGN
+*  Used to align memory as prefix before memory declaration. For example:
+   \code
+   RAM_ALIGN
+   int myArray[16];
+   \endcode
+
+   Note, that not all platforms support this mechanism. For example with TI
+compilers a preprocessor pragma is used, but to do something like
+
+   \code
+   #define RAM_ALIGN #pragma DATA_ALIGN(x)
+   \endcode
+
+   would require the preprocessor to process this line twice to fully resolve
+it. Hence, a fully platform-independant way to use alignment is not supported.
+
+* \def    ALIGNMENT_DEFAULT
+*         Default alignment in bytes.
+*/
+
+#define ALIGNMENT_DEFAULT 8
+
+/* RAM_ALIGN keyword causes memory alignment of global variables. */
+#if defined(_MSC_VER)
+#define RAM_ALIGN __declspec(align(ALIGNMENT_DEFAULT))
+#elif defined(__GNUC__)
+#define RAM_ALIGN __attribute__((aligned(ALIGNMENT_DEFAULT)))
+#else
+#define RAM_ALIGN
+#endif
+
+/*!
+ * \def  RESTRICT
+ *       The restrict keyword is supported by some platforms and RESTRICT maps
+ * to either the corresponding keyword on each platform or to void if the
+ *       compiler does not provide such feature. It tells the compiler that a
+ * pointer points to memory that does not overlap with other memories pointed to
+ * by other pointers. If this keyword is used and the assumption of no
+ * overlap is not true the resulting code might crash.
+ *
+ * \def  WORD_ALIGNED(x)
+ *       Tells the compiler that pointer x is 16 bit aligned. It does not cause
+ * the address itself to be aligned, but serves as a hint to the optimizer. The
+ * alignment of the pointer must be guarranteed, if not the code might
+ * crash.
+ *
+ * \def  DWORD_ALIGNED(x)
+ *       Tells the compiler that pointer x is 32 bit aligned. It does not cause
+ * the address itself to be aligned, but serves as a hint to the optimizer. The
+ * alignment of the pointer must be guarranteed, if not the code might
+ * crash.
+ *
+ */
+#define RESTRICT
+#define WORD_ALIGNED(x) C_ALLOC_ALIGNED_CHECK2((const void *)(x), 2);
+#define DWORD_ALIGNED(x) C_ALLOC_ALIGNED_CHECK2((const void *)(x), 4);
+
+/*-----------------------------------------------------------------------------------
+ * ALIGN_SIZE
+ *-----------------------------------------------------------------------------------*/
+/*!
+ * \brief  This macro aligns a given value depending on ::ALIGNMENT_DEFAULT.
+ *
+ * For example if #ALIGNMENT_DEFAULT equals 8, then:
+ * - ALIGN_SIZE(3) returns 8
+ * - ALIGN_SIZE(8) returns 8
+ * - ALIGN_SIZE(9) returns 16
+ */
+#define ALIGN_SIZE(a)                                                          \
+  ((a) + (((INT)ALIGNMENT_DEFAULT - ((size_t)(a) & (ALIGNMENT_DEFAULT - 1))) & \
+          (ALIGNMENT_DEFAULT - 1)))
+
+/*!
+ * \brief  This macro aligns a given address depending on ::ALIGNMENT_DEFAULT.
+ */
+#define ALIGN_PTR(a)                                      \
+  ((void *)((unsigned char *)(a) +                        \
+            ((((INT)ALIGNMENT_DEFAULT -                   \
+               ((size_t)(a) & (ALIGNMENT_DEFAULT - 1))) & \
+              (ALIGNMENT_DEFAULT - 1)))))
+
+/* Alignment macro for libSYS heap implementation */
+#define ALIGNMENT_EXTRES (ALIGNMENT_DEFAULT)
+#define ALGN_SIZE_EXTRES(a)                                               \
+  ((a) + (((INT)ALIGNMENT_EXTRES - ((INT)(a) & (ALIGNMENT_EXTRES - 1))) & \
+          (ALIGNMENT_EXTRES - 1)))
+
+/*!
+ * \def  FDK_FORCEINLINE
+ *       Sometimes compiler do not do what they are told to do, and in case of
+ * inlining some additional command might be necessary depending on the
+ * platform.
+ *
+ * \def  FDK_INLINE
+ *       Defines how the compiler is told to inline stuff.
+ */
+#ifndef FDK_FORCEINLINE
+#if defined(__GNUC__) && !defined(__SDE_MIPS__)
+#define FDK_FORCEINLINE inline __attribute((always_inline))
+#else
+#define FDK_FORCEINLINE inline
+#endif
+#endif
+
+#define FDK_INLINE static inline
+
+/*!
+ * \def  LNK_SECTION_DATA_L1
+ *       The LNK_SECTION_* defines allow memory to be drawn from specific memory
+ *       sections. Used as prefix before variable declaration.
+ *
+ * \def  LNK_SECTION_DATA_L2
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_L1_DATA_A
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_L1_DATA_B
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_CONSTDATA_L1
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_CONSTDATA
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_CODE_L1
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_CODE_L2
+ *       See ::LNK_SECTION_DATA_L1
+ * \def  LNK_SECTION_INITCODE
+ *       See ::LNK_SECTION_DATA_L1
+ */
+/**************************************************
+ * Code Section macros
+ **************************************************/
+#define LNK_SECTION_CODE_L1
+#define LNK_SECTION_CODE_L2
+#define LNK_SECTION_INITCODE
+
+/* Memory section macros. */
+
+/* default fall back */
+#define LNK_SECTION_DATA_L1
+#define LNK_SECTION_DATA_L2
+#define LNK_SECTION_CONSTDATA
+#define LNK_SECTION_CONSTDATA_L1
+
+#define LNK_SECTION_L1_DATA_A
+#define LNK_SECTION_L1_DATA_B
+
+/**************************************************
+ * Macros regarding static code analysis
+ **************************************************/
+#ifdef __cplusplus
+#if !defined(__has_cpp_attribute)
+#define __has_cpp_attribute(x) 0
+#endif
+#if defined(__clang__) && __has_cpp_attribute(clang::fallthrough)
+#define FDK_FALLTHROUGH [[clang::fallthrough]]
+#endif
+#endif
+
+#ifndef FDK_FALLTHROUGH
+#if defined(__GNUC__) && (__GNUC__ >= 7)
+#define FDK_FALLTHROUGH __attribute__((fallthrough))
+#else
+#define FDK_FALLTHROUGH
+#endif
+#endif
+
+#ifdef _MSC_VER
+/*
+ * Sometimes certain features are excluded from compilation and therefore the
+ * warning 4065 may occur: "switch statement contains 'default' but no 'case'
+ * labels" We consider this warning irrelevant and disable it.
+ */
+#pragma warning(disable : 4065)
+#endif
+
+#endif /* MACHINE_TYPE_H */
diff --git a/third_party/libfdkaac/include/syslib_channelMapDescr.h b/third_party/libfdkaac/include/syslib_channelMapDescr.h
new file mode 100644
index 0000000..375a24d
--- /dev/null
+++ b/third_party/libfdkaac/include/syslib_channelMapDescr.h
@@ -0,0 +1,202 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright  1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1.    INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2.    COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3.    NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4.    DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5.    CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+   Author(s):   Thomas Dietzen
+
+   Description:
+
+*******************************************************************************/
+
+/** \file   syslib_channelMapDescr.h
+ *  \brief  Function and structure declarations for the channel map descriptor implementation.
+ */
+
+#ifndef SYSLIB_CHANNELMAPDESCR_H
+#define SYSLIB_CHANNELMAPDESCR_H
+
+#include "machine_type.h"
+
+/**
+ * \brief  Contains information needed for a single channel map.
+ */
+typedef struct {
+  const UCHAR*
+      pChannelMap; /*!< Actual channel mapping for one single configuration. */
+  UCHAR numChannels; /*!< The number of channels for the channel map which is
+                        the maximum used channel index+1. */
+} CHANNEL_MAP_INFO;
+
+/**
+ * \brief   This is the main data struct. It contains the mapping for all
+ * channel configurations such as administration information.
+ *
+ * CAUTION: Do not access this structure directly from a algorithm specific
+ * library. Always use one of the API access functions below!
+ */
+typedef struct {
+  const CHANNEL_MAP_INFO* pMapInfoTab; /*!< Table of channel maps. */
+  UINT mapInfoTabLen; /*!< Length of the channel map table array. */
+  UINT fPassThrough;  /*!< Flag that defines whether the specified mapping shall
+                         be applied  (value: 0) or the input just gets passed
+                         through (MPEG mapping). */
+} FDK_channelMapDescr;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \brief  Initialize a given channel map descriptor.
+ *
+ * \param  pMapDescr      Pointer to a channel map descriptor to be initialized.
+ * \param  pMapInfoTab    Table of channel maps to initizalize the descriptor
+ with.
+ *                        If a NULL pointer is given a default table for
+ WAV-like mapping will be used.
+ * \param  mapInfoTabLen  Length of the channel map table array (pMapInfoTab).
+ If a zero length is given a default table for WAV-like mapping will be used.
+ * \param  fPassThrough   If the flag is set the reordering (given by
+ pMapInfoTab) will be bypassed.
+ */
+void FDK_chMapDescr_init(FDK_channelMapDescr* const pMapDescr,
+                         const CHANNEL_MAP_INFO* const pMapInfoTab,
+                         const UINT mapInfoTabLen, const UINT fPassThrough);
+
+/**
+ * \brief  Change the channel reordering state of a given channel map
+ * descriptor.
+ *
+ * \param  pMapDescr     Pointer to a (initialized) channel map descriptor.
+ * \param  fPassThrough  If the flag is set the reordering (given by
+ * pMapInfoTab) will be bypassed.
+ * \return               Value unequal to zero if set operation was not
+ * successful. And zero on success.
+ */
+int FDK_chMapDescr_setPassThrough(FDK_channelMapDescr* const pMapDescr,
+                                  UINT fPassThrough);
+
+/**
+ * \brief  Get the mapping value for a specific channel and map index.
+ *
+ * \param  pMapDescr  Pointer to channel map descriptor.
+ * \param  chIdx      Channel index.
+ * \param  mapIdx     Mapping index (corresponding to the channel configuration
+ * index).
+ * \return            Mapping value.
+ */
+UCHAR FDK_chMapDescr_getMapValue(const FDK_channelMapDescr* const pMapDescr,
+                                 const UCHAR chIdx, const UINT mapIdx);
+
+/**
+ * \brief  Evaluate whether channel map descriptor is reasonable or not.
+ *
+ * \param  pMapDescr Pointer to channel map descriptor.
+ * \return           Value unequal to zero if descriptor is valid, otherwise
+ * zero.
+ */
+int FDK_chMapDescr_isValid(const FDK_channelMapDescr* const pMapDescr);
+
+/**
+ * Extra variables for setting up Wg4 channel mapping.
+ */
+extern const CHANNEL_MAP_INFO FDK_mapInfoTabWg4[];
+extern const UINT FDK_mapInfoTabLenWg4;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !defined(SYSLIB_CHANNELMAPDESCR_H) */
diff --git a/third_party/llvm-project/libunwind/BUILD.gn b/third_party/llvm-project/libunwind/BUILD.gn
index 544605a..b8663a6 100644
--- a/third_party/llvm-project/libunwind/BUILD.gn
+++ b/third_party/llvm-project/libunwind/BUILD.gn
@@ -123,6 +123,7 @@
     # Build libunwind with concurrency built upon Starboard mutexes and
     # condition variables.
     "_LIBUNWIND_HAS_STARBOARD_THREADS",
+    "NDEBUG"
   ]
 }
 
diff --git a/third_party/mini_chromium/base/base_wrapper.h b/third_party/mini_chromium/base/base_wrapper.h
index ae57c30..491f74f 100644
--- a/third_party/mini_chromium/base/base_wrapper.h
+++ b/third_party/mini_chromium/base/base_wrapper.h
@@ -16,14 +16,35 @@
 #define MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
 
 // Change the symbol name to avoid collisions with //base
+#define Alias MAlias
+
+#define AssertAcquired MAssertAcquired
 #define FilePath MFilePath
+#define CheckHeldAndUnmark MCheckHeldAndUnmark
+#define CheckUnheldAndMark MCheckUnheldAndMark
+#define ConditionVariable MConditionVariable
 #define GetLogMessageHandler MGetLogMessageHandler
+#define Lock MLock
+#define LockImpl MLockImpl
 #define LogMessage MLogMessage
+#define PlatformThreadLocalStorage MPlatformThreadLocalStorage
+#define RandBytes MRandBytes
+#define RandBytesAsString MRandBytesAsString
+#define RandDouble MRandDouble
+#define RandGenerator MRandGenerator
+#define RandInt MRandInt
+#define RandUint64 MRandUint64
 #define ReadUnicodeCharacter MReadUnicodeCharacter
 #define SetLogMessageHandler MSetLogMessageHandler
+#define StringAppendV MStringAppendV
+#define StringPrintf MStringPrintf
+#define ThreadLocalStorage MThreadLocalStorage
 #define UTF16ToUTF8 MUTF16ToUTF8
 #define UmaHistogramSparse MUmaHistogramSparse
+#define UncheckedMalloc MUncheckedMalloc
 #define WriteUnicodeCharacter MWriteUnicodeCharacter
 #define c16len mc16len
+#define utf8_nextCharSafeBody mutf8_nextCharSafeBody
+
 
 #endif  // MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
diff --git a/third_party/mini_chromium/base/debug/alias.h b/third_party/mini_chromium/base/debug/alias.h
index 08d833a..3d764ce 100644
--- a/third_party/mini_chromium/base/debug/alias.h
+++ b/third_party/mini_chromium/base/debug/alias.h
@@ -5,6 +5,8 @@
 #ifndef MINI_CHROMIUM_BASE_DEBUG_ALIAS_H_
 #define MINI_CHROMIUM_BASE_DEBUG_ALIAS_H_
 
+#include "base/base_wrapper.h"
+
 namespace base {
 namespace debug {
 
diff --git a/third_party/mini_chromium/base/process/memory.h b/third_party/mini_chromium/base/process/memory.h
index fca8745..5205e8f 100644
--- a/third_party/mini_chromium/base/process/memory.h
+++ b/third_party/mini_chromium/base/process/memory.h
@@ -7,6 +7,7 @@
 
 #include <stddef.h>
 
+#include "base/base_wrapper.h"
 #include "base/compiler_specific.h"
 
 namespace base {
diff --git a/third_party/mini_chromium/base/rand_util.h b/third_party/mini_chromium/base/rand_util.h
index 21ece1b..c0906bc 100644
--- a/third_party/mini_chromium/base/rand_util.h
+++ b/third_party/mini_chromium/base/rand_util.h
@@ -9,6 +9,8 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
+
 namespace base {
 
 uint64_t RandUint64();
diff --git a/third_party/mini_chromium/base/strings/stringprintf.h b/third_party/mini_chromium/base/strings/stringprintf.h
index 1b1e60b..d2d794c 100644
--- a/third_party/mini_chromium/base/strings/stringprintf.h
+++ b/third_party/mini_chromium/base/strings/stringprintf.h
@@ -9,6 +9,7 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "base/compiler_specific.h"
 
 namespace base {
diff --git a/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc b/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
index 20af747..3c83e80 100644
--- a/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
+++ b/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/base_wrapper.h"
 #include "base/synchronization/condition_variable.h"
 
 #include "base/logging.h"
diff --git a/third_party/mini_chromium/base/synchronization/lock.cc b/third_party/mini_chromium/base/synchronization/lock.cc
index e94e637..dedcc82 100644
--- a/third_party/mini_chromium/base/synchronization/lock.cc
+++ b/third_party/mini_chromium/base/synchronization/lock.cc
@@ -6,6 +6,7 @@
 // is functionally a wrapper around the LockImpl class, so the only
 // real intelligence in the class is in the debugging logic.
 
+#include "base/base_wrapper.h"
 #include "base/synchronization/lock.h"
 
 #include "base/logging.h"
diff --git a/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc b/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
index 27120df..8ae216c 100644
--- a/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
+++ b/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/base_wrapper.h"
 #include "base/synchronization/lock_impl.h"
 
 #include <errno.h>
diff --git a/third_party/mini_chromium/base/third_party/icu/icu_utf.h b/third_party/mini_chromium/base/third_party/icu/icu_utf.h
index df3fb5f..ebae921 100644
--- a/third_party/mini_chromium/base/third_party/icu/icu_utf.h
+++ b/third_party/mini_chromium/base/third_party/icu/icu_utf.h
@@ -14,6 +14,8 @@
 
 #include <stdint.h>
 
+#include "base/base_wrapper.h"
+
 namespace base_icu {
 
 // source/common/unicode/umachine.h
diff --git a/third_party/mini_chromium/base/threading/thread_local_storage.h b/third_party/mini_chromium/base/threading/thread_local_storage.h
index fa79f00..211ef2c 100644
--- a/third_party/mini_chromium/base/threading/thread_local_storage.h
+++ b/third_party/mini_chromium/base/threading/thread_local_storage.h
@@ -5,6 +5,7 @@
 #ifndef MINI_CHROMIUM_BASE_THREADING_THREAD_LOCAL_STORAGE_H_
 #define MINI_CHROMIUM_BASE_THREADING_THREAD_LOCAL_STORAGE_H_
 
+#include "base/base_wrapper.h"
 #include "base/macros.h"
 #include "build/build_config.h"
 
diff --git a/third_party/web_platform_tests/resources/testharness.js b/third_party/web_platform_tests/resources/testharness.js
index 83bb012..49f49fc 100644
--- a/third_party/web_platform_tests/resources/testharness.js
+++ b/third_party/web_platform_tests/resources/testharness.js
@@ -197,6 +197,13 @@
         add_result_callback(function (test) {
             this_obj.output_handler.show_status();
         });
+        // Only show test summary when browsing tests in Chrome.
+        // Cobalt doesn't support html elements at this moment.
+        if (navigator.userAgent.includes("Chrome")) {
+            add_completion_callback(function (tests, harness_status) {
+                this_obj.output_handler.show_results(tests, harness_status);
+            });
+        }
 
         this.setup_messages(settings.message_events);
     };
@@ -544,6 +551,90 @@
     }
 
     /**
+     * Make a copy of a Promise in the current realm.
+     *
+     * @param {Promise} promise the given promise that may be from a different
+     *                          realm
+     * @returns {Promise}
+     *
+     * An arbitrary promise provided by the caller may have originated in
+     * another frame that have since navigated away, rendering the frame's
+     * document inactive. Such a promise cannot be used with `await` or
+     * Promise.resolve(), as microtasks associated with it may be prevented
+     * from being run. See https://github.com/whatwg/html/issues/5319 for a
+     * particular case.
+     *
+     * In functions we define here, there is an expectation from the caller
+     * that the promise is from the current realm, that can always be used with
+     * `await`, etc. We therefore create a new promise in this realm that
+     * inherit the value and status from the given promise.
+     */
+
+    function bring_promise_to_current_realm(promise) {
+        return new Promise(promise.then.bind(promise));
+    }
+
+    function promise_rejects_js(test, constructor, promise, description) {
+        return bring_promise_to_current_realm(promise)
+            .then(test.unreached_func("Should have rejected: " + description))
+            .catch(function(e) {
+                assert_throws_js_impl(constructor, function() { throw e },
+                                          description, "promise_rejects_js");
+            });
+    }
+
+    /**
+     * Assert that a Promise is rejected with the right DOMException.
+     *
+     * @param test the test argument passed to promise_test
+     * @param {number|string} type.  See documentation for assert_throws_dom.
+     *
+     * For the remaining arguments, there are two ways of calling
+     * promise_rejects_dom:
+     *
+     * 1) If the DOMException is expected to come from the current global, the
+     * third argument should be the promise expected to reject, and a fourth,
+     * optional, argument is the assertion description.
+     *
+     * 2) If the DOMException is expected to come from some other global, the
+     * third argument should be the DOMException constructor from that global,
+     * the fourth argument the promise expected to reject, and the fifth,
+     * optional, argument the assertion description.
+     */
+
+    function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) {
+        let constructor, promise, description;
+        if (typeof promiseOrConstructor === "function" &&
+            promiseOrConstructor.name === "DOMException") {
+            constructor = promiseOrConstructor;
+            promise = descriptionOrPromise;
+            description = maybeDescription;
+        } else {
+            constructor = self.DOMException;
+            promise = promiseOrConstructor;
+            description = descriptionOrPromise;
+            assert(maybeDescription === undefined,
+                    "Too many args pased to no-constructor version of promise_rejects_dom");
+        }
+        return bring_promise_to_current_realm(promise)
+            .then(test.unreached_func("Should have rejected: " + description))
+            .catch(function(e) {
+                assert_throws_dom_impl(type, function() { throw e }, description,
+                                        "promise_rejects_dom", constructor);
+            });
+    }
+
+    function promise_rejects_exactly(test, exception, promise, description) {
+        return bring_promise_to_current_realm(promise)
+            .then(test.unreached_func("Should have rejected: " + description))
+            .catch(function(e) {
+                assert_throws_exactly_impl(exception, function() { throw e },
+                                            description, "promise_rejects_exactly");
+            });
+    }
+
+
+    /**
      * This constructor helper allows DOM events to be handled using Promises,
      * which can make it a lot easier to test a very specific series of events,
      * including ensuring that unexpected events are not fired at any point.
@@ -667,6 +758,9 @@
     expose(async_test, 'async_test');
     expose(promise_test, 'promise_test');
     expose(promise_rejects, 'promise_rejects');
+    expose(promise_rejects_js, 'promise_rejects_js');
+    expose(promise_rejects_dom, 'promise_rejects_dom');
+    expose(promise_rejects_exactly, 'promise_rejects_exactly');
     expose(generate_tests, 'generate_tests');
     expose(setup, 'setup');
     expose(done, 'done');
@@ -824,6 +918,41 @@
      * Assertions
      */
 
+    function expose_assert(f, name) {
+        function assert_wrapper(...args) {
+            let status = Test.statuses.TIMEOUT;
+            let stack = null;
+            try {
+                if (settings.debug) {
+                    console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+                }
+                if (tests.output) {
+                    tests.set_assert(name, args);
+                }
+                const rv = f.apply(undefined, args);
+                status = Test.statuses.PASS;
+                return rv;
+            } catch(e) {
+                if (e instanceof AssertionError) {
+                    status = Test.statuses.FAIL;
+                    stack = e.stack;
+                 } else {
+                    status = Test.statuses.ERROR;
+                 }
+                throw e;
+            } finally {
+                if (tests.output && !stack) {
+                    stack = get_stack();
+                }
+                if (tests.output) {
+                    tests.set_assert_status(status, stack);
+                }
+            }
+        }
+        expose(assert_wrapper, name);
+    }
+
+
     function assert_true(actual, description)
     {
         assert(actual === true, "assert_true", description,
@@ -1262,6 +1391,303 @@
     }
     expose(assert_throws, "assert_throws");
 
+    /**
+     * Assert a JS Error with the expected constructor is thrown.
+     *
+     * @param {object} constructor The expected exception constructor.
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws_js(constructor, func, description)
+    {
+        assert_throws_js_impl(constructor, func, description,
+                              "assert_throws_js");
+    }
+    expose_assert(assert_throws_js, "assert_throws_js");
+
+    /**
+     * Like assert_throws_js but allows specifying the assertion type
+     * (assert_throws_js or promise_rejects_js, in practice).
+     */
+    function assert_throws_js_impl(constructor, func, description,
+                                   assertion_type)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            // Basic sanity-checks on the thrown exception.
+            assert(typeof e === "object",
+                    assertion_type, description,
+                    "${func} threw ${e} with type ${type}, not an object",
+                    {func:func, e:e, type:typeof e});
+
+            assert(e !== null,
+                    assertion_type, description,
+                    "${func} threw null, not an object",
+                    {func:func});
+
+            // Basic sanity-check on the passed-in constructor
+            assert(typeof constructor == "function",
+                    assertion_type, description,
+                    "${constructor} is not a constructor",
+                    {constructor:constructor});
+            var obj = constructor;
+            while (obj) {
+                if (typeof obj === "function" &&
+                    obj.name === "Error") {
+                    break;
+                }
+                obj = Object.getPrototypeOf(obj);
+            }
+            assert(obj != null,
+                    assertion_type, description,
+                    "${constructor} is not an Error subtype",
+                    {constructor:constructor});
+
+            // And checking that our exception is reasonable
+            assert(e.constructor === constructor &&
+                    e.name === constructor.name,
+                    assertion_type, description,
+                    "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+                    {func:func, actual:e, actual_name:e.name,
+                    expected:constructor,
+                    expected_name:constructor.name});
+        }
+    }
+
+    /**
+     * Assert a DOMException with the expected type is thrown.
+     *
+     * @param {number|string} type The expected exception name or code.  See the
+     *        table of names and codes at
+     *        https://heycam.github.io/webidl/#dfn-error-names-table
+     *        If a number is passed it should be one of the numeric code values
+     *        in that table (e.g. 3, 4, etc).  If a string is passed it can
+     *        either be an exception name (e.g. "HierarchyRequestError",
+     *        "WrongDocumentError") or the name of the corresponding error code
+     *        (e.g. "HIERARCHY_REQUEST_ERR", "WRONG_DOCUMENT_ERR").
+     *
+     * For the remaining arguments, there are two ways of calling
+     * promise_rejects_dom:
+     *
+     * 1) If the DOMException is expected to come from the current global, the
+     * second argument should be the function expected to throw and a third,
+     * optional, argument is the assertion description.
+     *
+     * 2) If the DOMException is expected to come from some other global, the
+     * second argument should be the DOMException constructor from that global,
+     * the third argument the function expected to throw, and the fourth, optional,
+     * argument the assertion description.
+     */
+    function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription)
+    {
+        let constructor, func, description;
+        if (funcOrConstructor.name === "DOMException") {
+            constructor = funcOrConstructor;
+            func = descriptionOrFunc;
+            description = maybeDescription;
+        } else {
+            constructor = self.DOMException;
+            func = funcOrConstructor;
+            description = descriptionOrFunc;
+            assert(maybeDescription === undefined,
+                    "Too many args pased to no-constructor version of assert_throws_dom");
+        }
+        assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
+    }
+    expose_assert(assert_throws_dom, "assert_throws_dom");
+
+    /**
+     * Similar to assert_throws_dom but allows specifying the assertion type
+     * (assert_throws_dom or promise_rejects_dom, in practice).  The
+     * "constructor" argument must be the DOMException constructor from the
+     * global we expect the exception to come from.
+     */
+    function assert_throws_dom_impl(type, func, description, assertion_type, constructor)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            // Basic sanity-checks on the thrown exception.
+            assert(typeof e === "object",
+                   assertion_type, description,
+                   "${func} threw ${e} with type ${type}, not an object",
+                   {func:func, e:e, type:typeof e});
+
+            assert(e !== null,
+                   assertion_type, description,
+                   "${func} threw null, not an object",
+                   {func:func});
+
+            // Sanity-check our type
+            assert(typeof type == "number" ||
+                   typeof type == "string",
+                   assertion_type, description,
+                   "${type} is not a number or string",
+                   {type:type});
+
+            var codename_name_map = {
+                INDEX_SIZE_ERR: 'IndexSizeError',
+                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+                NOT_FOUND_ERR: 'NotFoundError',
+                NOT_SUPPORTED_ERR: 'NotSupportedError',
+                INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+                INVALID_STATE_ERR: 'InvalidStateError',
+                SYNTAX_ERR: 'SyntaxError',
+                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+                NAMESPACE_ERR: 'NamespaceError',
+                INVALID_ACCESS_ERR: 'InvalidAccessError',
+                TYPE_MISMATCH_ERR: 'TypeMismatchError',
+                SECURITY_ERR: 'SecurityError',
+                NETWORK_ERR: 'NetworkError',
+                ABORT_ERR: 'AbortError',
+                URL_MISMATCH_ERR: 'URLMismatchError',
+                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+                TIMEOUT_ERR: 'TimeoutError',
+                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+                DATA_CLONE_ERR: 'DataCloneError'
+            };
+
+            var name_code_map = {
+                IndexSizeError: 1,
+                HierarchyRequestError: 3,
+                WrongDocumentError: 4,
+                InvalidCharacterError: 5,
+                NoModificationAllowedError: 7,
+                NotFoundError: 8,
+                NotSupportedError: 9,
+                InUseAttributeError: 10,
+                InvalidStateError: 11,
+                SyntaxError: 12,
+                InvalidModificationError: 13,
+                NamespaceError: 14,
+                InvalidAccessError: 15,
+                TypeMismatchError: 17,
+                SecurityError: 18,
+                NetworkError: 19,
+                AbortError: 20,
+                URLMismatchError: 21,
+                QuotaExceededError: 22,
+                TimeoutError: 23,
+                InvalidNodeTypeError: 24,
+                DataCloneError: 25,
+
+                EncodingError: 0,
+                NotReadableError: 0,
+                UnknownError: 0,
+                ConstraintError: 0,
+                DataError: 0,
+                TransactionInactiveError: 0,
+                ReadOnlyError: 0,
+                VersionError: 0,
+                OperationError: 0,
+                NotAllowedError: 0
+            };
+
+            var code_name_map = {};
+            for (var key in name_code_map) {
+                if (name_code_map[key] > 0) {
+                    code_name_map[name_code_map[key]] = key;
+                }
+            }
+
+            var required_props = {};
+            var name;
+
+            if (typeof type === "number") {
+                if (type === 0) {
+                    throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+                } else if (!(type in code_name_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+                }
+                name = code_name_map[type];
+                required_props.code = type;
+            } else if (typeof type === "string") {
+                name = type in codename_name_map ? codename_name_map[type] : type;
+                if (!(name in name_code_map)) {
+                    throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+                }
+
+                required_props.code = name_code_map[name];
+            }
+
+            if (required_props.code === 0 ||
+                ("name" in e &&
+                e.name !== e.name.toUpperCase() &&
+                e.name !== "DOMException")) {
+                // New style exception: also test the name property.
+                required_props.name = name;
+            }
+
+            for (var prop in required_props) {
+                assert(prop in e && e[prop] == required_props[prop],
+                       assertion_type, description,
+                       "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+            }
+
+            // Check that the exception is from the right global.  This check is last
+            // so more specific, and more informative, checks on the properties can
+            // happen in case a totally incorrect exception is thrown.
+            assert(e.constructor === constructor,
+                   assertion_type, description,
+                   "${func} threw an exception from the wrong global",
+                   {func});
+
+        }
+    }
+
+    /**
+     * Assert the provided value is thrown.
+     *
+     * @param {value} exception The expected exception.
+     * @param {Function} func Function which should throw.
+     * @param {string} description Error description for the case that the error is not thrown.
+     */
+    function assert_throws_exactly(exception, func, description)
+    {
+        assert_throws_exactly_impl(exception, func, description,
+                                   "assert_throws_exactly");
+    }
+    expose_assert(assert_throws_exactly, "assert_throws_exactly");
+
+    /**
+     * Like assert_throws_exactly but allows specifying the assertion type
+     * (assert_throws_exactly or promise_rejects_exactly, in practice).
+     */
+    function assert_throws_exactly_impl(exception, func, description,
+                                        assertion_type)
+    {
+        try {
+            func.call(this);
+            assert(false, assertion_type, description,
+                   "${func} did not throw", {func:func});
+        } catch (e) {
+            if (e instanceof AssertionError) {
+                throw e;
+            }
+
+            assert(same_value(e, exception), assertion_type, description,
+                   "${func} threw ${e} but we expected it to throw ${exception}",
+                   {func:func, e:e, exception:exception});
+        }
+    }
+
     function assert_unreached(description) {
          assert(false, "assert_unreached", description,
                 "Reached unreachable code");
diff --git a/third_party/web_platform_tests/service-workers/META.yml b/third_party/web_platform_tests/service-workers/META.yml
new file mode 100644
index 0000000..03a0dd0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/META.yml
@@ -0,0 +1,6 @@
+spec: https://w3c.github.io/ServiceWorker/
+suggested_reviewers:
+  - asutherland
+  - mkruisselbrink
+  - mattto
+  - wanderview
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/META.yml b/third_party/web_platform_tests/service-workers/cache-storage/META.yml
new file mode 100644
index 0000000..bf34474
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+  - inexorabletash
+  - wanderview
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js
new file mode 100644
index 0000000..960d1bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js
@@ -0,0 +1,81 @@
+// META: title=Cache Storage: Abort
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+// We perform the same tests on put, add, addAll. Parameterise the tests to
+// reduce repetition.
+const methodsToTest = {
+  put: async (cache, request) => {
+    const response = await fetch(request);
+    return cache.put(request, response);
+  },
+  add: async (cache, request) => cache.add(request),
+  addAll: async (cache, request) => cache.addAll([request]),
+};
+
+for (const method in methodsToTest) {
+  const perform = methodsToTest[method];
+
+  cache_test(async (cache, test) => {
+    const controller = new AbortController();
+    const signal = controller.signal;
+    controller.abort();
+    const request = new Request('../resources/simple.txt', { signal });
+    return promise_rejects_dom(test, 'AbortError', perform(cache, request),
+                          `${method} should reject`);
+  }, `${method}() on an already-aborted request should reject with AbortError`);
+
+  cache_test(async (cache, test) => {
+    const controller = new AbortController();
+    const signal = controller.signal;
+    const request = new Request('../resources/simple.txt', { signal });
+    const promise = perform(cache, request);
+    controller.abort();
+    return promise_rejects_dom(test, 'AbortError', promise,
+                          `${method} should reject`);
+  }, `${method}() synchronously followed by abort should reject with ` +
+     `AbortError`);
+
+  cache_test(async (cache, test) => {
+    const controller = new AbortController();
+    const signal = controller.signal;
+    const stateKey = token();
+    const abortKey = token();
+    const request = new Request(
+        `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
+        { signal });
+
+    const promise = perform(cache, request);
+
+    // Wait for the server to start sending the response body.
+    let opened = false;
+    do {
+      // Normally only one fetch to 'stash-take' is needed, but the fetches
+      // will be served in reverse order sometimes
+      // (i.e., 'stash-take' gets served before 'infinite-slow-response').
+
+      const response =
+            await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
+      const body = await response.json();
+      if (body === 'open') opened = true;
+    } while (!opened);
+
+    // Sadly the above loop cannot guarantee that the browser has started
+    // processing the response body. This delay is needed to make the test
+    // failures non-flaky in Chrome version 66. My deepest apologies.
+    await new Promise(resolve => setTimeout(resolve, 250));
+
+    controller.abort();
+
+    await promise_rejects_dom(test, 'AbortError', promise,
+                          `${method} should reject`);
+
+    // infinite-slow-response.py doesn't know when to stop.
+    return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
+  }, `${method}() followed by abort after headers received should reject ` +
+     `with AbortError`);
+}
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js
new file mode 100644
index 0000000..eca516a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js
@@ -0,0 +1,368 @@
+// META: title=Cache.add and Cache.addAll
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add(),
+      'Cache.add should throw a TypeError when no arguments are given.');
+  }, 'Cache.add called with no arguments');
+
+cache_test(function(cache) {
+    return cache.add('./resources/simple.txt')
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.add should resolve with undefined on success.');
+          return cache.match('./resources/simple.txt');
+        })
+        .then(function(response) {
+          assert_class_string(response, 'Response',
+                              'Cache.add should put a resource in the cache.');
+          return response.text();
+        })
+        .then(function(body) {
+          assert_equals(body, 'a simple text file\n',
+                        'Cache.add should retrieve the correct body.');
+        });
+  }, 'Cache.add called with relative URL specified as a string');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add('javascript://this-is-not-http-mmkay'),
+      'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
+  }, 'Cache.add called with non-HTTP/HTTPS URL');
+
+cache_test(function(cache) {
+    var request = new Request('./resources/simple.txt');
+    return cache.add(request)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.add should resolve with undefined on success.');
+        });
+  }, 'Cache.add called with Request object');
+
+cache_test(function(cache, test) {
+    var request = new Request('./resources/simple.txt',
+                              {method: 'POST', body: 'This is a body.'});
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add(request),
+      'Cache.add should throw a TypeError for non-GET requests.');
+  }, 'Cache.add called with POST request');
+
+cache_test(function(cache) {
+    var request = new Request('./resources/simple.txt');
+    return cache.add(request)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.add should resolve with undefined on success.');
+        })
+      .then(function() {
+          return cache.add(request);
+        })
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.add should resolve with undefined on success.');
+        });
+  }, 'Cache.add called twice with the same Request object');
+
+cache_test(function(cache) {
+    var request = new Request('./resources/simple.txt');
+    return request.text()
+      .then(function() {
+          assert_false(request.bodyUsed);
+        })
+      .then(function() {
+          return cache.add(request);
+        });
+  }, 'Cache.add with request with null body (not consumed)');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add('./resources/fetch-status.py?status=206'),
+      'Cache.add should reject on partial response');
+  }, 'Cache.add with 206 response');
+
+cache_test(function(cache, test) {
+    var urls = ['./resources/fetch-status.py?status=206',
+                './resources/fetch-status.py?status=200'];
+    var requests = urls.map(function(url) {
+        return new Request(url);
+      });
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.addAll(requests),
+      'Cache.addAll should reject with TypeError if any request fails');
+  }, 'Cache.addAll with 206 response');
+
+cache_test(function(cache, test) {
+    var urls = ['./resources/fetch-status.py?status=206',
+                './resources/fetch-status.py?status=200'];
+    var requests = urls.map(function(url) {
+        var cross_origin_url = new URL(url, location.href);
+        cross_origin_url.hostname = REMOTE_HOST;
+        return new Request(cross_origin_url.href, { mode: 'no-cors' });
+      });
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.addAll(requests),
+      'Cache.addAll should reject with TypeError if any request fails');
+  }, 'Cache.addAll with opaque-filtered 206 response');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add('this-does-not-exist-please-dont-create-it'),
+      'Cache.add should reject if response is !ok');
+  }, 'Cache.add with request that results in a status of 404');
+
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.add('./resources/fetch-status.py?status=500'),
+      'Cache.add should reject if response is !ok');
+  }, 'Cache.add with request that results in a status of 500');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.addAll(),
+      'Cache.addAll with no arguments should throw TypeError.');
+  }, 'Cache.addAll with no arguments');
+
+cache_test(function(cache, test) {
+    // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
+    var urls = ['./resources/simple.txt', undefined, './resources/blank.html'];
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.addAll(urls),
+      'Cache.addAll should throw TypeError for an undefined argument.');
+  }, 'Cache.addAll with a mix of valid and undefined arguments');
+
+cache_test(function(cache) {
+    return cache.addAll([])
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.addAll should resolve with undefined on ' +
+                        'success.');
+          return cache.keys();
+        })
+      .then(function(result) {
+          assert_equals(result.length, 0,
+                        'There should be no entry in the cache.');
+        });
+  }, 'Cache.addAll with an empty array');
+
+cache_test(function(cache) {
+    // Assumes the existence of ../resources/simple.txt and
+    // ../resources/blank.html
+    var urls = ['./resources/simple.txt',
+                self.location.href,
+                './resources/blank.html'];
+    return cache.addAll(urls)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.addAll should resolve with undefined on ' +
+                        'success.');
+          return Promise.all(
+            urls.map(function(url) { return cache.match(url); }));
+        })
+      .then(function(responses) {
+          assert_class_string(
+            responses[0], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          assert_class_string(
+            responses[1], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          assert_class_string(
+            responses[2], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          return Promise.all(
+            responses.map(function(response) { return response.text(); }));
+        })
+      .then(function(bodies) {
+          assert_equals(
+            bodies[0], 'a simple text file\n',
+            'Cache.add should retrieve the correct body.');
+          assert_equals(
+            bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+            'Cache.add should retrieve the correct body.');
+        });
+  }, 'Cache.addAll with string URL arguments');
+
+cache_test(function(cache) {
+    // Assumes the existence of ../resources/simple.txt and
+    // ../resources/blank.html
+    var urls = ['./resources/simple.txt',
+                self.location.href,
+                './resources/blank.html'];
+    var requests = urls.map(function(url) {
+        return new Request(url);
+      });
+    return cache.addAll(requests)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.addAll should resolve with undefined on ' +
+                        'success.');
+          return Promise.all(
+            urls.map(function(url) { return cache.match(url); }));
+        })
+      .then(function(responses) {
+          assert_class_string(
+            responses[0], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          assert_class_string(
+            responses[1], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          assert_class_string(
+            responses[2], 'Response',
+            'Cache.addAll should put a resource in the cache.');
+          return Promise.all(
+            responses.map(function(response) { return response.text(); }));
+        })
+      .then(function(bodies) {
+          assert_equals(
+            bodies[0], 'a simple text file\n',
+            'Cache.add should retrieve the correct body.');
+          assert_equals(
+            bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+            'Cache.add should retrieve the correct body.');
+        });
+  }, 'Cache.addAll with Request arguments');
+
+cache_test(function(cache, test) {
+    // Assumes that ../resources/simple.txt and ../resources/blank.html exist.
+    // The second resource does not.
+    var urls = ['./resources/simple.txt',
+                'this-resource-should-not-exist',
+                './resources/blank.html'];
+    var requests = urls.map(function(url) {
+        return new Request(url);
+      });
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.addAll(requests),
+      'Cache.addAll should reject with TypeError if any request fails')
+      .then(function() {
+          return Promise.all(urls.map(function(url) {
+              return cache.match(url);
+            }));
+      })
+      .then(function(matches) {
+          assert_array_equals(
+            matches,
+            [undefined, undefined, undefined],
+            'If any response fails, no response should be added to cache');
+      });
+  }, 'Cache.addAll with a mix of succeeding and failing requests');
+
+cache_test(function(cache, test) {
+    var request = new Request('../resources/simple.txt');
+    return promise_rejects_dom(
+      test,
+      'InvalidStateError',
+      cache.addAll([request, request]),
+      'Cache.addAll should throw InvalidStateError if the same request is added ' +
+      'twice.');
+  }, 'Cache.addAll called with the same Request object specified twice');
+
+cache_test(async function(cache, test) {
+    const url = './resources/vary.py?vary=x-shape';
+    let requests = [
+      new Request(url, { headers: { 'x-shape': 'circle' }}),
+      new Request(url, { headers: { 'x-shape': 'square' }}),
+    ];
+    let result = await cache.addAll(requests);
+    assert_equals(result, undefined, 'Cache.addAll() should succeed');
+  }, 'Cache.addAll should succeed when entries differ by vary header');
+
+cache_test(async function(cache, test) {
+    const url = './resources/vary.py?vary=x-shape';
+    let requests = [
+      new Request(url, { headers: { 'x-shape': 'circle' }}),
+      new Request(url, { headers: { 'x-shape': 'circle' }}),
+    ];
+    await promise_rejects_dom(
+      test,
+      'InvalidStateError',
+      cache.addAll(requests),
+      'Cache.addAll() should reject when entries are duplicate by vary header');
+  }, 'Cache.addAll should reject when entries are duplicate by vary header');
+
+// VARY header matching is asymmetric.  Determining if two entries are duplicate
+// depends on which entry's response is used in the comparison.  The target
+// response's VARY header determines what request headers are examined.  This
+// test verifies that Cache.addAll() duplicate checking handles this asymmetric
+// behavior correctly.
+cache_test(async function(cache, test) {
+    const base_url = './resources/vary.py';
+
+    // Define a request URL that sets a VARY header in the
+    // query string to be echoed back by the server.
+    const url = base_url + '?vary=x-size';
+
+    // Set a cookie to override the VARY header of the response
+    // when the request is made with credentials.  This will
+    // take precedence over the query string vary param.  This
+    // is a bit confusing, but it's necessary to construct a test
+    // where the URL is the same, but the VARY headers differ.
+    //
+    // Note, the test could also pass this information in additional
+    // request headers.  If the cookie approach becomes too unwieldy
+    // this test could be rewritten to use that technique.
+    await fetch(base_url + '?set-vary-value-override-cookie=x-shape');
+    test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie'));
+
+    let requests = [
+      // This request will result in a Response with a "Vary: x-shape"
+      // header.  This *will not* result in a duplicate match with the
+      // other entry.
+      new Request(url, { headers: { 'x-shape': 'circle',
+                                    'x-size': 'big' },
+                         credentials: 'same-origin' }),
+
+      // This request will result in a Response with a "Vary: x-size"
+      // header.  This *will* result in a duplicate match with the other
+      // entry.
+      new Request(url, { headers: { 'x-shape': 'square',
+                                    'x-size': 'big' },
+                         credentials: 'omit' }),
+    ];
+    await promise_rejects_dom(
+      test,
+      'InvalidStateError',
+      cache.addAll(requests),
+      'Cache.addAll() should reject when one entry has a vary header ' +
+      'matching an earlier entry.');
+
+    // Test the reverse order now.
+    await promise_rejects_dom(
+      test,
+      'InvalidStateError',
+      cache.addAll(requests.reverse()),
+      'Cache.addAll() should reject when one entry has a vary header ' +
+      'matching a later entry.');
+
+  }, 'Cache.addAll should reject when one entry has a vary header ' +
+     'matching another entry');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js
new file mode 100644
index 0000000..3eae2b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js
@@ -0,0 +1,164 @@
+// META: title=Cache.delete
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+
+// Construct a generic Request object. The URL is |test_url|. All other fields
+// are defaults.
+function new_test_request() {
+  return new Request(test_url);
+}
+
+// Construct a generic Response object.
+function new_test_response() {
+  return new Response('Hello world!', { status: 200 });
+}
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.delete(),
+      'Cache.delete should reject with a TypeError when called with no ' +
+      'arguments.');
+  }, 'Cache.delete with no arguments');
+
+cache_test(function(cache) {
+    return cache.put(new_test_request(), new_test_response())
+      .then(function() {
+          return cache.delete(test_url);
+        })
+      .then(function(result) {
+          assert_true(result,
+                      'Cache.delete should resolve with "true" if an entry ' +
+                      'was successfully deleted.');
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_equals(result, undefined,
+            'Cache.delete should remove matching entries from cache.');
+        });
+  }, 'Cache.delete called with a string URL');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    return cache.put(request, new_test_response())
+      .then(function() {
+          return cache.delete(request);
+        })
+      .then(function(result) {
+          assert_true(result,
+                      'Cache.delete should resolve with "true" if an entry ' +
+                      'was successfully deleted.');
+        });
+  }, 'Cache.delete called with a Request object');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new_test_response();
+    return cache.put(request, response)
+      .then(function() {
+          return cache.delete(new Request(test_url, {method: 'HEAD'}));
+        })
+      .then(function(result) {
+          assert_false(result,
+                       'Cache.delete should not match a non-GET request ' +
+                       'unless ignoreMethod option is set.');
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, response,
+            'Cache.delete should leave non-matching response in the cache.');
+          return cache.delete(new Request(test_url, {method: 'HEAD'}),
+                              {ignoreMethod: true});
+        })
+      .then(function(result) {
+          assert_true(result,
+                      'Cache.delete should match a non-GET request ' +
+                      ' if ignoreMethod is true.');
+        });
+  }, 'Cache.delete called with a HEAD request');
+
+cache_test(function(cache) {
+    var vary_request = new Request('http://example.com/c',
+                                   {headers: {'Cookies': 'is-for-cookie'}});
+    var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+    var mismatched_vary_request = new Request('http://example.com/c');
+
+    return cache.put(vary_request.clone(), vary_response.clone())
+      .then(function() {
+          return cache.delete(mismatched_vary_request.clone());
+        })
+      .then(function(result) {
+          assert_false(result,
+                       'Cache.delete should not delete if vary does not ' +
+                       'match unless ignoreVary is true');
+          return cache.delete(mismatched_vary_request.clone(),
+                              {ignoreVary: true});
+        })
+      .then(function(result) {
+          assert_true(result,
+                      'Cache.delete should ignore vary if ignoreVary is true');
+        });
+  }, 'Cache.delete supports ignoreVary');
+
+cache_test(function(cache) {
+    return cache.delete(test_url)
+      .then(function(result) {
+          assert_false(result,
+                       'Cache.delete should resolve with "false" if there ' +
+                       'are no matching entries.');
+        });
+  }, 'Cache.delete with a non-existent entry');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a_with_query.request,
+                          { ignoreSearch: true })
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ]);
+          return cache.delete(entries.a_with_query.request,
+                              { ignoreSearch: true });
+        })
+      .then(function(result) {
+          return cache.matchAll(entries.a_with_query.request,
+                                { ignoreSearch: true });
+        })
+      .then(function(result) {
+          assert_response_array_equals(result, []);
+        });
+  },
+  'Cache.delete with ignoreSearch option (request with search parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a_with_query.request,
+                          { ignoreSearch: true })
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ]);
+          // cache.delete()'s behavior should be the same if ignoreSearch is
+          // not provided or if ignoreSearch is false.
+          return cache.delete(entries.a_with_query.request,
+                              { ignoreSearch: false });
+        })
+      .then(function(result) {
+          return cache.matchAll(entries.a_with_query.request,
+                                { ignoreSearch: true });
+        })
+      .then(function(result) {
+          assert_response_array_equals(result, [ entries.a.response ]);
+        });
+  },
+  'Cache.delete with ignoreSearch option (when it is specified as false)');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000..3c96348
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = './resources/cache-keys-attributes-for-service-worker.js';
+
+function wait(ms) {
+  return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+    const scope = './resources/blank.html?name=isReloadNavigation';
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: false, stored: false');
+      await new Promise((resolve) => {
+        frame.onload = resolve;
+        frame.contentWindow.location.reload();
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: true, stored: true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+}, 'Request.IsReloadNavigation should persist.');
+
+promise_test(async (t) => {
+    const scope = './resources/blank.html?name=isHistoryNavigation';
+    let frame;
+    let reg;
+
+    try {
+      reg = await service_worker_unregister_and_register(t, worker, scope);
+      await wait_for_state(t, reg.installing, 'activated');
+      frame = await with_iframe(scope);
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: false, stored: false');
+      // Use step_timeout(0) to ensure the history entry is created for Blink
+      // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+      await wait(0);
+      await new Promise((resolve) => {
+        frame.onload = resolve;
+        frame.src = '../resources/blank.html?ignore';
+      });
+      await wait(0);
+      await new Promise((resolve) => {
+        frame.onload = resolve;
+        frame.contentWindow.history.go(-1);
+      });
+      assert_equals(frame.contentDocument.body.textContent,
+                    'original: true, stored: true');
+    } finally {
+      if (frame) {
+        frame.remove();
+      }
+      if (reg) {
+        await reg.unregister();
+      }
+    }
+}, 'Request.IsHistoryNavigation should persist.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js
new file mode 100644
index 0000000..232fb76
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js
@@ -0,0 +1,212 @@
+// META: title=Cache.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+cache_test(cache => {
+    return cache.keys()
+      .then(requests => {
+          assert_equals(
+            requests.length, 0,
+            'Cache.keys should resolve to an empty array for an empty cache');
+        });
+  }, 'Cache.keys() called on an empty cache');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys('not-present-in-the-cache')
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [],
+            'Cache.keys should resolve with an empty array on failure.');
+        });
+  }, 'Cache.keys with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(entries.a.request.url)
+      .then(function(result) {
+          assert_request_array_equals(result, [entries.a.request],
+                                      'Cache.keys should match by URL.');
+        });
+  }, 'Cache.keys with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(entries.a.request)
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [entries.a.request],
+            'Cache.keys should match by Request.');
+        });
+  }, 'Cache.keys with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(new Request(entries.a.request.url))
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [entries.a.request],
+            'Cache.keys should match by Request.');
+        });
+  }, 'Cache.keys with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(entries.a.request, {ignoreSearch: true})
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              entries.a.request,
+              entries.a_with_query.request
+            ],
+            'Cache.keys with ignoreSearch should ignore the ' +
+            'search parameters of cached request.');
+        });
+  },
+  'Cache.keys with ignoreSearch option (request with no search ' +
+  'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(entries.a_with_query.request, {ignoreSearch: true})
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              entries.a.request,
+              entries.a_with_query.request
+            ],
+            'Cache.keys with ignoreSearch should ignore the ' +
+            'search parameters of request.');
+        });
+  },
+  'Cache.keys with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com/');
+    var head_request = new Request('http://example.com/', {method: 'HEAD'});
+    var response = new Response('foo');
+    return cache.put(request.clone(), response.clone())
+      .then(function() {
+          return cache.keys(head_request.clone());
+        })
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [],
+            'Cache.keys should resolve with an empty array with a ' +
+            'mismatched method.');
+          return cache.keys(head_request.clone(),
+                            {ignoreMethod: true});
+        })
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              request,
+            ],
+            'Cache.keys with ignoreMethod should ignore the ' +
+            'method of request.');
+        });
+  }, 'Cache.keys supports ignoreMethod');
+
+cache_test(function(cache) {
+    var vary_request = new Request('http://example.com/c',
+                                   {headers: {'Cookies': 'is-for-cookie'}});
+    var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+    var mismatched_vary_request = new Request('http://example.com/c');
+
+    return cache.put(vary_request.clone(), vary_response.clone())
+      .then(function() {
+          return cache.keys(mismatched_vary_request.clone());
+        })
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [],
+            'Cache.keys should resolve with an empty array with a ' +
+            'mismatched vary.');
+          return cache.keys(mismatched_vary_request.clone(),
+                              {ignoreVary: true});
+        })
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              vary_request,
+            ],
+            'Cache.keys with ignoreVary should ignore the ' +
+            'vary of request.');
+        });
+  }, 'Cache.keys supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(entries.cat.request.url + '#mouse')
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              entries.cat.request,
+            ],
+            'Cache.keys should ignore URL fragment.');
+        });
+  }, 'Cache.keys with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys('http')
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [],
+            'Cache.keys should treat query as a URL and not ' +
+            'just a string fragment.');
+        });
+  }, 'Cache.keys with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys()
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            simple_entries.map(entry => entry.request),
+            'Cache.keys without parameters should match all entries.');
+        });
+  }, 'Cache.keys without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(undefined)
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            simple_entries.map(entry => entry.request),
+            'Cache.keys with undefined request should match all entries.');
+        });
+  }, 'Cache.keys with explicitly undefined request');
+
+cache_test(cache => {
+    return cache.keys(undefined, {})
+      .then(requests => {
+          assert_equals(
+            requests.length, 0,
+            'Cache.keys should resolve to an empty array for an empty cache');
+        });
+  }, 'Cache.keys with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+    return cache.keys()
+      .then(function(result) {
+          assert_request_array_equals(
+            result,
+            [
+              entries.vary_cookie_is_cookie.request,
+              entries.vary_cookie_is_good.request,
+              entries.vary_cookie_absent.request,
+            ],
+            'Cache.keys without parameters should match all entries.');
+        });
+  }, 'Cache.keys without parameters and VARY entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'}))
+      .then(function(result) {
+          assert_request_array_equals(
+            result, [],
+            'Cache.keys should not match HEAD request unless ignoreMethod ' +
+            'option is set.');
+        });
+  }, 'Cache.keys with a HEAD Request');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js
new file mode 100644
index 0000000..9ca4590
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js
@@ -0,0 +1,437 @@
+// META: title=Cache.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/get-host-info.sub.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match('not-present-in-the-cache')
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.match failures should resolve with undefined.');
+        });
+  }, 'Cache.match with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(entries.a.request.url)
+      .then(function(result) {
+          assert_response_equals(result, entries.a.response,
+                                 'Cache.match should match by URL.');
+        });
+  }, 'Cache.match with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(entries.a.request)
+      .then(function(result) {
+          assert_response_equals(result, entries.a.response,
+                                 'Cache.match should match by Request.');
+        });
+  }, 'Cache.match with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    var alt_response = new Response('', {status: 201});
+
+    return self.caches.open('second_matching_cache')
+      .then(function(cache) {
+          return cache.put(entries.a.request, alt_response.clone());
+        })
+      .then(function() {
+          return cache.match(entries.a.request);
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, entries.a.response,
+            'Cache.match should match the first cache.');
+        });
+  }, 'Cache.match with multiple cache hits');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(new Request(entries.a.request.url))
+      .then(function(result) {
+          assert_response_equals(result, entries.a.response,
+                                 'Cache.match should match by Request.');
+        });
+  }, 'Cache.match with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(new Request(entries.a.request.url, {method: 'HEAD'}))
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.match should not match HEAD Request.');
+        });
+  }, 'Cache.match with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(entries.a.request,
+                       {ignoreSearch: true})
+      .then(function(result) {
+          assert_response_in_array(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ],
+            'Cache.match with ignoreSearch should ignore the ' +
+            'search parameters of cached request.');
+        });
+  },
+  'Cache.match with ignoreSearch option (request with no search ' +
+  'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(entries.a_with_query.request,
+                       {ignoreSearch: true})
+      .then(function(result) {
+          assert_response_in_array(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ],
+            'Cache.match with ignoreSearch should ignore the ' +
+            'search parameters of request.');
+        });
+  },
+  'Cache.match with ignoreSearch option (request with search parameter)');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com/');
+    var head_request = new Request('http://example.com/', {method: 'HEAD'});
+    var response = new Response('foo');
+    return cache.put(request.clone(), response.clone())
+      .then(function() {
+          return cache.match(head_request.clone());
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'Cache.match should resolve as undefined with a ' +
+            'mismatched method.');
+          return cache.match(head_request.clone(),
+                             {ignoreMethod: true});
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, response,
+            'Cache.match with ignoreMethod should ignore the ' +
+            'method of request.');
+        });
+  }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+    var vary_request = new Request('http://example.com/c',
+                                   {headers: {'Cookies': 'is-for-cookie'}});
+    var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+    var mismatched_vary_request = new Request('http://example.com/c');
+
+    return cache.put(vary_request.clone(), vary_response.clone())
+      .then(function() {
+          return cache.match(mismatched_vary_request.clone());
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'Cache.match should resolve as undefined with a ' +
+            'mismatched vary.');
+          return cache.match(mismatched_vary_request.clone(),
+                              {ignoreVary: true});
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, vary_response,
+            'Cache.match with ignoreVary should ignore the ' +
+            'vary of request.');
+        });
+  }, 'Cache.match supports ignoreVary');
+
+cache_test(function(cache) {
+    let has_cache_name = false;
+    const opts = {
+      get cacheName() {
+        has_cache_name = true;
+        return undefined;
+      }
+    };
+    return self.caches.open('foo')
+      .then(function() {
+          return cache.match('bar', opts);
+        })
+      .then(function() {
+          assert_false(has_cache_name,
+                       'Cache.match does not support cacheName option ' +
+                       'which was removed in CacheQueryOptions.');
+        });
+  }, 'Cache.match does not support cacheName option');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match(entries.cat.request.url + '#mouse')
+      .then(function(result) {
+          assert_response_equals(result, entries.cat.response,
+                                 'Cache.match should ignore URL fragment.');
+        });
+  }, 'Cache.match with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.match('http')
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'Cache.match should treat query as a URL and not ' +
+            'just a string fragment.');
+        });
+  }, 'Cache.match with string fragment "http" as query');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+    return cache.match('http://example.com/c')
+      .then(function(result) {
+          assert_response_in_array(
+            result,
+            [
+              entries.vary_cookie_absent.response
+            ],
+            'Cache.match should honor "Vary" header.');
+        });
+  }, 'Cache.match with responses containing "Vary" header');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com');
+    var response;
+    var request_url = new URL('./resources/simple.txt', location.href).href;
+    return fetch(request_url)
+      .then(function(fetch_result) {
+          response = fetch_result;
+          assert_equals(
+            response.url, request_url,
+            '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
+            'Reponse.url should return the URL of the response.');
+          return cache.put(request, response.clone());
+        })
+      .then(function() {
+          return cache.match(request.url);
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, response,
+            'Cache.match should return a Response object that has the same ' +
+            'properties as the stored response.');
+          return cache.match(response.url);
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'Cache.match should not match cache entry based on response URL.');
+        });
+  }, 'Cache.match with Request and Response objects with different URLs');
+
+cache_test(function(cache) {
+    var request_url = new URL('./resources/simple.txt', location.href).href;
+    return fetch(request_url)
+      .then(function(fetch_result) {
+          return cache.put(new Request(request_url), fetch_result);
+        })
+      .then(function() {
+          return cache.match(request_url);
+        })
+      .then(function(result) {
+          return result.text();
+        })
+      .then(function(body_text) {
+          assert_equals(body_text, 'a simple text file\n',
+                        'Cache.match should return a Response object with a ' +
+                        'valid body.');
+        })
+      .then(function() {
+          return cache.match(request_url);
+        })
+      .then(function(result) {
+          return result.text();
+        })
+      .then(function(body_text) {
+          assert_equals(body_text, 'a simple text file\n',
+                        'Cache.match should return a Response object with a ' +
+                        'valid body each time it is called.');
+        });
+  }, 'Cache.match invoked multiple times for the same Request/Response');
+
+cache_test(function(cache) {
+    var request_url = new URL('./resources/simple.txt', location.href).href;
+    return fetch(request_url)
+      .then(function(fetch_result) {
+          return cache.put(new Request(request_url), fetch_result);
+        })
+      .then(function() {
+          return cache.match(request_url);
+        })
+      .then(function(result) {
+          return result.blob();
+        })
+      .then(function(blob) {
+          var sliced = blob.slice(2,8);
+
+          return new Promise(function (resolve, reject) {
+              var reader = new FileReader();
+              reader.onloadend = function(event) {
+                resolve(event.target.result);
+              };
+              reader.readAsText(sliced);
+            });
+        })
+      .then(function(text) {
+          assert_equals(text, 'simple',
+                        'A Response blob returned by Cache.match should be ' +
+                        'sliceable.' );
+        });
+  }, 'Cache.match blob should be sliceable');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    var request = new Request(entries.a.request.clone(), {method: 'POST'});
+    return cache.match(request)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.match should not find a match');
+        });
+  }, 'Cache.match with POST Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    var response = entries.non_2xx_response.response;
+    return cache.match(entries.non_2xx_response.request.url)
+      .then(function(result) {
+          assert_response_equals(
+              result, entries.non_2xx_response.response,
+              'Cache.match should return a Response object that has the ' +
+                  'same properties as a stored non-2xx response.');
+        });
+  }, 'Cache.match with a non-2xx Response');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    var response = entries.error_response.response;
+    return cache.match(entries.error_response.request.url)
+      .then(function(result) {
+          assert_response_equals(
+              result, entries.error_response.response,
+              'Cache.match should return a Response object that has the ' +
+                  'same properties as a stored network error response.');
+        });
+  }, 'Cache.match with a network error Response');
+
+cache_test(function(cache) {
+    // This test validates that we can get a Response from the Cache API,
+    // clone it, and read just one side of the clone.  This was previously
+    // bugged in FF for Responses with large bodies.
+    var data = [];
+    data.length = 80 * 1024;
+    data.fill('F');
+    var response;
+    return cache.put('/', new Response(data.toString()))
+      .then(function(result) {
+          return cache.match('/');
+        })
+      .then(function(r) {
+          // Make sure the original response is not GC'd.
+          response = r;
+          // Return only the clone.  We purposefully test that the other
+          // half of the clone does not need to be read here.
+          return response.clone().text();
+        })
+      .then(function(text) {
+          assert_equals(text, data.toString(), 'cloned body text can be read correctly');
+        });
+  }, 'Cache produces large Responses that can be cloned and read correctly.');
+
+cache_test(async (cache) => {
+    const url = get_host_info().HTTPS_REMOTE_ORIGIN +
+      '/service-workers/cache-storage/resources/simple.txt?pipe=' +
+      'header(access-control-allow-origin,*)|' +
+      'header(access-control-expose-headers,*)|' +
+      'header(foo,bar)|' +
+      'header(set-cookie,X)';
+
+    const response = await fetch(url);
+    await cache.put(new Request(url), response);
+    const cached_response = await cache.match(url);
+
+    const headers = cached_response.headers;
+    assert_equals(headers.get('access-control-expose-headers'), '*');
+    assert_equals(headers.get('foo'), 'bar');
+    assert_equals(headers.get('set-cookie'), null);
+  }, 'cors-exposed header should be stored correctly.');
+
+cache_test(async (cache) => {
+    // A URL that should load a resource with a known mime type.
+    const url = '/service-workers/cache-storage/resources/blank.html';
+    const expected_mime_type = 'text/html';
+
+    // Verify we get the expected mime type from the network.  Note,
+    // we cannot use an exact match here since some browsers append
+    // character encoding information to the blob.type value.
+    const net_response = await fetch(url);
+    const net_mime_type = (await net_response.blob()).type;
+    assert_true(net_mime_type.includes(expected_mime_type),
+                'network response should include the expected mime type');
+
+    // Verify we get the exact same mime type when reading the same
+    // URL resource back out of the cache.
+    await cache.add(url);
+    const cache_response = await cache.match(url);
+    const cache_mime_type = (await cache_response.blob()).type;
+    assert_equals(cache_mime_type, net_mime_type,
+                  'network and cache response mime types should match');
+  }, 'MIME type should be set from content-header correctly.');
+
+cache_test(async (cache) => {
+    const url = '/dummy';
+    const original_type = 'text/html';
+    const override_type = 'text/plain';
+    const init_with_headers = {
+      headers: {
+        'content-type': original_type
+      }
+    }
+
+    // Verify constructing a synthetic response with a content-type header
+    // gets the correct mime type.
+    const response = new Response('hello world', init_with_headers);
+    const original_response_type = (await response.blob()).type;
+    assert_true(original_response_type.includes(original_type),
+                'original response should include the expected mime type');
+
+    // Verify overwriting the content-type header changes the mime type.
+    const overwritten_response = new Response('hello world', init_with_headers);
+    overwritten_response.headers.set('content-type', override_type);
+    const overwritten_response_type = (await overwritten_response.blob()).type;
+    assert_equals(overwritten_response_type, override_type,
+                  'mime type can be overridden');
+
+    // Verify the Response read from Cache uses the original mime type
+    // computed when it was first constructed.
+    const tmp = new Response('hello world', init_with_headers);
+    tmp.headers.set('content-type', override_type);
+    await cache.put(url, tmp);
+    const cache_response = await cache.match(url);
+    const cache_mime_type = (await cache_response.blob()).type;
+    assert_equals(cache_mime_type, override_type,
+                  'overwritten and cached response mime types should match');
+  }, 'MIME type should reflect Content-Type headers of response.');
+
+cache_test(async (cache) => {
+  const url = new URL('./resources/vary.py?vary=foo',
+      get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+  const original_request = new Request(url, { mode: 'no-cors',
+                                              headers: { 'foo': 'bar' } });
+  const fetch_response = await fetch(original_request);
+  assert_equals(fetch_response.type, 'opaque');
+
+  await cache.put(original_request, fetch_response);
+
+  const match_response_1 = await cache.match(original_request);
+  assert_not_equals(match_response_1, undefined);
+
+  // Verify that cache.match() finds the entry even if queried with a varied
+  // header that does not match the cache key.  Vary headers should be ignored
+  // for opaque responses.
+  const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } });
+  const match_response_2 = await cache.match(different_request);
+  assert_not_equals(match_response_2, undefined);
+}, 'Cache.match ignores vary headers on opaque response.');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js
new file mode 100644
index 0000000..93c5517
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js
@@ -0,0 +1,244 @@
+// META: title=Cache.matchAll
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll('not-present-in-the-cache')
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [],
+            'Cache.matchAll should resolve with an empty array on failure.');
+        });
+  }, 'Cache.matchAll with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a.request.url)
+      .then(function(result) {
+          assert_response_array_equals(result, [entries.a.response],
+                                       'Cache.matchAll should match by URL.');
+        });
+  }, 'Cache.matchAll with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a.request)
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [entries.a.response],
+            'Cache.matchAll should match by Request.');
+        });
+  }, 'Cache.matchAll with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(new Request(entries.a.request.url))
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [entries.a.response],
+            'Cache.matchAll should match by Request.');
+        });
+  }, 'Cache.matchAll with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}),
+                          {ignoreSearch: true})
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [],
+            'Cache.matchAll should not match HEAD Request.');
+        });
+  }, 'Cache.matchAll with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a.request,
+                          {ignoreSearch: true})
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ],
+            'Cache.matchAll with ignoreSearch should ignore the ' +
+            'search parameters of cached request.');
+        });
+  },
+  'Cache.matchAll with ignoreSearch option (request with no search ' +
+  'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.a_with_query.request,
+                          {ignoreSearch: true})
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.a.response,
+              entries.a_with_query.response
+            ],
+            'Cache.matchAll with ignoreSearch should ignore the ' +
+            'search parameters of request.');
+        });
+  },
+  'Cache.matchAll with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com/');
+    var head_request = new Request('http://example.com/', {method: 'HEAD'});
+    var response = new Response('foo');
+    return cache.put(request.clone(), response.clone())
+      .then(function() {
+          return cache.matchAll(head_request.clone());
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [],
+            'Cache.matchAll should resolve with empty array for a ' +
+            'mismatched method.');
+          return cache.matchAll(head_request.clone(),
+                                {ignoreMethod: true});
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [response],
+            'Cache.matchAll with ignoreMethod should ignore the ' +
+            'method of request.');
+        });
+  }, 'Cache.matchAll supports ignoreMethod');
+
+cache_test(function(cache) {
+    var vary_request = new Request('http://example.com/c',
+                                   {headers: {'Cookies': 'is-for-cookie'}});
+    var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+    var mismatched_vary_request = new Request('http://example.com/c');
+
+    return cache.put(vary_request.clone(), vary_response.clone())
+      .then(function() {
+          return cache.matchAll(mismatched_vary_request.clone());
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [],
+            'Cache.matchAll should resolve as undefined with a ' +
+            'mismatched vary.');
+          return cache.matchAll(mismatched_vary_request.clone(),
+                              {ignoreVary: true});
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [vary_response],
+            'Cache.matchAll with ignoreVary should ignore the ' +
+            'vary of request.');
+        });
+  }, 'Cache.matchAll supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(entries.cat.request.url + '#mouse')
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.cat.response,
+            ],
+            'Cache.matchAll should ignore URL fragment.');
+        });
+  }, 'Cache.matchAll with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll('http')
+      .then(function(result) {
+          assert_response_array_equals(
+            result, [],
+            'Cache.matchAll should treat query as a URL and not ' +
+            'just a string fragment.');
+        });
+  }, 'Cache.matchAll with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll()
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            simple_entries.map(entry => entry.response),
+            'Cache.matchAll without parameters should match all entries.');
+        });
+  }, 'Cache.matchAll without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+    return cache.matchAll(undefined)
+      .then(result => {
+          assert_response_array_equals(
+            result,
+            simple_entries.map(entry => entry.response),
+            'Cache.matchAll with undefined request should match all entries.');
+        });
+  }, 'Cache.matchAll with explicitly undefined request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+  return cache.matchAll(undefined, {})
+      .then(result => {
+          assert_response_array_equals(
+            result,
+            simple_entries.map(entry => entry.response),
+            'Cache.matchAll with undefined request should match all entries.');
+        });
+  }, 'Cache.matchAll with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+    return cache.matchAll('http://example.com/c')
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.vary_cookie_absent.response
+            ],
+            'Cache.matchAll should exclude matches if a vary header is ' +
+            'missing in the query request, but is present in the cached ' +
+            'request.');
+        })
+
+      .then(function() {
+          return cache.matchAll(
+            new Request('http://example.com/c',
+                        {headers: {'Cookies': 'none-of-the-above'}}));
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+            ],
+            'Cache.matchAll should exclude matches if a vary header is ' +
+            'missing in the cached request, but is present in the query ' +
+            'request.');
+        })
+
+      .then(function() {
+          return cache.matchAll(
+            new Request('http://example.com/c',
+                        {headers: {'Cookies': 'is-for-cookie'}}));
+        })
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [entries.vary_cookie_is_cookie.response],
+            'Cache.matchAll should match the entire header if a vary header ' +
+            'is present in both the query and cached requests.');
+        });
+  }, 'Cache.matchAll with responses containing "Vary" header');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+    return cache.matchAll('http://example.com/c',
+                          {ignoreVary: true})
+      .then(function(result) {
+          assert_response_array_equals(
+            result,
+            [
+              entries.vary_cookie_is_cookie.response,
+              entries.vary_cookie_is_good.response,
+              entries.vary_cookie_absent.response
+            ],
+            'Cache.matchAll should support multiple vary request/response ' +
+            'pairs.');
+        });
+  }, 'Cache.matchAll with multiple vary pairs');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js
new file mode 100644
index 0000000..dbf2650
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js
@@ -0,0 +1,411 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new Response(test_body);
+    return cache.put(request, response)
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Cache.put should resolve with undefined on success.');
+        });
+  }, 'Cache.put called with simple Request and Response');
+
+cache_test(function(cache) {
+    var test_url = new URL('./resources/simple.txt', location.href).href;
+    var request = new Request(test_url);
+    var response;
+    return fetch(test_url)
+      .then(function(fetch_result) {
+          response = fetch_result.clone();
+          return cache.put(request, fetch_result);
+        })
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, response,
+                                 'Cache.put should update the cache with ' +
+                                 'new request and response.');
+          return result.text();
+        })
+      .then(function(body) {
+          assert_equals(body, 'a simple text file\n',
+                        'Cache.put should store response body.');
+        });
+  }, 'Cache.put called with Request and Response from fetch()');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new Response(test_body);
+    assert_false(request.bodyUsed,
+                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+                 'Request.bodyUsed should be initially false.');
+    return cache.put(request, response)
+      .then(function() {
+        assert_false(request.bodyUsed,
+                     'Cache.put should not mark empty request\'s body used');
+      });
+  }, 'Cache.put with Request without a body');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new Response();
+    assert_false(response.bodyUsed,
+                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+                 'Response.bodyUsed should be initially false.');
+    return cache.put(request, response)
+      .then(function() {
+        assert_false(response.bodyUsed,
+                     'Cache.put should not mark empty response\'s body used');
+      });
+  }, 'Cache.put with Response without a body');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new Response(test_body);
+    return cache.put(request, response.clone())
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, response,
+                                 'Cache.put should update the cache with ' +
+                                 'new Request and Response.');
+        });
+  }, 'Cache.put with a Response containing an empty URL');
+
+cache_test(function(cache) {
+    var request = new Request(test_url);
+    var response = new Response('', {
+        status: 200,
+        headers: [['Content-Type', 'text/plain']]
+      });
+    return cache.put(request, response)
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_equals(result.status, 200, 'Cache.put should store status.');
+          assert_equals(result.headers.get('Content-Type'), 'text/plain',
+                        'Cache.put should store headers.');
+          return result.text();
+        })
+      .then(function(body) {
+          assert_equals(body, '',
+                        'Cache.put should store response body.');
+        });
+  }, 'Cache.put with an empty response body');
+
+cache_test(function(cache, test) {
+    var request = new Request(test_url);
+    var response = new Response('', {
+        status: 206,
+        headers: [['Content-Type', 'text/plain']]
+      });
+
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(request, response),
+      'Cache.put should reject 206 Responses with a TypeError.');
+  }, 'Cache.put with synthetic 206 response');
+
+cache_test(function(cache, test) {
+    var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href;
+    var request = new Request(test_url);
+    var response;
+    return fetch(test_url)
+      .then(function(fetch_result) {
+          assert_equals(fetch_result.status, 206,
+                        'Test framework error: The status code should be 206.');
+          response = fetch_result.clone();
+          return promise_rejects_js(test, TypeError, cache.put(request, fetch_result));
+        });
+  }, 'Cache.put with HTTP 206 response');
+
+cache_test(function(cache, test) {
+    // We need to jump through some hoops to allow the test to perform opaque
+    // response filtering, but bypass the ORB safelist check. This is
+    // done, by forcing the MIME type retrieval to fail and the
+    // validation of partial first response to succeed.
+    var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)";
+    var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href);
+    test_url.hostname = REMOTE_HOST;
+    var request = new Request(test_url.href, { mode: 'no-cors' });
+    var response;
+    return fetch(request)
+      .then(function(fetch_result) {
+          assert_equals(fetch_result.type, 'opaque',
+              'Test framework error: The response type should be opaque.');
+          assert_equals(fetch_result.status, 0,
+              'Test framework error: The status code should be 0 for an ' +
+              ' opaque-filtered response. This is actually HTTP 206.');
+          response = fetch_result.clone();
+          return cache.put(request, fetch_result);
+        })
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_not_equals(result, undefined,
+              'Cache.put should store an entry for the opaque response');
+        });
+  }, 'Cache.put with opaque-filtered HTTP 206 response');
+
+cache_test(function(cache) {
+    var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href;
+    var request = new Request(test_url);
+    var response;
+    return fetch(test_url)
+      .then(function(fetch_result) {
+          assert_equals(fetch_result.status, 500,
+                        'Test framework error: The status code should be 500.');
+          response = fetch_result.clone();
+          return cache.put(request, fetch_result);
+        })
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, response,
+                                 'Cache.put should update the cache with ' +
+                                 'new request and response.');
+          return result.text();
+        })
+      .then(function(body) {
+          assert_equals(body, '',
+                        'Cache.put should store response body.');
+        });
+  }, 'Cache.put with HTTP 500 response');
+
+cache_test(function(cache) {
+    var alternate_response_body = 'New body';
+    var alternate_response = new Response(alternate_response_body,
+                                          { statusText: 'New status' });
+    return cache.put(new Request(test_url),
+                     new Response('Old body', { statusText: 'Old status' }))
+      .then(function() {
+          return cache.put(new Request(test_url), alternate_response.clone());
+        })
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, alternate_response,
+                                 'Cache.put should replace existing ' +
+                                 'response with new response.');
+          return result.text();
+        })
+      .then(function(body) {
+          assert_equals(body, alternate_response_body,
+                        'Cache put should store new response body.');
+        });
+  }, 'Cache.put called twice with matching Requests and different Responses');
+
+cache_test(function(cache) {
+    var first_url = test_url;
+    var second_url = first_url + '#(O_o)';
+    var third_url = first_url + '#fragment';
+    var alternate_response_body = 'New body';
+    var alternate_response = new Response(alternate_response_body,
+                                          { statusText: 'New status' });
+    return cache.put(new Request(first_url),
+                     new Response('Old body', { statusText: 'Old status' }))
+      .then(function() {
+          return cache.put(new Request(second_url), alternate_response.clone());
+        })
+      .then(function() {
+          return cache.match(test_url);
+        })
+      .then(function(result) {
+          assert_response_equals(result, alternate_response,
+                                 'Cache.put should replace existing ' +
+                                 'response with new response.');
+          return result.text();
+        })
+      .then(function(body) {
+          assert_equals(body, alternate_response_body,
+                        'Cache put should store new response body.');
+        })
+      .then(function() {
+          return cache.put(new Request(third_url), alternate_response.clone());
+        })
+      .then(function() {
+          return cache.keys();
+        })
+      .then(function(results) {
+          // Should match urls (without fragments or with different ones) to the
+          // same cache key. However, result.url should be the latest url used.
+          assert_equals(results[0].url, third_url);
+          return;
+        });
+}, 'Cache.put called multiple times with request URLs that differ only by a fragment');
+
+cache_test(function(cache) {
+    var url = 'http://example.com/foo';
+    return cache.put(url, new Response('some body'))
+      .then(function() { return cache.match(url); })
+      .then(function(response) { return response.text(); })
+      .then(function(body) {
+          assert_equals(body, 'some body',
+                        'Cache.put should accept a string as request.');
+        });
+  }, 'Cache.put with a string request');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(new Request(test_url), 'Hello world!'),
+      'Cache.put should only accept a Response object as the response.');
+  }, 'Cache.put with an invalid response');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(new Request('file:///etc/passwd'),
+                new Response(test_body)),
+      'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
+  }, 'Cache.put with a non-HTTP/HTTPS request');
+
+cache_test(function(cache) {
+    var response = new Response(test_body);
+    return cache.put(new Request('relative-url'), response.clone())
+      .then(function() {
+          return cache.match(new URL('relative-url', location.href).href);
+        })
+      .then(function(result) {
+          assert_response_equals(result, response,
+                                 'Cache.put should accept a relative URL ' +
+                                 'as the request.');
+        });
+  }, 'Cache.put with a relative URL');
+
+cache_test(function(cache, test) {
+    var request = new Request('http://example.com/foo', { method: 'HEAD' });
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(request, new Response(test_body)),
+      'Cache.put should throw a TypeError for non-GET requests.');
+  }, 'Cache.put with a non-GET request');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(new Request(test_url), null),
+      'Cache.put should throw a TypeError for a null response.');
+  }, 'Cache.put with a null response');
+
+cache_test(function(cache, test) {
+    var request = new Request(test_url, {method: 'POST', body: test_body});
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(request, new Response(test_body)),
+      'Cache.put should throw a TypeError for a POST request.');
+  }, 'Cache.put with a POST request');
+
+cache_test(function(cache) {
+    var response = new Response(test_body);
+    assert_false(response.bodyUsed,
+                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+                 'Response.bodyUsed should be initially false.');
+    return response.text().then(function() {
+      assert_true(
+        response.bodyUsed,
+        '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
+          'The text() method should make the body disturbed.');
+      var request = new Request(test_url);
+      return cache.put(request, response).then(() => {
+          assert_unreached('cache.put should be rejected');
+        }, () => {});
+    });
+  }, 'Cache.put with a used response body');
+
+cache_test(function(cache) {
+    var response = new Response(test_body);
+    return cache.put(new Request(test_url), response)
+      .then(function() {
+          assert_throws_js(TypeError, () => response.body.getReader());
+      });
+  }, 'getReader() after Cache.put');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(new Request(test_url),
+                new Response(test_body, { headers: { VARY: '*' }})),
+      'Cache.put should reject VARY:* Responses with a TypeError.');
+  }, 'Cache.put with a VARY:* Response');
+
+cache_test(function(cache, test) {
+    return promise_rejects_js(
+      test,
+      TypeError,
+      cache.put(new Request(test_url),
+                new Response(test_body,
+                             { headers: { VARY: 'Accept-Language,*' }})),
+      'Cache.put should reject Responses with an embedded VARY:* with a ' +
+      'TypeError.');
+  }, 'Cache.put with an embedded VARY:* Response');
+
+cache_test(async function(cache, test) {
+    const url = new URL('./resources/vary.py?vary=*',
+        get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+    const request = new Request(url, { mode: 'no-cors' });
+    const response = await fetch(request);
+    assert_equals(response.type, 'opaque');
+    await cache.put(request, response);
+  }, 'Cache.put with a VARY:* opaque response should not reject');
+
+cache_test(function(cache) {
+    var url = 'foo.html';
+    var redirectURL = 'http://example.com/foo-bar.html';
+    var redirectResponse = Response.redirect(redirectURL);
+    assert_equals(redirectResponse.headers.get('Location'), redirectURL,
+                  'Response.redirect() should set Location header.');
+    return cache.put(url, redirectResponse.clone())
+      .then(function() {
+          return cache.match(url);
+        })
+      .then(function(response) {
+          assert_response_equals(response, redirectResponse,
+                                 'Redirect response is reproduced by the Cache API');
+          assert_equals(response.headers.get('Location'), redirectURL,
+                        'Location header is preserved by Cache API.');
+        });
+  }, 'Cache.put should store Response.redirect() correctly');
+
+cache_test(async (cache) => {
+    var request = new Request(test_url);
+    var response = new Response(new Blob([test_body]));
+    await cache.put(request, response);
+    var cachedResponse = await cache.match(request);
+    assert_equals(await cachedResponse.text(), test_body);
+  }, 'Cache.put called with simple Request and blob Response');
+
+cache_test(async (cache) => {
+  var formData = new FormData();
+  formData.append("name", "value");
+
+  var request = new Request(test_url);
+  var response = new Response(formData);
+  await cache.put(request, response);
+  var cachedResponse = await cache.match(request);
+  var cachedResponseText = await cachedResponse.text();
+  assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1);
+}, 'Cache.put called with simple Request and form data Response');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
new file mode 100644
index 0000000..0b5ef7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
@@ -0,0 +1,71 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+promise_test(async function(test) {
+  var inboxBucket = await navigator.storageBuckets.open('inbox');
+  var draftsBucket = await navigator.storageBuckets.open('drafts');
+
+  test.add_cleanup(async function() {
+    await navigator.storageBuckets.delete('inbox');
+    await navigator.storageBuckets.delete('drafts');
+  });
+
+  const cacheName = 'attachments';
+  const cacheKey = 'receipt1.txt';
+
+  var inboxCache = await inboxBucket.caches.open(cacheName);
+  var draftsCache = await draftsBucket.caches.open(cacheName);
+
+  await inboxCache.put(cacheKey, new Response('bread x 2'))
+  await draftsCache.put(cacheKey, new Response('eggs x 1'));
+
+  return inboxCache.match(cacheKey)
+      .then(function(result) {
+        return result.text();
+      })
+      .then(function(body) {
+        assert_equals(body, 'bread x 2', 'Wrong cache contents');
+        return draftsCache.match(cacheKey);
+      })
+      .then(function(result) {
+        return result.text();
+      })
+      .then(function(body) {
+        assert_equals(body, 'eggs x 1', 'Wrong cache contents');
+      });
+}, 'caches from different buckets have different contents');
+
+promise_test(async function(test) {
+  var inboxBucket = await navigator.storageBuckets.open('inbox');
+  var draftBucket = await navigator.storageBuckets.open('drafts');
+
+  test.add_cleanup(async function() {
+    await navigator.storageBuckets.delete('inbox');
+    await navigator.storageBuckets.delete('drafts');
+  });
+
+  var caches = inboxBucket.caches;
+  var attachments = await caches.open('attachments');
+  await attachments.put('receipt1.txt', new Response('bread x 2'));
+  var result = await attachments.match('receipt1.txt');
+  assert_equals(await result.text(), 'bread x 2');
+
+  await navigator.storageBuckets.delete('inbox');
+
+  await promise_rejects_dom(
+      test, 'UnknownError', caches.open('attachments'));
+
+  // Also test when `caches` is first accessed after the deletion.
+  await navigator.storageBuckets.delete('drafts');
+  return promise_rejects_dom(
+      test, 'UnknownError', draftBucket.caches.open('attachments'));
+}, 'cache.open promise is rejected when bucket is gone');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js
new file mode 100644
index 0000000..f19522b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js
@@ -0,0 +1,35 @@
+// META: title=CacheStorage.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_cache_list =
+  ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
+
+promise_test(function(test) {
+    return self.caches.keys()
+      .then(function(keys) {
+          assert_true(Array.isArray(keys),
+                      'CacheStorage.keys should return an Array.');
+          return Promise.all(keys.map(function(key) {
+              return self.caches.delete(key);
+            }));
+        })
+      .then(function() {
+          return Promise.all(test_cache_list.map(function(key) {
+              return self.caches.open(key);
+            }));
+        })
+
+      .then(function() { return self.caches.keys(); })
+      .then(function(keys) {
+          assert_true(Array.isArray(keys),
+                      'CacheStorage.keys should return an Array.');
+          assert_array_equals(keys,
+                              test_cache_list,
+                              'CacheStorage.keys should only return ' +
+                              'existing caches.');
+        });
+  }, 'CacheStorage keys');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js
new file mode 100644
index 0000000..0c31b72
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js
@@ -0,0 +1,245 @@
+// META: title=CacheStorage.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+(function() {
+  var next_index = 1;
+
+  // Returns a transaction (request, response, and url) for a unique URL.
+  function create_unique_transaction(test) {
+    var uniquifier = String(next_index++);
+    var url = 'http://example.com/' + uniquifier;
+
+    return {
+      request: new Request(url),
+      response: new Response('hello'),
+      url: url
+    };
+  }
+
+  self.create_unique_transaction = create_unique_transaction;
+})();
+
+cache_test(function(cache) {
+    var transaction = create_unique_transaction();
+
+    return cache.put(transaction.request.clone(), transaction.response.clone())
+      .then(function() {
+          return self.caches.match(transaction.request);
+        })
+      .then(function(response) {
+          assert_response_equals(response, transaction.response,
+                                 'The response should not have changed.');
+        });
+}, 'CacheStorageMatch with no cache name provided');
+
+cache_test(function(cache) {
+    var transaction = create_unique_transaction();
+
+    var test_cache_list = ['a', 'b', 'c'];
+    return cache.put(transaction.request.clone(), transaction.response.clone())
+      .then(function() {
+          return Promise.all(test_cache_list.map(function(key) {
+              return self.caches.open(key);
+            }));
+        })
+      .then(function() {
+          return self.caches.match(transaction.request);
+        })
+      .then(function(response) {
+          assert_response_equals(response, transaction.response,
+                                 'The response should not have changed.');
+        });
+}, 'CacheStorageMatch from one of many caches');
+
+promise_test(function(test) {
+    var transaction = create_unique_transaction();
+
+    var test_cache_list = ['x', 'y', 'z'];
+    return Promise.all(test_cache_list.map(function(key) {
+        return self.caches.open(key);
+      }))
+      .then(function() { return self.caches.open('x'); })
+      .then(function(cache) {
+          return cache.put(transaction.request.clone(),
+                           transaction.response.clone());
+        })
+      .then(function() {
+          return self.caches.match(transaction.request, {cacheName: 'x'});
+        })
+      .then(function(response) {
+          assert_response_equals(response, transaction.response,
+                                 'The response should not have changed.');
+        })
+      .then(function() {
+          return self.caches.match(transaction.request, {cacheName: 'y'});
+        })
+      .then(function(response) {
+          assert_equals(response, undefined,
+                        'Cache y should not have a response for the request.');
+        });
+}, 'CacheStorageMatch from one of many caches by name');
+
+cache_test(function(cache) {
+    var transaction = create_unique_transaction();
+    return cache.put(transaction.url, transaction.response.clone())
+      .then(function() {
+          return self.caches.match(transaction.request);
+        })
+      .then(function(response) {
+          assert_response_equals(response, transaction.response,
+                                 'The response should not have changed.');
+        });
+}, 'CacheStorageMatch a string request');
+
+cache_test(function(cache) {
+    var transaction = create_unique_transaction();
+    return cache.put(transaction.request.clone(), transaction.response.clone())
+      .then(function() {
+          return self.caches.match(new Request(transaction.request.url,
+                                              {method: 'HEAD'}));
+        })
+      .then(function(response) {
+          assert_equals(response, undefined,
+                        'A HEAD request should not be matched');
+        });
+}, 'CacheStorageMatch a HEAD request');
+
+promise_test(function(test) {
+    var transaction = create_unique_transaction();
+    return self.caches.match(transaction.request)
+      .then(function(response) {
+          assert_equals(response, undefined,
+                        'The response should not be found.');
+        });
+}, 'CacheStorageMatch with no cached entry');
+
+promise_test(function(test) {
+    var transaction = create_unique_transaction();
+    return self.caches.delete('foo')
+      .then(function() {
+          return self.caches.has('foo');
+        })
+      .then(function(has_foo) {
+          assert_false(has_foo, "The cache should not exist.");
+          return self.caches.match(transaction.request, {cacheName: 'foo'});
+        })
+      .then(function(response) {
+          assert_equals(response, undefined,
+                        'The match with bad cache name should resolve to ' +
+                        'undefined.');
+          return self.caches.has('foo');
+        })
+      .then(function(has_foo) {
+          assert_false(has_foo, "The cache should still not exist.");
+        });
+}, 'CacheStorageMatch with no caches available but name provided');
+
+cache_test(function(cache) {
+    var transaction = create_unique_transaction();
+
+    return self.caches.delete('')
+      .then(function() {
+          return self.caches.has('');
+        })
+      .then(function(has_cache) {
+          assert_false(has_cache, "The cache should not exist.");
+          return cache.put(transaction.request, transaction.response.clone());
+        })
+      .then(function() {
+          return self.caches.match(transaction.request, {cacheName: ''});
+        })
+      .then(function(response) {
+          assert_equals(response, undefined,
+                        'The response should not be found.');
+          return self.caches.open('');
+        })
+      .then(function(cache) {
+          return cache.put(transaction.request, transaction.response);
+        })
+      .then(function() {
+          return self.caches.match(transaction.request, {cacheName: ''});
+        })
+      .then(function(response) {
+          assert_response_equals(response, transaction.response,
+                                 'The response should be matched.');
+          return self.caches.delete('');
+        });
+}, 'CacheStorageMatch with empty cache name provided');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com/?foo');
+    var no_query_request = new Request('http://example.com/');
+    var response = new Response('foo');
+    return cache.put(request.clone(), response.clone())
+      .then(function() {
+          return self.caches.match(no_query_request.clone());
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'CacheStorageMatch should resolve as undefined with a ' +
+            'mismatched query.');
+          return self.caches.match(no_query_request.clone(),
+                                   {ignoreSearch: true});
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, response,
+            'CacheStorageMatch with ignoreSearch should ignore the ' +
+            'query of the request.');
+        });
+  }, 'CacheStorageMatch supports ignoreSearch');
+
+cache_test(function(cache) {
+    var request = new Request('http://example.com/');
+    var head_request = new Request('http://example.com/', {method: 'HEAD'});
+    var response = new Response('foo');
+    return cache.put(request.clone(), response.clone())
+      .then(function() {
+          return self.caches.match(head_request.clone());
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'CacheStorageMatch should resolve as undefined with a ' +
+            'mismatched method.');
+          return self.caches.match(head_request.clone(),
+                                   {ignoreMethod: true});
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, response,
+            'CacheStorageMatch with ignoreMethod should ignore the ' +
+            'method of request.');
+        });
+  }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+    var vary_request = new Request('http://example.com/c',
+                                   {headers: {'Cookies': 'is-for-cookie'}});
+    var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+    var mismatched_vary_request = new Request('http://example.com/c');
+
+    return cache.put(vary_request.clone(), vary_response.clone())
+      .then(function() {
+          return self.caches.match(mismatched_vary_request.clone());
+        })
+      .then(function(result) {
+          assert_equals(
+            result, undefined,
+            'CacheStorageMatch should resolve as undefined with a ' +
+            ' mismatched vary.');
+          return self.caches.match(mismatched_vary_request.clone(),
+                                   {ignoreVary: true});
+        })
+      .then(function(result) {
+          assert_response_equals(
+            result, vary_response,
+            'CacheStorageMatch with ignoreVary should ignore the ' +
+            'vary of request.');
+        });
+  }, 'CacheStorageMatch supports ignoreVary');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js
new file mode 100644
index 0000000..b7d5af7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js
@@ -0,0 +1,239 @@
+// META: title=CacheStorage
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+promise_test(function(t) {
+    var cache_name = 'cache-storage/foo';
+    return self.caches.delete(cache_name)
+      .then(function() {
+          return self.caches.open(cache_name);
+        })
+      .then(function(cache) {
+          assert_true(cache instanceof Cache,
+                      'CacheStorage.open should return a Cache.');
+        });
+  }, 'CacheStorage.open');
+
+promise_test(function(t) {
+    var cache_name = 'cache-storage/bar';
+    var first_cache = null;
+    var second_cache = null;
+    return self.caches.open(cache_name)
+      .then(function(cache) {
+          first_cache = cache;
+          return self.caches.delete(cache_name);
+        })
+      .then(function() {
+          return first_cache.add('./resources/simple.txt');
+        })
+      .then(function() {
+          return self.caches.keys();
+        })
+      .then(function(cache_names) {
+          assert_equals(cache_names.indexOf(cache_name), -1);
+          return self.caches.open(cache_name);
+        })
+      .then(function(cache) {
+          second_cache = cache;
+          return second_cache.keys();
+        })
+      .then(function(keys) {
+          assert_equals(keys.length, 0);
+          return first_cache.keys();
+        })
+      .then(function(keys) {
+          assert_equals(keys.length, 1);
+          // Clean up
+          return self.caches.delete(cache_name);
+        });
+  }, 'CacheStorage.delete dooms, but does not delete immediately');
+
+promise_test(function(t) {
+    // Note that this test may collide with other tests running in the same
+    // origin that also uses an empty cache name.
+    var cache_name = '';
+    return self.caches.delete(cache_name)
+      .then(function() {
+          return self.caches.open(cache_name);
+        })
+      .then(function(cache) {
+          assert_true(cache instanceof Cache,
+                      'CacheStorage.open should accept an empty name.');
+        });
+  }, 'CacheStorage.open with an empty name');
+
+promise_test(function(t) {
+    return promise_rejects_js(
+      t,
+      TypeError,
+      self.caches.open(),
+      'CacheStorage.open should throw TypeError if called with no arguments.');
+  }, 'CacheStorage.open with no arguments');
+
+promise_test(function(t) {
+    var test_cases = [
+      {
+        name: 'cache-storage/lowercase',
+        should_not_match:
+          [
+            'cache-storage/Lowercase',
+            ' cache-storage/lowercase',
+            'cache-storage/lowercase '
+          ]
+      },
+      {
+        name: 'cache-storage/has a space',
+        should_not_match:
+          [
+            'cache-storage/has'
+          ]
+      },
+      {
+        name: 'cache-storage/has\000_in_the_name',
+        should_not_match:
+          [
+            'cache-storage/has',
+            'cache-storage/has_in_the_name'
+          ]
+      }
+    ];
+    return Promise.all(test_cases.map(function(testcase) {
+        var cache_name = testcase.name;
+        return self.caches.delete(cache_name)
+          .then(function() {
+              return self.caches.open(cache_name);
+            })
+          .then(function() {
+              return self.caches.has(cache_name);
+            })
+          .then(function(result) {
+              assert_true(result,
+                          'CacheStorage.has should return true for existing ' +
+                          'cache.');
+            })
+          .then(function() {
+              return Promise.all(
+                testcase.should_not_match.map(function(cache_name) {
+                    return self.caches.has(cache_name)
+                      .then(function(result) {
+                          assert_false(result,
+                                       'CacheStorage.has should only perform ' +
+                                       'exact matches on cache names.');
+                        });
+                  }));
+            })
+          .then(function() {
+              return self.caches.delete(cache_name);
+            });
+      }));
+  }, 'CacheStorage.has with existing cache');
+
+promise_test(function(t) {
+    return self.caches.has('cheezburger')
+      .then(function(result) {
+          assert_false(result,
+                       'CacheStorage.has should return false for ' +
+                       'nonexistent cache.');
+        });
+  }, 'CacheStorage.has with nonexistent cache');
+
+promise_test(function(t) {
+    var cache_name = 'cache-storage/open';
+    var cache;
+    return self.caches.delete(cache_name)
+      .then(function() {
+          return self.caches.open(cache_name);
+        })
+      .then(function(result) {
+          cache = result;
+        })
+      .then(function() {
+          return cache.add('./resources/simple.txt');
+        })
+      .then(function() {
+          return self.caches.open(cache_name);
+        })
+      .then(function(result) {
+          assert_true(result instanceof Cache,
+                      'CacheStorage.open should return a Cache object');
+          assert_not_equals(result, cache,
+                            'CacheStorage.open should return a new Cache ' +
+                            'object each time its called.');
+          return Promise.all([cache.keys(), result.keys()]);
+        })
+      .then(function(results) {
+          var expected_urls = results[0].map(function(r) { return r.url });
+          var actual_urls = results[1].map(function(r) { return r.url });
+          assert_array_equals(actual_urls, expected_urls,
+                              'CacheStorage.open should return a new Cache ' +
+                              'object for the same backing store.');
+        });
+  }, 'CacheStorage.open with existing cache');
+
+promise_test(function(t) {
+    var cache_name = 'cache-storage/delete';
+
+    return self.caches.delete(cache_name)
+      .then(function() {
+          return self.caches.open(cache_name);
+        })
+      .then(function() { return self.caches.delete(cache_name); })
+      .then(function(result) {
+          assert_true(result,
+                      'CacheStorage.delete should return true after ' +
+                      'deleting an existing cache.');
+        })
+
+      .then(function() { return self.caches.has(cache_name); })
+      .then(function(cache_exists) {
+          assert_false(cache_exists,
+                       'CacheStorage.has should return false after ' +
+                       'fulfillment of CacheStorage.delete promise.');
+        });
+  }, 'CacheStorage.delete with existing cache');
+
+promise_test(function(t) {
+    return self.caches.delete('cheezburger')
+      .then(function(result) {
+          assert_false(result,
+                       'CacheStorage.delete should return false for a ' +
+                       'nonexistent cache.');
+        });
+  }, 'CacheStorage.delete with nonexistent cache');
+
+promise_test(function(t) {
+    var unpaired_name = 'unpaired\uD800';
+    var converted_name = 'unpaired\uFFFD';
+
+    // The test assumes that a cache with converted_name does not
+    // exist, but if the implementation fails the test then such
+    // a cache will be created. Start off in a fresh state by
+    // deleting all caches.
+    return delete_all_caches()
+      .then(function() {
+          return self.caches.has(converted_name);
+      })
+      .then(function(cache_exists) {
+          assert_false(cache_exists,
+                       'Test setup failure: cache should not exist');
+      })
+      .then(function() { return self.caches.open(unpaired_name); })
+      .then(function() { return self.caches.keys(); })
+      .then(function(keys) {
+          assert_true(keys.indexOf(unpaired_name) !== -1,
+                      'keys should include cache with bad name');
+      })
+      .then(function() { return self.caches.has(unpaired_name); })
+      .then(function(cache_exists) {
+          assert_true(cache_exists,
+                      'CacheStorage names should be not be converted.');
+        })
+      .then(function() { return self.caches.has(converted_name); })
+      .then(function(cache_exists) {
+          assert_false(cache_exists,
+                       'CacheStorage names should be not be converted.');
+        });
+  }, 'CacheStorage names are DOMStrings not USVStrings');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/common.https.html b/third_party/web_platform_tests/service-workers/cache-storage/common.https.html
deleted file mode 100644
index d5f7d24..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/common.https.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Verify that Window and Workers see same storage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script>
-
-function wait_for_message(worker) {
-    return new Promise(function(resolve) {
-        worker.addEventListener('message', function listener(e) {
-            resolve(e.data);
-            worker.removeEventListener('message', listener);
-        });
-    });
-}
-
-promise_test(function(t) {
-    var cache_name = 'common-test';
-    return self.caches.delete(cache_name)
-        .then(function() {
-            var worker = new Worker('resources/common-worker.js');
-            worker.postMessage({name: cache_name});
-            return wait_for_message(worker);
-        })
-        .then(function(message) {
-            return self.caches.open(cache_name);
-        })
-        .then(function(cache) {
-            return Promise.all([
-                cache.match('https://example.com/a'),
-                cache.match('https://example.com/b'),
-                cache.match('https://example.com/c')
-            ]);
-        })
-        .then(function(responses) {
-            return Promise.all(responses.map(
-                function(response) { return response.text(); }
-            ));
-        })
-        .then(function(bodies) {
-            assert_equals(bodies[0], 'a',
-                          'Body should match response put by worker');
-            assert_equals(bodies[1], 'b',
-                          'Body should match response put by worker');
-            assert_equals(bodies[2], 'c',
-                          'Body should match response put by worker');
-        });
-}, 'Window sees cache puts by Worker');
-
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js b/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js
new file mode 100644
index 0000000..eba312c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js
@@ -0,0 +1,44 @@
+// META: title=Cache Storage: Verify that Window and Workers see same storage
+// META: timeout=long
+
+function wait_for_message(worker) {
+    return new Promise(function(resolve) {
+        worker.addEventListener('message', function listener(e) {
+            resolve(e.data);
+            worker.removeEventListener('message', listener);
+        });
+    });
+}
+
+promise_test(function(t) {
+    var cache_name = 'common-test';
+    return self.caches.delete(cache_name)
+        .then(function() {
+            var worker = new Worker('resources/common-worker.js');
+            worker.postMessage({name: cache_name});
+            return wait_for_message(worker);
+        })
+        .then(function(message) {
+            return self.caches.open(cache_name);
+        })
+        .then(function(cache) {
+            return Promise.all([
+                cache.match('https://example.com/a'),
+                cache.match('https://example.com/b'),
+                cache.match('https://example.com/c')
+            ]);
+        })
+        .then(function(responses) {
+            return Promise.all(responses.map(
+                function(response) { return response.text(); }
+            ));
+        })
+        .then(function(bodies) {
+            assert_equals(bodies[0], 'a',
+                          'Body should match response put by worker');
+            assert_equals(bodies[1], 'b',
+                          'Body should match response put by worker');
+            assert_equals(bodies[2], 'c',
+                          'Body should match response put by worker');
+        });
+}, 'Window sees cache puts by Worker');
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
new file mode 100644
index 0000000..ec930a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script type="module">
+  const cache = await window.caches.open('cache_name_0')
+  await cache.add("")
+  const resp1 = await cache.match("")
+  const readStream = resp1.body
+  // Cloning will open the stream via NS_AsyncCopy in Gecko
+  resp1.clone()
+  // Give a little bit of time
+  await new Promise(setTimeout)
+  // At this point the previous open operation is about to finish but not yet.
+  // It will finish after the second open operation is made, potentially causing incorrect state.
+  await readStream.getReader().read();
+  document.documentElement.classList.remove('test-wait')
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html b/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html
new file mode 100644
index 0000000..0fe4a0a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cache Storage: Verify credentials are respected by Cache operations</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<style>iframe { display: none; }</style>
+<script>
+
+var worker = "./resources/credentials-worker.js";
+var scope = "./resources/credentials-iframe.html";
+promise_test(function(t) {
+  return self.caches.delete('credentials')
+    .then(function() {
+      return service_worker_unregister_and_register(t, worker, scope)
+    })
+    .then(function(reg) {
+      return wait_for_state(t, reg.installing, 'activated');
+    })
+    .then(function() {
+      return with_iframe(scope);
+    })
+    .then(function(frame) {
+      frame.contentWindow.postMessage([
+        {name: 'file.txt', username: 'aa', password: 'bb'},
+        {name: 'file.txt', username: 'cc', password: 'dd'},
+        {name: 'file.txt'}
+      ], '*');
+      return new Promise(function(resolve, reject) {
+        window.onmessage = t.step_func(function(e) {
+          resolve(e.data);
+        });
+      });
+    })
+    .then(function(data) {
+      assert_equals(data.length, 3, 'three entries should be present');
+      assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2,
+        'two entries should contain credentials');
+      assert_true(data.some(function(url) { return /aa:bb@/.test(url); }),
+        'entry with credentials aa:bb should be present');
+      assert_true(data.some(function(url) { return /cc:dd@/.test(url); }),
+        'entry with credentials cc:dd should be present');
+    });
+}, "Cache API matching includes credentials");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html b/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html
new file mode 100644
index 0000000..1cfc256
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const cache_exists_js = (cache_name, response_queue_name) => `
+  try {
+    const exists = await self.caches.has("${cache_name}");
+    if (exists) {
+      await send("${response_queue_name}", "true");
+    } else {
+      await send("${response_queue_name}", "false");
+    }
+  } catch {
+    await send("${response_queue_name}", "exception");
+  }
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+  const importScript = ${importScript};
+  await importScript("/html/cross-origin-embedder-policy/credentialless" +
+                   "/resources/common.js");
+  await importScript("/html/anonymous-iframe/resources/common.js");
+  await importScript("/common/utils.js");
+  await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+  // Create a same-origin iframe in a cross-site popup.
+  const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+  await send(not_same_site_popup_uuid,
+       add_iframe_js(same_site_origin, response_queue_uuid));
+  const iframe_1_uuid = await receive(response_queue_uuid);
+
+  // Create a same-origin iframe in a same-site popup.
+  const same_origin_popup_uuid = newPopup(t, same_site_origin);
+  await send(same_origin_popup_uuid,
+       add_iframe_js(same_site_origin, response_queue_uuid));
+  const iframe_2_uuid = await receive(response_queue_uuid);
+
+  return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      const response_queue_uuid = token();
+
+      const [iframe_1_uuid, iframe_2_uuid] =
+        await create_test_iframes(t, response_queue_uuid);
+
+      const cache_name = token();
+      await self.caches.open(cache_name);
+      t.add_cleanup(() => self.caches.delete(cache_name));
+
+      await send(iframe_2_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "true") {
+        reject("Cache not visible in same-top-level-site iframe");
+      }
+
+      await send(iframe_1_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "false") {
+        reject("Cache visible in not-same-top-level-site iframe");
+      }
+
+      resolve();
+    } catch (e) {
+      reject(e);
+    }
+  });
+}, "CacheStorage caches shouldn't be shared with a cross-partition iframe");
+
+const newWorker = (origin) => {
+  const worker_token = token();
+  const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+  const worker = new Worker(worker_url);
+  return worker_token;
+}
+
+promise_test(t => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      const response_queue_uuid = token();
+
+      const create_worker_js = (origin) => `
+        const importScript = ${importScript};
+        await importScript("/html/cross-origin-embedder-policy/credentialless" +
+                         "/resources/common.js");
+        await importScript("/html/anonymous-iframe/resources/common.js");
+        await importScript("/common/utils.js");
+        const newWorker = ${newWorker};
+        await send("${response_queue_uuid}", newWorker("${origin}"));
+      `;
+
+      const [iframe_1_uuid, iframe_2_uuid] =
+        await create_test_iframes(t, response_queue_uuid);
+
+      // Create a dedicated worker in the cross-top-level-site iframe.
+      await send(iframe_1_uuid, create_worker_js(same_site_origin));
+      const worker_1_uuid = await receive(response_queue_uuid);
+
+      // Create a dedicated worker in the same-top-level-site iframe.
+      await send(iframe_2_uuid, create_worker_js(same_site_origin));
+      const worker_2_uuid = await receive(response_queue_uuid);
+
+      const cache_name = token();
+      await self.caches.open(cache_name);
+      t.add_cleanup(() => self.caches.delete(cache_name));
+
+      await send(worker_2_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "true") {
+        reject("Cache not visible in same-top-level-site worker");
+      }
+
+      await send(worker_1_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "false") {
+        reject("Cache visible in not-same-top-level-site worker");
+      }
+      resolve();
+    } catch (e) {
+      reject(e);
+    }
+  });
+}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+  const worker_token = token();
+  const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+  const worker = new SharedWorker(worker_url, worker_token);
+  return worker_token;
+}
+
+promise_test(t => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      const response_queue_uuid = token();
+
+      const create_worker_js = (origin) => `
+        const importScript = ${importScript};
+        await importScript("/html/cross-origin-embedder-policy/credentialless" +
+                         "/resources/common.js");
+        await importScript("/html/anonymous-iframe/resources/common.js");
+        await importScript("/common/utils.js");
+        const newSharedWorker = ${newSharedWorker};
+        await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+      `;
+
+      const [iframe_1_uuid, iframe_2_uuid] =
+        await create_test_iframes(t, response_queue_uuid);
+
+      // Create a shared worker in the cross-top-level-site iframe.
+      await send(iframe_1_uuid, create_worker_js(same_site_origin));
+      const worker_1_uuid = await receive(response_queue_uuid);
+
+      // Create a shared worker in the same-top-level-site iframe.
+      await send(iframe_2_uuid, create_worker_js(same_site_origin));
+      const worker_2_uuid = await receive(response_queue_uuid);
+
+      const cache_name = token();
+      await self.caches.open(cache_name);
+      t.add_cleanup(() => self.caches.delete(cache_name));
+
+      await send(worker_2_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "true") {
+        reject("Cache not visible in same-top-level-site worker");
+      }
+
+      await send(worker_1_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "false") {
+        reject("Cache visible in not-same-top-level-site worker");
+      }
+      resolve();
+    } catch (e) {
+      reject(e);
+    }
+  });
+}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+  const worker_token = token();
+  const worker_url = origin + executor_service_worker_path +
+                     `&uuid=${worker_token}`;
+  const worker_url_path = executor_service_worker_path.substring(0,
+                              executor_service_worker_path.lastIndexOf('/'));
+  const scope = worker_url_path + "/not-used/";
+  const reg = await navigator.serviceWorker.register(worker_url,
+                                                     {'scope': scope});
+  return worker_token;
+}
+
+promise_test(t => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      const response_queue_uuid = token();
+
+      const create_worker_js = (origin) => `
+        const importScript = ${importScript};
+        await importScript("/html/cross-origin-embedder-policy/credentialless" +
+                         "/resources/common.js");
+        await importScript("/html/anonymous-iframe/resources/common.js");
+        await importScript("/common/utils.js");
+        const newServiceWorker = ${newServiceWorker};
+        await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+      `;
+
+      const [iframe_1_uuid, iframe_2_uuid] =
+        await create_test_iframes(t, response_queue_uuid);
+
+      // Create a service worker in the same-top-level-site iframe.
+      await send(iframe_2_uuid, create_worker_js(same_site_origin));
+      const worker_2_uuid = await receive(response_queue_uuid);
+
+      t.add_cleanup(() =>
+        send(worker_2_uuid, "self.registration.unregister();"));
+
+      const cache_name = token();
+      await self.caches.open(cache_name);
+      t.add_cleanup(() => self.caches.delete(cache_name));
+
+      await send(worker_2_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "true") {
+        reject("Cache not visible in same-top-level-site worker");
+      }
+
+      // Create a service worker in the cross-top-level-site iframe. Note that
+      // if service workers are unpartitioned then this new service worker would
+      // replace the one created above. This is why we wait to create the second
+      // service worker until after we are done with the first one.
+      await send(iframe_1_uuid, create_worker_js(same_site_origin));
+      const worker_1_uuid = await receive(response_queue_uuid);
+
+      t.add_cleanup(() =>
+        send(worker_1_uuid, "self.registration.unregister();"));
+
+      await send(worker_1_uuid,
+           cache_exists_js(cache_name, response_queue_uuid));
+      if (await receive(response_queue_uuid) !== "false") {
+        reject("Cache visible in not-same-top-level-site worker");
+      }
+
+      resolve();
+    } catch (e) {
+      reject(e);
+    }
+  });
+}, "CacheStorage caches shouldn't be shared with a cross-partition service worker");
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000..ee574d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+    const params = new URL(event.request.url).searchParams;
+    if (params.has('ignore')) {
+      return;
+    }
+    if (!params.has('name')) {
+      event.respondWith(Promise.reject(TypeError('No name is provided.')));
+      return;
+    }
+
+    event.respondWith(Promise.resolve().then(async () => {
+        const name = params.get('name');
+        await caches.delete('foo');
+        const cache = await caches.open('foo');
+        await cache.put(event.request, new Response('hello'));
+        const keys = await cache.keys();
+
+        const original = event.request[name];
+        const stored = keys[0][name];
+        return new Response(`original: ${original}, stored: ${stored}`);
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html
new file mode 100644
index 0000000..00702df
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Controlled frame for Cache API test with credentials</title>
+<script>
+
+function xhr(url, username, password) {
+  return new Promise(function(resolve, reject) {
+    var xhr = new XMLHttpRequest(), async = true;
+    xhr.open('GET', url, async, username, password);
+    xhr.send();
+    xhr.onreadystatechange = function() {
+      if (xhr.readyState !== XMLHttpRequest.DONE)
+        return;
+      if (xhr.status === 200) {
+        resolve(xhr.responseText);
+      } else {
+        reject(new Error(xhr.statusText));
+      }
+    };
+  });
+}
+
+window.onmessage = function(e) {
+  Promise.all(e.data.map(function(item) {
+    return xhr(item.name, item.username, item.password);
+  }))
+    .then(function() {
+      navigator.serviceWorker.controller.postMessage('keys');
+      navigator.serviceWorker.onmessage = function(e) {
+        window.parent.postMessage(e.data, '*');
+      };
+    });
+};
+
+</script>
+<body>
+Hello? Yes, this is iframe.
+</body>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js
new file mode 100644
index 0000000..43965b5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js
@@ -0,0 +1,59 @@
+var cache_name = 'credentials';
+
+function assert_equals(actual, expected, message) {
+  if (!Object.is(actual, expected))
+    throw Error(message + ': expected: ' + expected + ', actual: ' + actual);
+}
+
+self.onfetch = function(e) {
+  if (!/\.txt$/.test(e.request.url)) return;
+  var content = e.request.url;
+  var cache;
+  e.respondWith(
+    self.caches.open(cache_name)
+      .then(function(result) {
+        cache = result;
+        return cache.put(e.request, new Response(content));
+      })
+
+      .then(function() { return cache.match(e.request); })
+      .then(function(result) { return result.text(); })
+      .then(function(text) {
+        assert_equals(text, content, 'Cache.match() body should match');
+      })
+
+      .then(function() { return cache.matchAll(e.request); })
+      .then(function(results) {
+        assert_equals(results.length, 1, 'Should have one response');
+        return results[0].text();
+      })
+      .then(function(text) {
+        assert_equals(text, content, 'Cache.matchAll() body should match');
+      })
+
+      .then(function() { return self.caches.match(e.request); })
+      .then(function(result) { return result.text(); })
+      .then(function(text) {
+        assert_equals(text, content, 'CacheStorage.match() body should match');
+      })
+
+     .then(function() {
+        return new Response('dummy');
+      })
+  );
+};
+
+self.onmessage = function(e) {
+  if (e.data === 'keys') {
+    self.caches.open(cache_name)
+      .then(function(cache) { return cache.keys(); })
+      .then(function(requests) {
+        var urls = requests.map(function(request) { return request.url; });
+        self.clients.matchAll().then(function(clients) {
+          clients.forEach(function(client) {
+            client.postMessage(urls);
+          });
+        });
+      });
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py b/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
index 71f13eb..b7109f4 100644
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
@@ -1,2 +1,2 @@
 def main(request, response):
-    return int(request.GET["status"]), [], ""
+    return int(request.GET[b"status"]), [], b""
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
index 9111095..050ac0b 100644
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
@@ -32,6 +32,241 @@
 function cache_test(test_function, description) {
   promise_test(function(test) {
       return create_temporary_cache(test)
-        .then(test_function);
+        .then(function(cache) { return test_function(cache, test); });
     }, description);
 }
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+var simple_entries = [
+  {
+    name: 'a',
+    request: new Request('http://example.com/a'),
+    response: new Response('')
+  },
+
+  {
+    name: 'b',
+    request: new Request('http://example.com/b'),
+    response: new Response('')
+  },
+
+  {
+    name: 'a_with_query',
+    request: new Request('http://example.com/a?q=r'),
+    response: new Response('')
+  },
+
+  {
+    name: 'A',
+    request: new Request('http://example.com/A'),
+    response: new Response('')
+  },
+
+  {
+    name: 'a_https',
+    request: new Request('https://example.com/a'),
+    response: new Response('')
+  },
+
+  {
+    name: 'a_org',
+    request: new Request('http://example.org/a'),
+    response: new Response('')
+  },
+
+  {
+    name: 'cat',
+    request: new Request('http://example.com/cat'),
+    response: new Response('')
+  },
+
+  {
+    name: 'catmandu',
+    request: new Request('http://example.com/catmandu'),
+    response: new Response('')
+  },
+
+  {
+    name: 'cat_num_lives',
+    request: new Request('http://example.com/cat?lives=9'),
+    response: new Response('')
+  },
+
+  {
+    name: 'cat_in_the_hat',
+    request: new Request('http://example.com/cat/in/the/hat'),
+    response: new Response('')
+  },
+
+  {
+    name: 'non_2xx_response',
+    request: new Request('http://example.com/non2xx'),
+    response: new Response('', {status: 404, statusText: 'nope'})
+  },
+
+  {
+    name: 'error_response',
+    request: new Request('http://example.com/error'),
+    response: Response.error()
+  },
+];
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+// These contain a mix of test cases that use Vary headers.
+var vary_entries = [
+  {
+    name: 'vary_cookie_is_cookie',
+    request: new Request('http://example.com/c',
+                         {headers: {'Cookies': 'is-for-cookie'}}),
+    response: new Response('',
+                           {headers: {'Vary': 'Cookies'}})
+  },
+
+  {
+    name: 'vary_cookie_is_good',
+    request: new Request('http://example.com/c',
+                         {headers: {'Cookies': 'is-good-enough-for-me'}}),
+    response: new Response('',
+                           {headers: {'Vary': 'Cookies'}})
+  },
+
+  {
+    name: 'vary_cookie_absent',
+    request: new Request('http://example.com/c'),
+    response: new Response('',
+                           {headers: {'Vary': 'Cookies'}})
+  }
+];
+
+// Run |test_function| with a Cache object and a map of entries. Prior to the
+// call, the Cache is populated by cache entries from |entries|. The latter is
+// expected to be an Object mapping arbitrary keys to objects of the form
+// {request: <Request object>, response: <Response object>}. Entries are
+// serially added to the cache in the order specified.
+//
+// |test_function| should return a Promise that can be used with promise_test.
+function prepopulated_cache_test(entries, test_function, description) {
+  cache_test(function(cache) {
+      var p = Promise.resolve();
+      var hash = {};
+      entries.forEach(function(entry) {
+          hash[entry.name] = entry;
+          p = p.then(function() {
+              return cache.put(entry.request.clone(), entry.response.clone())
+                  .catch(function(e) {
+                      assert_unreached(
+                          'Test setup failed for entry ' + entry.name + ': ' + e
+                      );
+                  });
+          });
+      });
+      return p
+        .then(function() {
+            assert_equals(Object.keys(hash).length, entries.length);
+        })
+        .then(function() {
+            return test_function(cache, hash);
+        });
+    }, description);
+}
+
+// Helper for testing with Headers objects. Compares Headers instances
+// by serializing |expected| and |actual| to arrays and comparing.
+function assert_header_equals(actual, expected, description) {
+    assert_class_string(actual, "Headers", description);
+    var header;
+    var actual_headers = [];
+    var expected_headers = [];
+    for (header of actual)
+        actual_headers.push(header[0] + ": " + header[1]);
+    for (header of expected)
+        expected_headers.push(header[0] + ": " + header[1]);
+    assert_array_equals(actual_headers, expected_headers,
+                        description + " Headers differ.");
+}
+
+// Helper for testing with Response objects. Compares simple
+// attributes defined on the interfaces, as well as the headers. It
+// does not compare the response bodies.
+function assert_response_equals(actual, expected, description) {
+    assert_class_string(actual, "Response", description);
+    ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
+        assert_equals(actual[attribute], expected[attribute],
+                      description + " Attributes differ: " + attribute + ".");
+    });
+    assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals. The order
+// is not significant.
+//
+// |expected| is assumed to not contain any duplicates.
+function assert_response_array_equivalent(actual, expected, description) {
+    assert_true(Array.isArray(actual), description);
+    assert_equals(actual.length, expected.length, description);
+    expected.forEach(function(expected_element) {
+        // assert_response_in_array treats the first argument as being
+        // 'actual', and the second as being 'expected array'. We are
+        // switching them around because we want to be resilient
+        // against the |actual| array containing duplicates.
+        assert_response_in_array(expected_element, actual, description);
+    });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_response_array_equals(actual, expected, description) {
+    assert_true(Array.isArray(actual), description);
+    assert_equals(actual.length, expected.length, description);
+    actual.forEach(function(value, index) {
+        assert_response_equals(value, expected[index],
+                               description + " : object[" + index + "]");
+    });
+}
+
+// Equivalent to assert_in_array, but uses assert_response_equals.
+function assert_response_in_array(actual, expected_array, description) {
+    assert_true(expected_array.some(function(element) {
+        try {
+            assert_response_equals(actual, element);
+            return true;
+        } catch (e) {
+            return false;
+        }
+    }), description);
+}
+
+// Helper for testing with Request objects. Compares simple
+// attributes defined on the interfaces, as well as the headers.
+function assert_request_equals(actual, expected, description) {
+    assert_class_string(actual, "Request", description);
+    ["url"].forEach(function(attribute) {
+        assert_equals(actual[attribute], expected[attribute],
+                      description + " Attributes differ: " + attribute + ".");
+    });
+    assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Requests as determined by assert_request_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_request_array_equals(actual, expected, description) {
+    assert_true(Array.isArray(actual), description);
+    assert_equals(actual.length, expected.length, description);
+    actual.forEach(function(value, index) {
+        assert_request_equals(value, expected[index],
+                              description + " : object[" + index + "]");
+    });
+}
+
+// Deletes all caches, returning a promise indicating success.
+function delete_all_caches() {
+  return self.caches.keys()
+    .then(function(keys) {
+      return Promise.all(keys.map(self.caches.delete.bind(self.caches)));
+    });
+}
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js
deleted file mode 100644
index b4a0c27..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * testharness-helpers contains various useful extensions to testharness.js to
- * allow them to be used across multiple tests before they have been
- * upstreamed. This file is intended to be usable from both document and worker
- * environments, so code should for example not rely on the DOM.
- */
-
-// Returns a promise that fulfills after the provided |promise| is fulfilled.
-// The |test| succeeds only if |promise| rejects with an exception matching
-// |code|. Accepted values for |code| follow those accepted for assert_throws().
-// The optional |description| describes the test being performed.
-//
-// E.g.:
-//   assert_promise_rejects(
-//       new Promise(...), // something that should throw an exception.
-//       'NotFoundError',
-//       'Should throw NotFoundError.');
-//
-//   assert_promise_rejects(
-//       new Promise(...),
-//       new TypeError(),
-//       'Should throw TypeError');
-function assert_promise_rejects(promise, code, description) {
-  return promise.then(
-    function() {
-      throw 'assert_promise_rejects: ' + description + ' Promise did not reject.';
-    },
-    function(e) {
-      if (code !== undefined) {
-        assert_throws(code, function() { throw e; }, description);
-      }
-    });
-}
-
-// Helper for testing with Headers objects. Compares Headers instances
-// by serializing |expected| and |actual| to arrays and comparing.
-function assert_header_equals(actual, expected, description) {
-    assert_class_string(actual, "Headers", description);
-    var header, actual_headers = [], expected_headers = [];
-    for (header of actual)
-        actual_headers.push(header[0] + ": " + header[1]);
-    for (header of expected)
-        expected_headers.push(header[0] + ": " + header[1]);
-    assert_array_equals(actual_headers, expected_headers,
-                        description + " Headers differ.");
-}
-
-// Helper for testing with Response objects. Compares simple
-// attributes defined on the interfaces, as well as the headers. It
-// does not compare the response bodies.
-function assert_response_equals(actual, expected, description) {
-    return Promise.all([actual.clone().text(), expected.clone().text()]).then(bodies => {
-        assert_equals(bodies[0], bodies[1]);
-    });
-    // assert_class_string(actual, "Response", description);
-    // ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
-    //     assert_equals(actual[attribute], expected[attribute],
-    //                   description + " Attributes differ: " + attribute + ".");
-    // });
-    // assert_header_equals(actual.headers, expected.headers, description);
-}
-
-// Assert that the two arrays |actual| and |expected| contain the same
-// set of Responses as determined by assert_response_equals. The order
-// is not significant.
-//
-// |expected| is assumed to not contain any duplicates.
-function assert_response_array_equivalent(actual, expected, description) {
-    assert_true(Array.isArray(actual), description);
-    assert_equals(actual.length, expected.length, description);
-    expected.forEach(function(expected_element) {
-        // assert_response_in_array treats the first argument as being
-        // 'actual', and the second as being 'expected array'. We are
-        // switching them around because we want to be resilient
-        // against the |actual| array containing duplicates.
-        assert_response_in_array(expected_element, actual, description);
-    });
-}
-
-// Asserts that two arrays |actual| and |expected| contain the same
-// set of Responses as determined by assert_response_equals(). The
-// corresponding elements must occupy corresponding indices in their
-// respective arrays.
-function assert_response_array_equals(actual, expected, description) {
-    assert_true(Array.isArray(actual), description);
-    assert_equals(actual.length, expected.length, description);
-    actual.forEach(function(value, index) {
-        assert_response_equals(value, expected[index],
-                               description + " : object[" + index + "]");
-    });
-}
-
-// Equivalent to assert_in_array, but uses assert_response_equals.
-function assert_response_in_array(actual, expected_array, description) {
-    assert_true(expected_array.some(function(element) {
-        try {
-            assert_response_equals(actual, element);
-            return true;
-        } catch (e) {
-            return false;
-        }
-    }), description);
-}
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py b/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py
new file mode 100644
index 0000000..7fde1b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py
@@ -0,0 +1,25 @@
+def main(request, response):
+  if b"clear-vary-value-override-cookie" in request.GET:
+    response.unset_cookie(b"vary-value-override")
+    return b"vary cookie cleared"
+
+  set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie",
+                                      default=b"")
+  if set_cookie_vary:
+    response.set_cookie(b"vary-value-override", set_cookie_vary)
+    return b"vary cookie set"
+
+  # If there is a vary-value-override cookie set, then use its value
+  # for the VARY header no matter what the query string is set to.  This
+  # override is necessary to test the case when two URLs are identical
+  # (including query), but differ by VARY header.
+  cookie_vary = request.cookies.get(b"vary-value-override")
+  if cookie_vary:
+    response.headers.set(b"vary", str(cookie_vary))
+  else:
+    # If there is no cookie, then use the query string value, if present.
+    query_vary = request.GET.first(b"vary", default=b"")
+    if query_vary:
+      response.headers.set(b"vary", query_vary)
+
+  return b"vary response"
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html b/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html
new file mode 100644
index 0000000..098fa89
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Cache Storage: Verify access in sandboxed iframes</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function load_iframe(src, sandbox) {
+    return new Promise(function(resolve, reject) {
+        var iframe = document.createElement('iframe');
+        iframe.onload = function() { resolve(iframe); };
+
+        iframe.sandbox = sandbox;
+        iframe.src = src;
+
+        document.documentElement.appendChild(iframe);
+    });
+}
+
+function wait_for_message(id) {
+    return new Promise(function(resolve) {
+        self.addEventListener('message', function listener(e) {
+            if (e.data.id === id) {
+                resolve(e.data);
+                self.removeEventListener('message', listener);
+            }
+        });
+    });
+}
+
+var counter = 0;
+
+promise_test(function(t) {
+    return load_iframe('./resources/iframe.html',
+                       'allow-scripts allow-same-origin')
+        .then(function(iframe) {
+            var id = ++counter;
+            iframe.contentWindow.postMessage({id: id}, '*');
+            return wait_for_message(id);
+        })
+        .then(function(message) {
+            assert_equals(
+                message.result, 'allowed',
+                'Access should be allowed if sandbox has allow-same-origin');
+        });
+}, 'Sandboxed iframe with allow-same-origin is allowed access');
+
+promise_test(function(t) {
+    return load_iframe('./resources/iframe.html',
+                       'allow-scripts')
+        .then(function(iframe) {
+            var id = ++counter;
+            iframe.contentWindow.postMessage({id: id}, '*');
+            return wait_for_message(id);
+        })
+        .then(function(message) {
+            assert_equals(
+                message.result, 'denied',
+                'Access should be denied if sandbox lacks allow-same-origin');
+            assert_equals(message.name, 'SecurityError',
+                          'Failure should be a SecurityError');
+        });
+}, 'Sandboxed iframe without allow-same-origin is denied access');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js
deleted file mode 100644
index 69ca447..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js
+++ /dev/null
@@ -1,138 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.add(),
-//       new TypeError(),
-//       'Cache.add should throw a TypeError when no arguments are given.');
-//   }, 'Cache.add called with no arguments');
-
-cache_test(function(cache) {
-    return cache.add('../resources/simple.txt')
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.add should resolve with undefined on success.');
-        });
-  }, 'Cache.add called with relative URL specified as a string');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.add('javascript://this-is-not-http-mmkay'),
-//       new TypeError(),
-//       'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
-//   }, 'Cache.add called with non-HTTP/HTTPS URL');
-
-cache_test(function(cache) {
-    var request = new Request('../resources/simple.txt');
-    return cache.add(request)
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.add should resolve with undefined on success.');
-        });
-  }, 'Cache.add called with Request object');
-
-cache_test(function(cache) {
-    var request = new Request('../resources/simple.txt');
-    return cache.add(request)
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.add should resolve with undefined on success.');
-        })
-      .then(function() {
-          return cache.add(request);
-        })
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.add should resolve with undefined on success.');
-        });
-  }, 'Cache.add called twice with the same Request object');
-
-cache_test(function(cache) {
-    return cache.add('this-does-not-exist-please-dont-create-it')
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.add should resolve with undefined on success.');
-        });
-  }, 'Cache.add with request that results in a status of 404');
-
-// cache_test(function(cache) {
-//     return cache.add('../resources/fetch-status.py?status=500')
-//       .then(function(result) {
-//           assert_equals(result, undefined,
-//                         'Cache.add should resolve with undefined on success.');
-//         });
-//   }, 'Cache.add with request that results in a status of 500');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.addAll(),
-//       new TypeError(),
-//       'Cache.addAll with no arguments should throw TypeError.');
-//   }, 'Cache.addAll with no arguments');
-
-// cache_test(function(cache) {
-//     // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-//     var urls = ['../resources/simple.txt', undefined, '../resources/blank.html'];
-//     return assert_promise_rejects(
-//       cache.addAll(),
-//       new TypeError(),
-//       'Cache.addAll should throw TypeError for an undefined argument.');
-//   }, 'Cache.addAll with a mix of valid and undefined arguments');
-
-// cache_test(function(cache) {
-//     // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-//     var urls = ['../resources/simple.txt', self.location.href, '../resources/blank.html'];
-//     return cache.addAll(urls)
-//       .then(function(result) {
-//           assert_equals(result, undefined,
-//                         'Cache.addAll should resolve with undefined on ' +
-//                         'success.');
-//         });
-//   }, 'Cache.addAll with string URL arguments');
-
-// cache_test(function(cache) {
-//     // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-//     var urls = ['../resources/simple.txt', self.location.href, '../resources/blank.html'];
-//     var requests = urls.map(function(url) {
-//         return new Request(url);
-//       });
-//     return cache.addAll(requests)
-//       .then(function(result) {
-//           assert_equals(result, undefined,
-//                         'Cache.addAll should resolve with undefined on ' +
-//                         'success.');
-//         });
-//   }, 'Cache.addAll with Request arguments');
-
-// cache_test(function(cache) {
-//     // Assumes that ../resources/simple.txt and ../resources/blank.html exist. The second
-//     // resource does not.
-//     var urls = ['../resources/simple.txt', 'this-resource-should-not-exist', '../resources/blank.html'];
-//     var requests = urls.map(function(url) {
-//         return new Request(url);
-//       });
-//     return cache.addAll(requests)
-//       .then(function(result) {
-//           assert_equals(result, undefined,
-//                         'Cache.addAll should resolve with undefined on ' +
-//                         'success.');
-//         });
-//   }, 'Cache.addAll with a mix of succeeding and failing requests');
-
-// cache_test(function(cache) {
-//     var request = new Request('../resources/simple.txt');
-//     return assert_promise_rejects(
-//       cache.addAll([request, request]),
-//       'InvalidStateError',
-//       'Cache.addAll should throw InvalidStateError if the same request is added ' +
-//       'twice.');
-//   }, 'Cache.addAll called with the same Request object specified twice');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js
deleted file mode 100644
index badce08..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js
+++ /dev/null
@@ -1,99 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-var test_url = 'https://example.com/foo';
-
-// Construct a generic Request object. The URL is |test_url|. All other fields
-// are defaults.
-function new_test_request() {
-  return new Request(test_url);
-}
-
-// Construct a generic Response object.
-function new_test_response() {
-  return new Response('Hello world!', { status: 200 });
-}
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.delete(),
-//       new TypeError(),
-//       'Cache.delete should reject with a TypeError when called with no ' +
-//       'arguments.');
-//   }, 'Cache.delete with no arguments');
-
-cache_test(function(cache) {
-    return cache.put(new_test_request(), new_test_response())
-      .then(function() {
-          return cache.delete(test_url);
-        })
-      .then(function(result) {
-          assert_true(result,
-                      'Cache.delete should resolve with "true" if an entry ' +
-                      'was successfully deleted.');
-          return cache.match(test_url);
-        })
-      .then(function(result) {
-          assert_equals(result, undefined,
-            'Cache.delete should remove matching entries from cache.');
-        });
-  }, 'Cache.delete called with a string URL');
-
-cache_test(function(cache) {
-    var request = new Request(test_url);
-    return cache.put(request, new_test_response())
-      .then(function() {
-          return cache.delete(request);
-        })
-      .then(function(result) {
-          assert_true(result,
-                      'Cache.delete should resolve with "true" if an entry ' +
-                      'was successfully deleted.');
-        });
-  }, 'Cache.delete called with a Request object');
-
-cache_test(function(cache) {
-    return cache.delete(test_url)
-      .then(function(result) {
-          assert_false(result,
-                       'Cache.delete should resolve with "false" if there ' +
-                       'are no matching entries.');
-        });
-  }, 'Cache.delete with a non-existent entry');
-
-var cache_entries = {
-  a: {
-    request: new Request('http://example.com/abc'),
-    response: new Response('')
-  },
-
-  b: {
-    request: new Request('http://example.com/b'),
-    response: new Response('')
-  },
-
-  a_with_query: {
-    request: new Request('http://example.com/abc?q=r'),
-    response: new Response('')
-  }
-};
-
-function prepopulated_cache_test(test_function, description) {
-  cache_test(function(cache) {
-      return Promise.all(Object.keys(cache_entries).map(function(k) {
-          return cache.put(cache_entries[k].request.clone(),
-                           cache_entries[k].response.clone());
-        }))
-        .then(function() {
-            return test_function(cache);
-          });
-    }, description);
-}
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js
deleted file mode 100644
index c0ccb16..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js
+++ /dev/null
@@ -1,446 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out. Consider caching a response with an
-// empty body.
-
-// A set of Request/Response pairs to be used with prepopulated_cache_test().
-var simple_entries = [
-  {
-    name: 'a',
-    request: new Request('http://example.com/a'),
-    response: new Response('a')
-  },
-
-  {
-    name: 'b',
-    request: new Request('http://example.com/b'),
-    response: new Response('b')
-  },
-
-  {
-    name: 'a_with_query',
-    request: new Request('http://example.com/a?q=r'),
-    response: new Response('a?q=r')
-  },
-
-  {
-    name: 'A',
-    request: new Request('http://example.com/A'),
-    response: new Response('A')
-  },
-
-  {
-    name: 'a_https',
-    request: new Request('https://example.com/a'),
-    response: new Response('a')
-  },
-
-  {
-    name: 'a_org',
-    request: new Request('http://example.org/a'),
-    response: new Response('a')
-  },
-
-  {
-    name: 'cat',
-    request: new Request('http://example.com/cat'),
-    response: new Response('cat')
-  },
-
-  {
-    name: 'catmandu',
-    request: new Request('http://example.com/catmandu'),
-    response: new Response('catmandu')
-  },
-
-  {
-    name: 'cat_num_lives',
-    request: new Request('http://example.com/cat?lives=9'),
-    response: new Response('cat?lives=9')
-  },
-
-  {
-    name: 'cat_in_the_hat',
-    request: new Request('http://example.com/cat/in/the/hat'),
-    response: new Response('cat/in/the/hat')
-  }
-];
-
-// A set of Request/Response pairs to be used with prepopulated_cache_test().
-// These contain a mix of test cases that use Vary headers.
-var vary_entries = [
-  {
-    name: 'vary_cookie_is_cookie',
-    request: new Request('http://example.com/c',
-                         {headers: {'Cookies': 'is-for-cookie'}}),
-    response: new Response('c',
-                           {headers: {'Vary': 'Cookies'}})
-  },
-
-  {
-    name: 'vary_cookie_is_good',
-    request: new Request('http://example.com/c',
-                         {headers: {'Cookies': 'is-good-enough-for-me'}}),
-    response: new Response('c',
-                           {headers: {'Vary': 'Cookies'}})
-  },
-
-  {
-    name: 'vary_cookie_absent',
-    request: new Request('http://example.com/c'),
-    response: new Response('c',
-                           {headers: {'Vary': 'Cookies'}})
-  }
-];
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll('not-present-in-the-cache')
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result, [],
-//             'Cache.matchAll should resolve with an empty array on failure.');
-//         });
-//   }, 'Cache.matchAll with no matching entries');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match('not-present-in-the-cache')
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.match failures should resolve with undefined.');
-        });
-  }, 'Cache.match with no matching entries');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(entries.a.request.url)
-//       .then(function(result) {
-//           assert_response_array_equals(result, [entries.a.response],
-//                                        'Cache.matchAll should match by URL.');
-//         });
-//   }, 'Cache.matchAll with URL');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(entries.a.request.url)
-      .then(function(result) {
-          assert_response_equals(result, entries.a.response,
-                                 'Cache.match should match by URL.');
-        });
-  }, 'Cache.match with URL');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(entries.a.request)
-//       .then(function(result) {
-//           assert_response_array_equals(
-//             result, [entries.a.response],
-//             'Cache.matchAll should match by Request.');
-//         });
-//   }, 'Cache.matchAll with Request');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(entries.a.request)
-      .then(function(result) {
-          assert_response_equals(result, entries.a.response,
-                                 'Cache.match should match by Request.');
-        });
-  }, 'Cache.match with Request');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(new Request(entries.a.request.url))
-//       .then(function(result) {
-//           assert_response_array_equals(
-//             result, [entries.a.response],
-//             'Cache.matchAll should match by Request.');
-//         });
-//   }, 'Cache.matchAll with new Request');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(new Request(entries.a.request.url))
-      .then(function(result) {
-          assert_response_equals(result, entries.a.response,
-                                 'Cache.match should match by Request.');
-        });
-  }, 'Cache.match with new Request');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(entries.a.request,
-//                           {ignoreSearch: true})
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//               entries.a.response,
-//               entries.a_with_query.response
-//             ],
-//             'Cache.matchAll with ignoreSearch should ignore the ' +
-//             'search parameters of cached request.');
-//         });
-//   },
-//   'Cache.matchAll with ignoreSearch option (request with no search ' +
-//   'parameters)');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(entries.a.request,
-                       {ignoreSearch: true})
-      .then(function(result) {
-          assert_response_in_array(
-            result,
-            [
-              entries.a.response,
-              entries.a_with_query.response
-            ],
-            'Cache.match with ignoreSearch should ignore the ' +
-            'search parameters of cached request.');
-        });
-  },
-  'Cache.match with ignoreSearch option (request with no search ' +
-  'parameters)');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(entries.a_with_query.request,
-//                           {ignoreSearch: true})
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//               entries.a.response,
-//               entries.a_with_query.response
-//             ],
-//             'Cache.matchAll with ignoreSearch should ignore the ' +
-//             'search parameters of request.');
-//         });
-//   },
-//   'Cache.matchAll with ignoreSearch option (request with search parameter)');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(entries.a_with_query.request,
-                       {ignoreSearch: true})
-      .then(function(result) {
-          assert_response_in_array(
-            result,
-            [
-              entries.a.response,
-              entries.a_with_query.response
-            ],
-            'Cache.match with ignoreSearch should ignore the ' +
-            'search parameters of request.');
-        });
-  },
-  'Cache.match with ignoreSearch option (request with search parameter)');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll(entries.cat.request.url + '#mouse')
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//               entries.cat.response,
-//             ],
-//             'Cache.matchAll should ignore URL fragment.');
-//         });
-//   }, 'Cache.matchAll with URL containing fragment');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match(entries.cat.request.url + '#mouse')
-      .then(function(result) {
-          assert_response_equals(result, entries.cat.response,
-                                 'Cache.match should ignore URL fragment.');
-        });
-  }, 'Cache.match with URL containing fragment');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     return cache.matchAll('http')
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result, [],
-//             'Cache.matchAll should treat query as a URL and not ' +
-//             'just a string fragment.');
-//         });
-//   }, 'Cache.matchAll with string fragment "http" as query');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
-    return cache.match('http')
-      .then(function(result) {
-          assert_equals(
-            result, undefined,
-            'Cache.match should treat query as a URL and not ' +
-            'just a string fragment.');
-        });
-  }, 'Cache.match with string fragment "http" as query');
-
-// prepopulated_cache_test(vary_entries, function(cache, entries) {
-//     return cache.matchAll('http://example.com/c')
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//               entries.vary_cookie_absent.response
-//             ],
-//             'Cache.matchAll should exclude matches if a vary header is ' +
-//             'missing in the query request, but is present in the cached ' +
-//             'request.');
-//         })
-
-//       .then(function() {
-//           return cache.matchAll(
-//             new Request('http://example.com/c',
-//                         {headers: {'Cookies': 'none-of-the-above'}}));
-//         })
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//             ],
-//             'Cache.matchAll should exclude matches if a vary header is ' +
-//             'missing in the cached request, but is present in the query ' +
-//             'request.');
-//         })
-
-//       .then(function() {
-//           return cache.matchAll(
-//             new Request('http://example.com/c',
-//                         {headers: {'Cookies': 'is-for-cookie'}}));
-//         })
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [entries.vary_cookie_is_cookie.response],
-//             'Cache.matchAll should match the entire header if a vary header ' +
-//             'is present in both the query and cached requests.');
-//         });
-//   }, 'Cache.matchAll with responses containing "Vary" header');
-
-prepopulated_cache_test(vary_entries, function(cache, entries) {
-    return cache.match('http://example.com/c')
-      .then(function(result) {
-          assert_response_in_array(
-            result,
-            [
-              entries.vary_cookie_absent.response
-            ],
-            'Cache.match should honor "Vary" header.');
-        });
-  }, 'Cache.match with responses containing "Vary" header');
-
-// prepopulated_cache_test(vary_entries, function(cache, entries) {
-//     return cache.matchAll('http://example.com/c',
-//                           {ignoreVary: true})
-//       .then(function(result) {
-//           assert_response_array_equivalent(
-//             result,
-//             [
-//               entries.vary_cookie_is_cookie.response,
-//               entries.vary_cookie_is_good.response,
-//               entries.vary_cookie_absent.response,
-//             ],
-//             'Cache.matchAll should honor "ignoreVary" parameter.');
-//         });
-//   }, 'Cache.matchAll with "ignoreVary" parameter');
-
-// cache_test(function(cache) {
-//     var request = new Request('http://example.com');
-//     var response;
-//     var request_url = new URL('../resources/simple.txt', location.href).href;
-//     return fetch(request_url)
-//       .then(function(fetch_result) {
-//           response = fetch_result;
-//           assert_equals(
-//             response.url, request_url,
-//             '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
-//             'Reponse.url should return the URL of the response.');
-//           return cache.put(request, response.clone());
-//         })
-//       .then(function() {
-//           return cache.match(request.url);
-//         })
-//       .then(function(result) {
-//           assert_response_equals(
-//             result, response,
-//             'Cache.match should return a Response object that has the same ' +
-//             'properties as the stored response.');
-//           return cache.match(response.url);
-//         })
-//       .then(function(result) {
-//           assert_equals(
-//             result, undefined,
-//             'Cache.match should not match cache entry based on response URL.');
-//         });
-//   }, 'Cache.match with Request and Response objects with different URLs');
-
-cache_test(function(cache) {
-    var request_url = new URL('../resources/simple.txt', location.href).href;
-    return fetch(request_url)
-      .then(function(fetch_result) {
-          return cache.put(new Request(request_url), fetch_result);
-        })
-      .then(function() {
-          return cache.match(request_url);
-        })
-      .then(function(result) {
-          return result.text();
-        })
-      .then(function(body_text) {
-          assert_equals(body_text, 'a simple text file\n',
-                        'Cache.match should return a Response object with a ' +
-                        'valid body.');
-        })
-      .then(function() {
-          return cache.match(request_url);
-        })
-      .then(function(result) {
-          return result.text();
-        })
-      .then(function(body_text) {
-          assert_equals(body_text, 'a simple text file\n',
-                        'Cache.match should return a Response object with a ' +
-                        'valid body each time it is called.');
-        });
-  }, 'Cache.match invoked multiple times for the same Request/Response');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-//     var request = new Request(entries.a.request, { method: 'POST' });
-//     return cache.match(request)
-//       .then(function(result) {
-//           assert_equals(result, undefined,
-//                         'Cache.match should not find a match');
-//         });
-//   }, 'Cache.match with POST Request');
-
-// Helpers ---
-
-// Run |test_function| with a Cache object as its only parameter. Prior to the
-// call, the Cache is populated by cache entries from |entries|. The latter is
-// expected to be an Object mapping arbitrary keys to objects of the form
-// {request: <Request object>, response: <Response object>}. There's no
-// guarantee on the order in which entries will be added to the cache.
-//
-// |test_function| should return a Promise that can be used with promise_test.
-function prepopulated_cache_test(entries, test_function, description) {
-  cache_test(function(cache) {
-      var p = Promise.resolve();
-      var hash = {};
-      entries.forEach(function(entry) {
-          p = p.then(function() {
-              return cache.put(entry.request.clone(),
-                               entry.response.clone())
-                .catch(function(e) {
-                    assert_unreached('Test setup failed for entry ' +
-                                     entry.name + ': ' + e);
-                  });
-            });
-          hash[entry.name] = entry;
-        });
-      p = p.then(function() {
-          assert_equals(Object.keys(hash).length, entries.length);
-        });
-
-      return p.then(function() {
-          return test_function(cache, hash);
-        });
-    }, description);
-}
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js
deleted file mode 100644
index 755b8c2..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js
+++ /dev/null
@@ -1,284 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-var test_url = 'https://example.com/foo';
-var test_body = 'Hello world!';
-
-cache_test(function(cache) {
-    var request = new Request(test_url);
-    var response = new Response(test_body);
-    return cache.put(request, response)
-      .then(function(result) {
-          assert_equals(result, undefined,
-                        'Cache.put should resolve with undefined on success.');
-        });
-  }, 'Cache.put called with simple Request and Response');
-
-// cache_test(function(cache) {
-//     var test_url = new URL('../resources/simple.txt', location.href).href;
-//     var request = new Request(test_url);
-//     var response;
-//     return fetch(test_url)
-//       .then(function(fetch_result) {
-//           response = fetch_result.clone();
-//           return cache.put(request, fetch_result);
-//         })
-//       .then(function() {
-//           return cache.match(test_url);
-//         })
-//       .then(function(result) {
-//           assert_response_equals(result, response,
-//                                  'Cache.put should update the cache with ' +
-//                                  'new request and response.');
-//           return result.text();
-//         })
-//       .then(function(body) {
-//           assert_equals(body, 'a simple text file\n',
-//                         'Cache.put should store response body.');
-//         });
-//   }, 'Cache.put called with Request and Response from fetch()');
-
-cache_test(function(cache) {
-    var request = new Request(test_url);
-    var response = new Response(test_body);
-    assert_false(request.bodyUsed,
-                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
-                 'Request.bodyUsed should be initially false.');
-    return cache.put(request, response)
-      .then(function() {
-        assert_false(request.bodyUsed,
-                     'Cache.put should not mark empty request\'s body used');
-      });
-  }, 'Cache.put with Request without a body');
-
-cache_test(function(cache) {
-    var request = new Request(test_url);
-    var response = new Response();
-    assert_false(response.bodyUsed,
-                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
-                 'Response.bodyUsed should be initially false.');
-    return cache.put(request, response)
-      .then(function() {
-        assert_false(response.bodyUsed,
-                     'Cache.put should not mark empty response\'s body used');
-      });
-  }, 'Cache.put with Response without a body');
-
-cache_test(function(cache) {
-    var request = new Request(test_url);
-    var response = new Response(test_body);
-    return cache.put(request, response.clone())
-      .then(function() {
-          return cache.match(test_url);
-        })
-      .then(function(result) {
-          assert_response_equals(result, response,
-                                 'Cache.put should update the cache with ' +
-                                 'new Request and Response.');
-        });
-  }, 'Cache.put with a Response containing an empty URL');
-
-// cache_test(function(cache) {
-//     var request = new Request(test_url);
-//     var response = new Response('', {
-//         status: 200,
-//         headers: [['Content-Type', 'text/plain']]
-//       });
-//     return cache.put(request, response)
-//       .then(function() {
-//           return cache.match(test_url);
-//         })
-//       .then(function(result) {
-//           assert_equals(result.status, 200, 'Cache.put should store status.');
-//           assert_equals(result.headers.get('Content-Type'), 'text/plain',
-//                         'Cache.put should store headers.');
-//           return result.text();
-//         })
-//       .then(function(body) {
-//           assert_equals(body, '',
-//                         'Cache.put should store response body.');
-//         });
-//   }, 'Cache.put with an empty response body');
-
-// cache_test(function(cache) {
-//     var test_url = new URL('../resources/fetch-status.py?status=500', location.href).href;
-//     var request = new Request(test_url);
-//     var response;
-//     return fetch(test_url)
-//       .then(function(fetch_result) {
-//           assert_equals(fetch_result.status, 500,
-//                         'Test framework error: The status code should be 500.');
-//           response = fetch_result.clone();
-//           return cache.put(request, fetch_result);
-//         })
-//       .then(function() {
-//           return cache.match(test_url);
-//         })
-//       .then(function(result) {
-//           assert_response_equals(result, response,
-//                                  'Cache.put should update the cache with ' +
-//                                  'new request and response.');
-//           return result.text();
-//         })
-//       .then(function(body) {
-//           assert_equals(body, '',
-//                         'Cache.put should store response body.');
-//         });
-//   }, 'Cache.put with HTTP 500 response');
-
-// cache_test(function(cache) {
-//     var alternate_response_body = 'New body';
-//     var alternate_response = new Response(alternate_response_body,
-//                                           { statusText: 'New status' });
-//     return cache.put(new Request(test_url),
-//                      new Response('Old body', { statusText: 'Old status' }))
-//       .then(function() {
-//           return cache.put(new Request(test_url), alternate_response.clone());
-//         })
-//       .then(function() {
-//           return cache.match(test_url);
-//         })
-//       .then(function(result) {
-//           assert_response_equals(result, alternate_response,
-//                                  'Cache.put should replace existing ' +
-//                                  'response with new response.');
-//           return result.text();
-//         })
-//       .then(function(body) {
-//           assert_equals(body, alternate_response_body,
-//                         'Cache put should store new response body.');
-//         });
-//   }, 'Cache.put called twice with matching Requests and different Responses');
-
-cache_test(function(cache) {
-    var first_url = test_url;
-    var second_url = first_url + '#(O_o)';
-    var alternate_response_body = 'New body';
-    var alternate_response = new Response(alternate_response_body,
-                                          { statusText: 'New status' });
-    return cache.put(new Request(first_url),
-                     new Response('Old body', { statusText: 'Old status' }))
-      .then(function() {
-          return cache.put(new Request(second_url), alternate_response.clone());
-        })
-      .then(function() {
-          return cache.match(test_url);
-        })
-      .then(function(result) {
-          assert_response_equals(result, alternate_response,
-                                 'Cache.put should replace existing ' +
-                                 'response with new response.');
-          return result.text();
-        })
-      .then(function(body) {
-          assert_equals(body, alternate_response_body,
-                        'Cache put should store new response body.');
-        });
-  }, 'Cache.put called twice with request URLs that differ only by a fragment');
-
-cache_test(function(cache) {
-    var url = 'http://example.com/foo';
-    return cache.put(url, new Response('some body'))
-      .then(function() { return cache.match(url); })
-      .then(function(response) { return response.text(); })
-      .then(function(body) {
-          assert_equals(body, 'some body',
-                        'Cache.put should accept a string as request.');
-        });
-  }, 'Cache.put with a string request');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.put(new Request(test_url), 'Hello world!'),
-//       new TypeError(),
-//       'Cache.put should only accept a Response object as the response.');
-//   }, 'Cache.put with an invalid response');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.put(new Request('file:///etc/passwd'),
-//                 new Response(test_body)),
-//       new TypeError(),
-//       'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
-//   }, 'Cache.put with a non-HTTP/HTTPS request');
-
-cache_test(function(cache) {
-    var response = new Response(test_body);
-    return cache.put(new Request('relative-url'), response.clone())
-      .then(function() {
-          return cache.match(new URL('relative-url', location.href).href);
-        })
-      .then(function(result) {
-          assert_response_equals(result, response,
-                                 'Cache.put should accept a relative URL ' +
-                                 'as the request.');
-        });
-  }, 'Cache.put with a relative URL');
-
-// cache_test(function(cache) {
-//     var request = new Request('http://example.com/foo', { method: 'HEAD' });
-//     return assert_promise_rejects(
-//       cache.put(request, new Response(test_body)),
-//       new TypeError(),
-//       'Cache.put should throw a TypeError for non-GET requests.');
-//   }, 'Cache.put with a non-GET request');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.put(new Request(test_url), null),
-//       new TypeError(),
-//       'Cache.put should throw a TypeError for a null response.');
-//   }, 'Cache.put with a null response');
-
-// cache_test(function(cache) {
-//     var request = new Request(test_url, {method: 'POST', body: test_body});
-//     return assert_promise_rejects(
-//       cache.put(request, new Response(test_body)),
-//       new TypeError(),
-//       'Cache.put should throw a TypeError for a POST request.');
-//   }, 'Cache.put with a POST request');
-
-cache_test(function(cache) {
-    var response = new Response(test_body);
-    assert_false(response.bodyUsed,
-                 '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
-                 'Response.bodyUsed should be initially false.');
-    return response.text().then(function() {
-      assert_true(
-        response.bodyUsed,
-        '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
-          'The text() method should set "body used" flag.');
-      return assert_promise_rejects(
-        cache.put(new Request(test_url), response),
-        new TypeError,
-        '[https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#cache-put] ' +
-        'Cache put should reject with TypeError when Response ' +
-        'body is already used.');
-      });
-  }, 'Cache.put with a used response body');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.put(new Request(test_url),
-//                 new Response(test_body, { headers: { VARY: '*' }})),
-//       new TypeError(),
-//       'Cache.put should reject VARY:* Responses with a TypeError.');
-//   }, 'Cache.put with a VARY:* Response');
-
-// cache_test(function(cache) {
-//     return assert_promise_rejects(
-//       cache.put(new Request(test_url),
-//                 new Response(test_body,
-//                              { headers: { VARY: 'Accept-Language,*' }})),
-//       new TypeError(),
-//       'Cache.put should reject Responses with an embedded VARY:* with a ' +
-//       'TypeError.');
-//   }, 'Cache.put with an embedded VARY:* Response');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js
deleted file mode 100644
index 4d7bc62..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js
+++ /dev/null
@@ -1,36 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-var test_cache_list =
-  ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
-
-promise_test(function(test) {
-    return self.caches.keys()
-      .then(function(keys) {
-          assert_true(Array.isArray(keys),
-                      'CacheStorage.keys should return an Array.');
-          return Promise.all(keys.map(function(key) {
-              return self.caches.delete(key);
-            }));
-        })
-      .then(function() {
-          return Promise.all(test_cache_list.map(function(key) {
-              return self.caches.open(key);
-            }));
-        })
-
-      .then(function() { return self.caches.keys(); })
-      .then(function(keys) {
-          assert_true(Array.isArray(keys),
-                      'CacheStorage.keys should return an Array.');
-          assert_array_equals(keys,
-                              test_cache_list,
-                              'CacheStorage.keys should only return ' +
-                              'existing caches.');
-        });
-  }, 'CacheStorage keys');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js
deleted file mode 100644
index b5122ac..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js
+++ /dev/null
@@ -1,129 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-(function() {
-  var next_index = 1;
-
-  // Returns a transaction (request, response, and url) for a unique URL.
-  function create_unique_transaction(test) {
-    var uniquifier = String(next_index++);
-    var url = 'http://example.com/' + uniquifier;
-
-    return {
-      request: new Request(url),
-      response: new Response('hello'),
-      url: url
-    };
-  }
-
-  self.create_unique_transaction = create_unique_transaction;
-})();
-
-cache_test(function(cache) {
-    var transaction = create_unique_transaction();
-
-    return cache.put(transaction.request.clone(), transaction.response.clone())
-      .then(function() {
-          return self.caches.match(transaction.request);
-        })
-      .then(function(response) {
-          assert_response_equals(response, transaction.response,
-                                 'The response should not have changed.');
-        });
-}, 'CacheStorageMatch with no cache name provided');
-
-// cache_test(function(cache) {
-//     var transaction = create_unique_transaction();
-
-//     var test_cache_list = ['a', 'b', 'c'];
-//     return cache.put(transaction.request.clone(), transaction.response.clone())
-//       .then(function() {
-//           return Promise.all(test_cache_list.map(function(key) {
-//               return self.caches.open(key);
-//             }));
-//         })
-//       .then(function() {
-//           return self.caches.match(transaction.request);
-//         })
-//       .then(function(response) {
-//           assert_response_equals(response, transaction.response,
-//                                  'The response should not have changed.');
-//         });
-// }, 'CacheStorageMatch from one of many caches');
-
-// promise_test(function(test) {
-//     var transaction = create_unique_transaction();
-
-//     var test_cache_list = ['x', 'y', 'z'];
-//     return Promise.all(test_cache_list.map(function(key) {
-//         return self.caches.open(key);
-//       }))
-//       .then(function() { return caches.open('x'); })
-//       .then(function(cache) {
-//           return cache.put(transaction.request.clone(),
-//                            transaction.response.clone());
-//         })
-//       .then(function() {
-//           return self.caches.match(transaction.request, {cacheName: 'x'});
-//         })
-//       .then(function(response) {
-//           assert_response_equals(response, transaction.response,
-//                                  'The response should not have changed.');
-//         })
-//       .then(function() {
-//           return self.caches.match(transaction.request, {cacheName: 'y'});
-//         })
-//       .then(function(response) {
-//           assert_equals(response, undefined,
-//                         'Cache y should not have a response for the request.');
-//         });
-// }, 'CacheStorageMatch from one of many caches by name');
-
-// cache_test(function(cache) {
-//     var transaction = create_unique_transaction();
-//     return cache.put(transaction.url, transaction.response.clone())
-//       .then(function() {
-//           return self.caches.match(transaction.request);
-//         })
-//       .then(function(response) {
-//           assert_response_equals(response, transaction.response,
-//                                  'The response should not have changed.');
-//         });
-// }, 'CacheStorageMatch a string request');
-
-// promise_test(function(test) {
-//     var transaction = create_unique_transaction();
-//     return self.caches.match(transaction.request)
-//       .then(function(response) {
-//           assert_equals(response, undefined,
-//                         'The response should not be found.');
-//         })
-// }, 'CacheStorageMatch with no cached entry');
-
-// promise_test(function(test) {
-//     var transaction = create_unique_transaction();
-//     return self.caches.has('foo')
-//       .then(function(has_foo) {
-//           assert_false(has_foo, "The cache should not exist.");
-//           return self.caches.match(transaction.request, {cacheName: 'foo'});
-//         })
-//       .then(function(response) {
-//           assert_unreached('The match with bad cache name should reject.');
-//         })
-//       .catch(function(err) {
-//           assert_equals(err.name, 'NotFoundError',
-//                         'The match should reject with NotFoundError.');
-//           return self.caches.has('foo');
-//         })
-//       .then(function(has_foo) {
-//           assert_false(has_foo, "The cache should still not exist.");
-//         })
-// }, 'CacheStorageMatch with no caches available but name provided');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js
deleted file mode 100644
index 905ab5d..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js
+++ /dev/null
@@ -1,201 +0,0 @@
-if (self.importScripts) {
-    importScripts('/resources/testharness.js');
-    importScripts('../resources/testharness-helpers.js');
-    importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-promise_test(function(t) {
-    var cache_name = 'cache-storage/foo';
-    return self.caches.delete(cache_name)
-      .then(function() {
-          return self.caches.open(cache_name);
-        })
-      .then(function(cache) {
-          assert_true(cache instanceof Cache,
-                      'CacheStorage.open should return a Cache.');
-        });
-  }, 'CacheStorage.open');
-
-promise_test(function(t) {
-    // Note that this test may collide with other tests running in the same
-    // origin that also uses an empty cache name.
-    var cache_name = '';
-    return self.caches.delete(cache_name)
-      .then(function() {
-          return self.caches.open(cache_name);
-        })
-      .then(function(cache) {
-          assert_true(cache instanceof Cache,
-                      'CacheStorage.open should accept an empty name.');
-        });
-  }, 'CacheStorage.open with an empty name');
-
-// promise_test(function(t) {
-//     return assert_promise_rejects(
-//       self.caches.open(),
-//       new TypeError(),
-//       'CacheStorage.open should throw TypeError if called with no arguments.');
-//   }, 'CacheStorage.open with no arguments');
-
-// promise_test(function(t) {
-//     var test_cases = [
-//       {
-//         name: 'cache-storage/lowercase',
-//         should_not_match:
-//           [
-//             'cache-storage/Lowercase',
-//             ' cache-storage/lowercase',
-//             'cache-storage/lowercase '
-//           ]
-//       },
-//       {
-//         name: 'cache-storage/has a space',
-//         should_not_match:
-//           [
-//             'cache-storage/has'
-//           ]
-//       },
-//       {
-//         name: 'cache-storage/has\000_in_the_name',
-//         should_not_match:
-//           [
-//             'cache-storage/has',
-//             'cache-storage/has_in_the_name'
-//           ]
-//       }
-//     ];
-//     return Promise.all(test_cases.map(function(testcase) {
-//         var cache_name = testcase.name;
-//         return self.caches.delete(cache_name)
-//           .then(function() {
-//               return self.caches.open(cache_name);
-//             })
-//           .then(function() {
-//               return self.caches.has(cache_name);
-//             })
-//           .then(function(result) {
-//               assert_true(result,
-//                           'CacheStorage.has should return true for existing ' +
-//                           'cache.');
-//             })
-//           .then(function() {
-//               return Promise.all(
-//                 testcase.should_not_match.map(function(cache_name) {
-//                     return self.caches.has(cache_name)
-//                       .then(function(result) {
-//                           assert_false(result,
-//                                        'CacheStorage.has should only perform ' +
-//                                        'exact matches on cache names.');
-//                         });
-//                   }));
-//             })
-//           .then(function() {
-//               return self.caches.delete(cache_name);
-//             });
-//       }));
-//   }, 'CacheStorage.has with existing cache');
-
-// promise_test(function(t) {
-//     return self.caches.has('cheezburger')
-//       .then(function(result) {
-//           assert_false(result,
-//                        'CacheStorage.has should return false for ' +
-//                        'nonexistent cache.');
-//         });
-//   }, 'CacheStorage.has with nonexistent cache');
-
-promise_test(function(t) {
-    var cache_name = 'cache-storage/open';
-    var url = '../resources/simple.txt';
-    var cache;
-    return self.caches.delete(cache_name)
-      .then(function() {
-          return self.caches.open(cache_name);
-        })
-      .then(function(result) {
-          cache = result;
-        })
-      .then(function() {
-          return cache.add('../resources/simple.txt');
-        })
-      .then(function() {
-          return self.caches.open(cache_name);
-        })
-      .then(function(result) {
-          assert_true(result instanceof Cache,
-                      'CacheStorage.open should return a Cache object');
-          // assert_not_equals(result, cache,
-          //                   'CacheStorage.open should return a new Cache ' +
-          //                   'object each time its called.');
-          return Promise.all([cache.keys(), result.keys()]);
-        })
-      .then(function(results) {
-          var expected_urls = results[0].map(function(r) { return r.url });
-          var actual_urls = results[1].map(function(r) { return r.url });
-          assert_array_equals(actual_urls, expected_urls,
-                              'CacheStorage.open should return a new Cache ' +
-                              'object for the same backing store.');
-        })
-  }, 'CacheStorage.open with existing cache');
-
-promise_test(function(t) {
-    var cache_name = 'cache-storage/delete';
-
-    return self.caches.delete(cache_name)
-      .then(function() {
-          return self.caches.open(cache_name);
-        })
-      .then(function() { return self.caches.delete(cache_name); })
-      .then(function(result) {
-          assert_true(result,
-                      'CacheStorage.delete should return true after ' +
-                      'deleting an existing cache.');
-        })
-
-      // .then(function() { return self.caches.has(cache_name); })
-      // .then(function(cache_exists) {
-      //     assert_false(cache_exists,
-      //                  'CacheStorage.has should return false after ' +
-      //                  'fulfillment of CacheStorage.delete promise.');
-      //   });
-  }, 'CacheStorage.delete with existing cache');
-
-// promise_test(function(t) {
-//     return self.caches.delete('cheezburger')
-//       .then(function(result) {
-//           assert_false(result,
-//                        'CacheStorage.delete should return false for a ' +
-//                        'nonexistent cache.');
-//         });
-//   }, 'CacheStorage.delete with nonexistent cache');
-
-// promise_test(function(t) {
-//     var bad_name = 'unpaired\uD800';
-//     var converted_name = 'unpaired\uFFFD'; // Don't create cache with this name.
-//     return self.caches.has(converted_name)
-//       .then(function(cache_exists) {
-//           assert_false(cache_exists,
-//                        'Test setup failure: cache should not exist');
-//       })
-//       .then(function() { return self.caches.open(bad_name); })
-//       .then(function() { return self.caches.keys(); })
-//       .then(function(keys) {
-//           assert_true(keys.indexOf(bad_name) !== -1,
-//                       'keys should include cache with bad name');
-//       })
-//       .then(function() { return self.caches.has(bad_name); })
-//       .then(function(cache_exists) {
-//           assert_true(cache_exists,
-//                       'CacheStorage names should be not be converted.');
-//         })
-//       .then(function() { return self.caches.has(converted_name); })
-//       .then(function(cache_exists) {
-//           assert_false(cache_exists,
-//                        'CacheStorage names should be not be converted.');
-//         });
-//   }, 'CacheStorage names are DOMStrings not USVStrings');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html
deleted file mode 100644
index 57e74b7..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-add.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html
deleted file mode 100644
index 7a5a43f..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-delete.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html
deleted file mode 100644
index 859b1cd..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-match.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html
deleted file mode 100644
index d67f939..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-put.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html
deleted file mode 100644
index ec7e14b..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage-keys.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html
deleted file mode 100644
index 937f143..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage-match.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html
deleted file mode 100644
index 62c6b63..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html
deleted file mode 100644
index 42e4b50..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-add.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html
deleted file mode 100644
index 754f785..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-delete.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html
deleted file mode 100644
index 093df8d..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-match.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html
deleted file mode 100644
index b32cfa2..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-put.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html
deleted file mode 100644
index acde773..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage-keys.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html
deleted file mode 100644
index 3c69d0f..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage-match.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html
deleted file mode 100644
index 7d015e3..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html
deleted file mode 100644
index 648bd59..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Verify access in sandboxed iframes</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script>
-
-function load_iframe(src, sandbox) {
-    return new Promise(function(resolve, reject) {
-        var iframe = document.createElement('iframe');
-        iframe.onload = function() { resolve(iframe); };
-
-        iframe.sandbox = sandbox;
-        iframe.src = src;
-
-        document.documentElement.appendChild(iframe);
-    });
-}
-
-function wait_for_message(id) {
-    return new Promise(function(resolve) {
-        self.addEventListener('message', function listener(e) {
-            if (e.data.id === id) {
-                resolve(e.data);
-                self.removeEventListener('message', listener);
-            }
-        });
-    });
-}
-
-var counter = 0;
-
-promise_test(function(t) {
-    return load_iframe('../resources/iframe.html',
-                       'allow-scripts allow-same-origin')
-        .then(function(iframe) {
-            var id = ++counter;
-            iframe.contentWindow.postMessage({id: id}, '*');
-            return wait_for_message(id);
-        })
-        .then(function(message) {
-            assert_equals(
-                message.result, 'allowed',
-                'Access should be allowed if sandbox has allow-same-origin');
-        });
-}, 'Sandboxed iframe with allow-same-origin is allowed access');
-
-promise_test(function(t) {
-    return load_iframe('../resources/iframe.html',
-                       'allow-scripts')
-        .then(function(iframe) {
-            var id = ++counter;
-            iframe.contentWindow.postMessage({id: id}, '*');
-            return wait_for_message(id);
-        })
-        .then(function(message) {
-            assert_equals(
-                message.result, 'denied',
-                'Access should be denied if sandbox lacks allow-same-origin');
-            assert_equals(message.name, 'SecurityError',
-                          'Failure should be a SecurityError');
-        });
-}, 'Sandboxed iframe without allow-same-origin is denied access');
-
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html
deleted file mode 100644
index 8e6deeb..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-add.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html
deleted file mode 100644
index 2dd06f3..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-delete.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html
deleted file mode 100644
index b0926fc..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-match.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html
deleted file mode 100644
index 8876930..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-put.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html
deleted file mode 100644
index 5c75ef8..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage-keys.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html
deleted file mode 100644
index 4d48683..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage-match.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html
deleted file mode 100644
index e10f5c5..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/idlharness.https.any.js b/third_party/web_platform_tests/service-workers/idlharness.https.any.js
new file mode 100644
index 0000000..8db5d4d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/idlharness.https.any.js
@@ -0,0 +1,53 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=cache-storage/resources/test-helpers.js
+// META: script=service-worker/resources/test-helpers.sub.js
+// META: timeout=long
+
+// https://w3c.github.io/ServiceWorker
+
+idl_test(
+  ['service-workers'],
+  ['dom', 'html'],
+  async (idl_array, t) => {
+    self.cacheInstance = await create_temporary_cache(t);
+
+    idl_array.add_objects({
+      CacheStorage: ['caches'],
+      Cache: ['self.cacheInstance'],
+      ServiceWorkerContainer: ['navigator.serviceWorker']
+    });
+
+    // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the
+    // other worker scopes.
+    if (self.GLOBAL.isWindow()) {
+      idl_array.add_objects({
+        ServiceWorkerRegistration: ['registrationInstance'],
+        ServiceWorker: ['registrationInstance.installing']
+      });
+
+      const scope = 'service-worker/resources/scope/idlharness';
+      const registration = await service_worker_unregister_and_register(
+          t, 'service-worker/resources/empty-worker.js', scope);
+      t.add_cleanup(() => registration.unregister());
+
+      self.registrationInstance = registration;
+    } else if (self.ServiceWorkerGlobalScope) {
+      // self.ServiceWorkerGlobalScope should only be defined for the
+      // ServiceWorker scope, which allows us to detect and test the interfaces
+      // exposed only for ServiceWorker.
+      idl_array.add_objects({
+        Clients: ['clients'],
+        ExtendableEvent: ['new ExtendableEvent("type")'],
+        FetchEvent: ['new FetchEvent("type", { request: new Request("") })'],
+        ServiceWorkerGlobalScope: ['self'],
+        ServiceWorkerRegistration: ['registration'],
+        ServiceWorker: ['serviceWorker'],
+        // TODO: Test instances of Client and WindowClient, e.g.
+        // Client: ['self.clientInstance'],
+        // WindowClient: ['self.windowClientInstance']
+      });
+    }
+  }
+);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
new file mode 100644
index 0000000..6f44bb1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker-Allowed header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const host_info = get_host_info();
+
+// Returns a URL for a service worker script whose Service-Worker-Allowed
+// header value is set to |allowed_path|. If |origin| is specified, that origin
+// is used.
+function build_script_url(allowed_path, origin) {
+  const script = 'resources/empty-worker.js';
+  const url = origin ? `${origin}${base_path()}${script}` : script;
+  return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`;
+}
+
+// register_test is a promise_test that registers a service worker.
+function register_test(script, scope, description) {
+  promise_test(async t => {
+    t.add_cleanup(() => {
+      return service_worker_unregister(t, scope);
+    });
+
+    const registration = await service_worker_unregister_and_register(
+        t, script, scope);
+    assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+    assert_equals(registration.scope, normalizeURL(scope));
+  }, description);
+}
+
+// register_fail_test is like register_test but expects a SecurityError.
+function register_fail_test(script, scope, description) {
+  promise_test(async t => {
+    t.add_cleanup(() => {
+      return service_worker_unregister(t, scope);
+    });
+
+    await service_worker_unregister(t, scope);
+    await promise_rejects_dom(t,
+                          'SecurityError',
+                          navigator.serviceWorker.register(script, {scope}));
+  }, description);
+}
+
+register_test(
+    build_script_url('/allowed-path'),
+    '/allowed-path',
+    'Registering within Service-Worker-Allowed path');
+
+register_test(
+    build_script_url(new URL('/allowed-path', document.location)),
+    '/allowed-path',
+    'Registering within Service-Worker-Allowed path (absolute URL)');
+
+register_test(
+    build_script_url('../allowed-path-with-parent'),
+    'allowed-path-with-parent',
+    'Registering within Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+    build_script_url('../allowed-path'),
+    '/disallowed-path',
+    'Registering outside Service-Worker-Allowed path'),
+
+register_fail_test(
+    build_script_url('../allowed-path-with-parent'),
+    '/allowed-path-with-parent',
+    'Registering outside Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+    build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+    'resources/this-scope-is-normally-allowed',
+    'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope');
+
+register_fail_test(
+    build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+    '/this-scope-is-normally-disallowed',
+    'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope');
+
+register_fail_test(
+    build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/',
+                     host_info.HTTPS_REMOTE_ORIGIN),
+    '/cross-origin/',
+    'Service-Worker-Allowed is cross-origin to page, same-origin to script');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
new file mode 100644
index 0000000..3e3cc8b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: close operation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+  'resources/close-worker.js', 'ServiceWorkerGlobalScope: close operation');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
new file mode 100644
index 0000000..525245f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+service_worker_test(
+    'resources/extendable-message-event-constructor-worker.js', document.title
+  );
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
new file mode 100644
index 0000000..89efd7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script src='./resources/extendable-message-event-utils.js'></script>
+<script>
+promise_test(function(t) {
+    var script = 'resources/extendable-message-event-worker.js';
+    var scope = 'resources/scope/extendable-message-event-from-toplevel';
+    var registration;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          registration = r;
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var saw_message = new Promise(function(resolve) {
+              navigator.serviceWorker.onmessage =
+                  function(event) { resolve(event.data); }
+            });
+          var channel = new MessageChannel;
+          registration.active.postMessage('', [channel.port1]);
+          return saw_message;
+        })
+      .then(function(results) {
+          var expected = {
+            constructor: { name: 'ExtendableMessageEvent' },
+            origin: location.origin,
+            lastEventId: '',
+            source: {
+                constructor: { name: 'WindowClient' },
+                frameType: 'top-level',
+                url: location.href,
+                visibilityState: 'visible',
+                focused: true
+            },
+            ports: [ { constructor: { name: 'MessagePort' } } ]
+          };
+          ExtendableMessageEventUtils.assert_equals(results, expected);
+        });
+  }, 'Post an extendable message from a top-level client');
+
+promise_test(function(t) {
+    var script = 'resources/extendable-message-event-worker.js';
+    var scope = 'resources/scope/extendable-message-event-from-nested';
+    var frame;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          add_completion_callback(function() { frame.remove(); });
+          var saw_message = new Promise(function(resolve) {
+              frame.contentWindow.navigator.serviceWorker.onmessage =
+                  function(event) { resolve(event.data); }
+            });
+          f.contentWindow.navigator.serviceWorker.controller.postMessage('');
+          return saw_message;
+        })
+      .then(function(results) {
+          var expected = {
+              constructor: { name: 'ExtendableMessageEvent' },
+              origin: location.origin,
+              lastEventId: '',
+              source: {
+                  constructor: { name: 'WindowClient' },
+                  url: frame.contentWindow.location.href,
+                  frameType: 'nested',
+                  visibilityState: 'visible',
+                  focused: false
+              },
+              ports: []
+            };
+          ExtendableMessageEventUtils.assert_equals(results, expected);
+        });
+  }, 'Post an extendable message from a nested client');
+
+promise_test(function(t) {
+    var script = 'resources/extendable-message-event-loopback-worker.js';
+    var scope = 'resources/scope/extendable-message-event-loopback';
+    var registration;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          registration = r;
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var results = [];
+          var saw_message = new Promise(function(resolve) {
+              navigator.serviceWorker.onmessage = function(event) {
+                switch (event.data.type) {
+                  case 'record':
+                    results.push(event.data.results);
+                    break;
+                  case 'finish':
+                    resolve(results);
+                    break;
+                }
+              };
+            });
+          registration.active.postMessage({type: 'start'});
+          return saw_message;
+        })
+      .then(function(results) {
+          assert_equals(results.length, 2);
+
+          var expected_trial_1 = {
+              constructor: { name: 'ExtendableMessageEvent' },
+              origin: location.origin,
+              lastEventId: '',
+              source: {
+                  constructor: { name: 'ServiceWorker' },
+                  scriptURL: normalizeURL(script),
+                  state: 'activated'
+              },
+              ports: []
+          };
+          assert_equals(results[0].trial, 1);
+          ExtendableMessageEventUtils.assert_equals(
+              results[0].event, expected_trial_1
+          );
+
+          var expected_trial_2 = {
+              constructor: { name: 'ExtendableMessageEvent' },
+              origin: location.origin,
+              lastEventId: '',
+              source: {
+                  constructor: { name: 'ServiceWorker' },
+                  scriptURL: normalizeURL(script),
+                  state: 'activated'
+              },
+              ports: [],
+          };
+          assert_equals(results[1].trial, 2);
+          ExtendableMessageEventUtils.assert_equals(
+              results[1].event, expected_trial_2
+          );
+        });
+  }, 'Post loopback extendable messages');
+
+promise_test(function(t) {
+    var script1 = 'resources/extendable-message-event-ping-worker.js';
+    var script2 = 'resources/extendable-message-event-pong-worker.js';
+    var scope = 'resources/scope/extendable-message-event-pingpong';
+    var registration;
+
+    return service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          registration = r;
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          // A controlled frame is necessary for keeping a waiting worker.
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          add_completion_callback(function() { frame.remove(); });
+          return navigator.serviceWorker.register(script2, {scope: scope});
+        })
+      .then(function(r) {
+          return wait_for_state(t, r.installing, 'installed');
+        })
+      .then(function() {
+          var results = [];
+          var saw_message = new Promise(function(resolve) {
+              navigator.serviceWorker.onmessage = function(event) {
+                switch (event.data.type) {
+                  case 'record':
+                    results.push(event.data.results);
+                    break;
+                  case 'finish':
+                    resolve(results);
+                    break;
+                }
+              };
+            });
+          registration.active.postMessage({type: 'start'});
+          return saw_message;
+        })
+      .then(function(results) {
+          assert_equals(results.length, 2);
+
+          var expected_ping = {
+              constructor: { name: 'ExtendableMessageEvent' },
+              origin: location.origin,
+              lastEventId: '',
+              source: {
+                  constructor: { name: 'ServiceWorker' },
+                  scriptURL: normalizeURL(script1),
+                  state: 'activated'
+              },
+              ports: []
+          };
+          assert_equals(results[0].pingOrPong, 'ping');
+          ExtendableMessageEventUtils.assert_equals(
+              results[0].event, expected_ping
+          );
+
+          var expected_pong = {
+              constructor: { name: 'ExtendableMessageEvent' },
+              origin: location.origin,
+              lastEventId: '',
+              source: {
+                  constructor: { name: 'ServiceWorker' },
+                  scriptURL: normalizeURL(script2),
+                  state: 'installed'
+              },
+              ports: []
+          };
+          assert_equals(results[1].pingOrPong, 'pong');
+          ExtendableMessageEventUtils.assert_equals(
+              results[1].event, expected_pong
+          );
+        });
+  }, 'Post extendable messages among service workers');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
new file mode 100644
index 0000000..5ca5f65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
@@ -0,0 +1,14 @@
+// META: title=fetch method on the right interface
+// META: global=serviceworker
+
+test(function() {
+    assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' +
+        'instance should not have "fetch" method as its property.');
+    assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' +
+        'inherit "fetch" method.');
+    assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch',
+        'WorkerGlobalScope should have "fetch" propery in its prototype.');
+    assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch,
+        'ServiceWorkerGlobalScope.fetch should be the same as ' +
+        'WorkerGlobalScope.fetch.');
+}, 'Fetch method on the right interface');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
new file mode 100644
index 0000000..399820d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Service Worker: isSecureContext</title>
+</head>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+    var url = 'isSecureContext.serviceworker.js';
+    var scope = 'empty.html';
+    var frame_sw, sw_registration;
+
+    await service_worker_unregister(t, scope);
+    var f = await with_iframe(scope);
+    t.add_cleanup(function() {
+        f.remove();
+    });
+    frame_sw = f.contentWindow.navigator.serviceWorker;
+    var registration = await navigator.serviceWorker.register(url, {scope: scope});
+    sw_registration = registration;
+    await wait_for_state(t, registration.installing, 'activated');
+    fetch_tests_from_worker(sw_registration.active);
+}, 'Setting up tests');
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
new file mode 100644
index 0000000..5033594
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+    assert_true(self.isSecureContext, true);
+}, "isSecureContext");
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
new file mode 100644
index 0000000..99dedeb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: postMessage</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/postmessage-loopback-worker.js';
+    var scope = 'resources/scope/postmessage-loopback';
+    var registration;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = function(event) {
+                resolve(event.data);
+              };
+            });
+          registration.active.postMessage({port: channel.port2},
+                                          [channel.port2]);
+          return saw_message;
+        })
+      .then(function(result) {
+          assert_equals(result, 'OK');
+        });
+  }, 'Post loopback messages');
+
+promise_test(function(t) {
+    var script1 = 'resources/postmessage-ping-worker.js';
+    var script2 = 'resources/postmessage-pong-worker.js';
+    var scope = 'resources/scope/postmessage-pingpong';
+    var registration;
+    var frame;
+
+    return service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          // A controlled frame is necessary for keeping a waiting worker.
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          return navigator.serviceWorker.register(script2, {scope: scope});
+        })
+      .then(function(r) {
+          return wait_for_state(t, r.installing, 'installed');
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = function(event) {
+                resolve(event.data);
+              };
+            });
+          registration.active.postMessage({port: channel.port2},
+                                          [channel.port2]);
+          return saw_message;
+        })
+      .then(function(result) {
+          assert_equals(result, 'OK');
+          frame.remove();
+        });
+  }, 'Post messages among service workers');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
new file mode 100644
index 0000000..aa3c74a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: registration</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/registration-attribute-worker.js';
+    var scope = 'resources/scope/registration-attribute';
+    var registration;
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(reg) {
+          registration = reg;
+          add_result_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(frame) {
+          var expected_events_seen = [
+            'updatefound',
+            'install',
+            'statechange(installed)',
+            'statechange(activating)',
+            'activate',
+            'statechange(activated)',
+            'fetch',
+          ];
+
+          assert_equals(
+              frame.contentDocument.body.textContent,
+              expected_events_seen.toString(),
+              'Service Worker should respond to fetch');
+          frame.remove();
+          return registration.unregister();
+        });
+  }, 'Verify registration attributes on ServiceWorkerGlobalScope');
+
+promise_test(function(t) {
+    var script = 'resources/registration-attribute-worker.js';
+    var newer_script = 'resources/registration-attribute-newer-worker.js';
+    var scope = 'resources/scope/registration-attribute';
+    var newer_worker;
+    var registration;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(reg) {
+          registration = reg;
+          add_result_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return navigator.serviceWorker.register(newer_script, {scope: scope});
+        })
+      .then(function(reg) {
+          assert_equals(reg, registration);
+          newer_worker = registration.installing;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var channel = new MessageChannel;
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = function(e) { resolve(e.data); };
+            });
+          newer_worker.postMessage({port: channel.port2}, [channel.port2]);
+          return saw_message;
+        })
+      .then(function(results) {
+          var script_url = normalizeURL(script);
+          var newer_script_url = normalizeURL(newer_script);
+          var expectations = [
+            'evaluate',
+            '  installing: empty',
+            '  waiting: empty',
+            '  active: ' + script_url,
+            'updatefound',
+            '  installing: ' + newer_script_url,
+            '  waiting: empty',
+            '  active: ' + script_url,
+            'install',
+            '  installing: ' + newer_script_url,
+            '  waiting: empty',
+            '  active: ' + script_url,
+            'statechange(installed)',
+            '  installing: empty',
+            '  waiting: ' + newer_script_url,
+            '  active: ' + script_url,
+            'statechange(activating)',
+            '  installing: empty',
+            '  waiting: empty',
+            '  active: ' + newer_script_url,
+            'activate',
+            '  installing: empty',
+            '  waiting: empty',
+            '  active: ' + newer_script_url,
+            'statechange(activated)',
+            '  installing: empty',
+            '  waiting: empty',
+            '  active: ' + newer_script_url,
+          ];
+          assert_array_equals(results, expectations);
+          return registration.unregister();
+        });
+  }, 'Verify registration attributes on ServiceWorkerGlobalScope of the ' +
+     'newer worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
new file mode 100644
index 0000000..41a8bc0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
@@ -0,0 +1,5 @@
+importScripts('../../resources/worker-testharness.js');
+
+test(function() {
+  assert_false('close' in self);
+}, 'ServiceWorkerGlobalScope close operation');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
new file mode 100644
index 0000000..f6838ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
@@ -0,0 +1,12 @@
+var source;
+
+self.addEventListener('message', function(e) {
+  source = e.source;
+  throw 'testError';
+});
+
+self.addEventListener('error', function(e) {
+  source.postMessage({
+    error: e.error, filename: e.filename, message: e.message, lineno: e.lineno,
+    colno: e.colno});
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
new file mode 100644
index 0000000..42da582
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
@@ -0,0 +1,197 @@
+importScripts('/resources/testharness.js');
+
+const TEST_OBJECT = { wanwan: 123 };
+const CHANNEL1 = new MessageChannel();
+const CHANNEL2 = new MessageChannel();
+const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1];
+function createEvent(initializer) {
+  if (initializer === undefined)
+    return new ExtendableMessageEvent('type');
+  return new ExtendableMessageEvent('type', initializer);
+}
+
+// These test cases are mostly copied from the following file in the Chromium
+// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb):
+// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html
+
+test(function() {
+  assert_false(createEvent().bubbles);
+  assert_false(createEvent().cancelable);
+  assert_equals(createEvent().data, null);
+  assert_equals(createEvent().origin, '');
+  assert_equals(createEvent().lastEventId, '');
+  assert_equals(createEvent().source, null);
+  assert_array_equals(createEvent().ports, []);
+}, 'no initializer specified');
+
+test(function() {
+  assert_false(createEvent({ bubbles: false }).bubbles);
+  assert_true(createEvent({ bubbles: true }).bubbles);
+}, '`bubbles` is specified');
+
+test(function() {
+  assert_false(createEvent({ cancelable: false }).cancelable);
+  assert_true(createEvent({ cancelable: true }).cancelable);
+}, '`cancelable` is specified');
+
+test(function() {
+  assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT);
+  assert_equals(createEvent({ data: undefined }).data, null);
+  assert_equals(createEvent({ data: null }).data, null);
+  assert_equals(createEvent({ data: false }).data, false);
+  assert_equals(createEvent({ data: true }).data, true);
+  assert_equals(createEvent({ data: '' }).data, '');
+  assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate');
+  assert_equals(createEvent({ data: 12345 }).data, 12345);
+  assert_equals(createEvent({ data: 18446744073709551615 }).data,
+                            18446744073709552000);
+  assert_equals(createEvent({ data: NaN }).data, NaN);
+  // Note that valueOf() is not called, when the left hand side is
+  // evaluated.
+  assert_false(
+      createEvent({ data: {
+          valueOf: function() { return TEST_OBJECT; } } }).data ==
+      TEST_OBJECT);
+  assert_equals(createEvent({ get data(){ return 123; } }).data, 123);
+  let thrown = { name: 'Error' };
+  assert_throws_exactly(thrown, function() {
+      createEvent({ get data() { throw thrown; } }); });
+}, '`data` is specified');
+
+test(function() {
+  assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy');
+  assert_equals(createEvent({ origin: '' }).origin, '');
+  assert_equals(createEvent({ origin: null }).origin, 'null');
+  assert_equals(createEvent({ origin: false }).origin, 'false');
+  assert_equals(createEvent({ origin: true }).origin, 'true');
+  assert_equals(createEvent({ origin: 12345 }).origin, '12345');
+  assert_equals(
+      createEvent({ origin: 18446744073709551615 }).origin,
+      '18446744073709552000');
+  assert_equals(createEvent({ origin: NaN }).origin, 'NaN');
+  assert_equals(createEvent({ origin: [] }).origin, '');
+  assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3');
+  assert_equals(
+      createEvent({ origin: { melancholy: 12345 } }).origin,
+      '[object Object]');
+  // Note that valueOf() is not called, when the left hand side is
+  // evaluated.
+  assert_equals(
+      createEvent({ origin: {
+          valueOf: function() { return 'melancholy'; } } }).origin,
+      '[object Object]');
+  assert_equals(
+      createEvent({ get origin() { return 123; } }).origin, '123');
+  let thrown = { name: 'Error' };
+  assert_throws_exactly(thrown, function() {
+      createEvent({ get origin() { throw thrown; } }); });
+}, '`origin` is specified');
+
+test(function() {
+  assert_equals(
+      createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy');
+  assert_equals(createEvent({ lastEventId: '' }).lastEventId, '');
+  assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null');
+  assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false');
+  assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true');
+  assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345');
+  assert_equals(
+      createEvent({ lastEventId: 18446744073709551615 }).lastEventId,
+      '18446744073709552000');
+  assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN');
+  assert_equals(createEvent({ lastEventId: [] }).lastEventId, '');
+  assert_equals(
+      createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3');
+  assert_equals(
+      createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId,
+      '[object Object]');
+  // Note that valueOf() is not called, when the left hand side is
+  // evaluated.
+  assert_equals(
+      createEvent({ lastEventId: {
+          valueOf: function() { return 'melancholy'; } } }).lastEventId,
+      '[object Object]');
+  assert_equals(
+      createEvent({ get lastEventId() { return 123; } }).lastEventId,
+      '123');
+  let thrown = { name: 'Error' };
+  assert_throws_exactly(thrown, function() {
+      createEvent({ get lastEventId() { throw thrown; } }); });
+}, '`lastEventId` is specified');
+
+test(function() {
+  assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+  assert_equals(
+      createEvent({ source: self.registration.active }).source,
+      self.registration.active);
+  assert_equals(
+      createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+  assert_throws_js(
+      TypeError, function() { createEvent({ source: this }); },
+      'source should be Client or ServiceWorker or MessagePort');
+}, '`source` is specified');
+
+test(function() {
+  // Valid message ports.
+  var passed_ports = createEvent({ ports: PORTS}).ports;
+  assert_equals(passed_ports[0], CHANNEL1.port1);
+  assert_equals(passed_ports[1], CHANNEL1.port2);
+  assert_equals(passed_ports[2], CHANNEL2.port1);
+  assert_array_equals(createEvent({ ports: [] }).ports, []);
+  assert_array_equals(createEvent({ ports: undefined }).ports, []);
+
+  // Invalid message ports.
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: [1, 2, 3] }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: TEST_OBJECT }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: null }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: this }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: false }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: true }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: '' }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: 'chocolate' }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: 12345 }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: 18446744073709551615 }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ ports: NaN }); });
+  assert_throws_js(TypeError,
+      function() { createEvent({ get ports() { return 123; } }); });
+  let thrown = { name: 'Error' };
+  assert_throws_exactly(thrown, function() {
+      createEvent({ get ports() { throw thrown; } }); });
+  // Note that valueOf() is not called, when the left hand side is
+  // evaluated.
+  var valueOf = function() { return PORTS; };
+  assert_throws_js(TypeError, function() {
+      createEvent({ ports: { valueOf: valueOf } }); });
+}, '`ports` is specified');
+
+test(function() {
+  var initializers = {
+      bubbles: true,
+      cancelable: true,
+      data: TEST_OBJECT,
+      origin: 'wonderful',
+      lastEventId: 'excellent',
+      source: CHANNEL1.port1,
+      ports: PORTS
+  };
+  assert_equals(createEvent(initializers).bubbles, true);
+  assert_equals(createEvent(initializers).cancelable, true);
+  assert_equals(createEvent(initializers).data, TEST_OBJECT);
+  assert_equals(createEvent(initializers).origin, 'wonderful');
+  assert_equals(createEvent(initializers).lastEventId, 'excellent');
+  assert_equals(createEvent(initializers).source, CHANNEL1.port1);
+  assert_equals(createEvent(initializers).ports[0], PORTS[0]);
+  assert_equals(createEvent(initializers).ports[1], PORTS[1]);
+  assert_equals(createEvent(initializers).ports[2], PORTS[2]);
+}, 'all initial values are specified');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
new file mode 100644
index 0000000..13cae88
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
@@ -0,0 +1,36 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+    switch (event.data.type) {
+      case 'start':
+        self.registration.active.postMessage(
+            {type: '1st', client_id: event.source.id});
+        break;
+      case '1st':
+        // 1st loopback message via ServiceWorkerRegistration.active.
+        var results = {
+            trial: 1,
+            event: ExtendableMessageEventUtils.serialize(event)
+        };
+        var client_id = event.data.client_id;
+        event.source.postMessage({type: '2nd', client_id: client_id});
+        event.waitUntil(clients.get(client_id)
+            .then(function(client) {
+                client.postMessage({type: 'record', results: results});
+              }));
+        break;
+      case '2nd':
+        // 2nd loopback message via ExtendableMessageEvent.source.
+        var results = {
+            trial: 2,
+            event: ExtendableMessageEventUtils.serialize(event)
+        };
+        var client_id = event.data.client_id;
+        event.waitUntil(clients.get(client_id)
+            .then(function(client) {
+                client.postMessage({type: 'record', results: results});
+                client.postMessage({type: 'finish'});
+              }));
+        break;
+      }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
new file mode 100644
index 0000000..d07b229
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
@@ -0,0 +1,23 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+    switch (event.data.type) {
+      case 'start':
+        // Send a ping message to another service worker.
+        self.registration.waiting.postMessage(
+            {type: 'ping', client_id: event.source.id});
+        break;
+      case 'pong':
+        var results = {
+            pingOrPong: 'pong',
+            event: ExtendableMessageEventUtils.serialize(event)
+        };
+        var client_id = event.data.client_id;
+        event.waitUntil(clients.get(client_id)
+            .then(function(client) {
+                client.postMessage({type: 'record', results: results});
+                client.postMessage({type: 'finish'});
+              }));
+        break;
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
new file mode 100644
index 0000000..5e9669e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
@@ -0,0 +1,18 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+    switch (event.data.type) {
+      case 'ping':
+        var results = {
+            pingOrPong: 'ping',
+            event: ExtendableMessageEventUtils.serialize(event)
+        };
+        var client_id = event.data.client_id;
+        event.waitUntil(clients.get(client_id)
+            .then(function(client) {
+                client.postMessage({type: 'record', results: results});
+                event.source.postMessage({type: 'pong', client_id: client_id});
+              }));
+        break;
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
new file mode 100644
index 0000000..d6a3b48
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
@@ -0,0 +1,78 @@
+var ExtendableMessageEventUtils = {};
+
+// Create a representation of a given ExtendableMessageEvent that is suitable
+// for transmission via the `postMessage` API.
+ExtendableMessageEventUtils.serialize = function(event) {
+  var ports = event.ports.map(function(port) {
+        return { constructor: { name: port.constructor.name } };
+    });
+  return {
+    constructor: {
+      name: event.constructor.name
+    },
+    origin: event.origin,
+    lastEventId: event.lastEventId,
+    source: {
+      constructor: {
+        name: event.source.constructor.name
+      },
+      url: event.source.url,
+      frameType: event.source.frameType,
+      visibilityState: event.source.visibilityState,
+      focused: event.source.focused
+    },
+    ports: ports
+  };
+};
+
+// Compare the actual and expected values of an ExtendableMessageEvent that has
+// been transformed using the `serialize` function defined in this file.
+ExtendableMessageEventUtils.assert_equals = function(actual, expected) {
+  assert_equals(
+    actual.constructor.name, expected.constructor.name, 'event constructor'
+  );
+  assert_equals(actual.origin, expected.origin, 'event `origin` property');
+  assert_equals(
+    actual.lastEventId,
+    expected.lastEventId,
+    'event `lastEventId` property'
+  );
+
+  assert_equals(
+    actual.source.constructor.name,
+    expected.source.constructor.name,
+    'event `source` property constructor'
+  );
+  assert_equals(
+    actual.source.url, expected.source.url, 'event `source` property `url`'
+  );
+  assert_equals(
+    actual.source.frameType,
+    expected.source.frameType,
+    'event `source` property `frameType`'
+  );
+  assert_equals(
+    actual.source.visibilityState,
+    expected.source.visibilityState,
+    'event `source` property `visibilityState`'
+  );
+  assert_equals(
+    actual.source.focused,
+    expected.source.focused,
+    'event `source` property `focused`'
+  );
+
+  assert_equals(
+    actual.ports.length,
+    expected.ports.length,
+    'event `ports` property length'
+  );
+
+  for (var idx = 0; idx < expected.ports.length; ++idx) {
+    assert_equals(
+      actual.ports[idx].constructor.name,
+      expected.ports[idx].constructor.name,
+      'MessagePort #' + idx + ' constructor'
+    );
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
new file mode 100644
index 0000000..f5e7647
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
@@ -0,0 +1,5 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+    event.source.postMessage(ExtendableMessageEventUtils.serialize(event));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
new file mode 100644
index 0000000..083e9aa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+    if ('port' in event.data) {
+      var port = event.data.port;
+
+      var channel = new MessageChannel();
+      channel.port1.onmessage = function(event) {
+        if ('pong' in event.data)
+          port.postMessage(event.data.pong);
+      };
+      self.registration.active.postMessage({ping: channel.port2},
+                                           [channel.port2]);
+    } else if ('ping' in event.data) {
+      event.data.ping.postMessage({pong: 'OK'});
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
new file mode 100644
index 0000000..ebb1ecc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+    if ('port' in event.data) {
+      var port = event.data.port;
+
+      var channel = new MessageChannel();
+      channel.port1.onmessage = function(event) {
+        if ('pong' in event.data)
+          port.postMessage(event.data.pong);
+      };
+
+      // Send a ping message to another service worker.
+      self.registration.waiting.postMessage({ping: channel.port2},
+                                            [channel.port2]);
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
new file mode 100644
index 0000000..4a0d90b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
@@ -0,0 +1,4 @@
+self.addEventListener('message', function(event) {
+    if ('ping' in event.data)
+      event.data.ping.postMessage({pong: 'OK'});
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
new file mode 100644
index 0000000..44f3e2e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
@@ -0,0 +1,33 @@
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var results = [];
+
+function stringify(worker) {
+  return worker ? worker.scriptURL : 'empty';
+}
+
+function record(event_name) {
+  results.push(event_name);
+  results.push('  installing: ' + stringify(self.registration.installing));
+  results.push('  waiting: ' + stringify(self.registration.waiting));
+  results.push('  active: ' + stringify(self.registration.active));
+}
+
+record('evaluate');
+
+self.registration.addEventListener('updatefound', function() {
+    record('updatefound');
+    var worker = self.registration.installing;
+    self.registration.installing.addEventListener('statechange', function() {
+        record('statechange(' + worker.state + ')');
+      });
+  });
+
+self.addEventListener('install', function(e) { record('install'); });
+
+self.addEventListener('activate', function(e) { record('activate'); });
+
+self.addEventListener('message', function(e) {
+    e.data.port.postMessage(results);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
new file mode 100644
index 0000000..315f437
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
@@ -0,0 +1,139 @@
+importScripts('../../resources/test-helpers.sub.js');
+importScripts('../../resources/worker-testharness.js');
+
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var events_seen = [];
+
+// TODO(nhiroki): Move these assertions to registration-attribute.html because
+// an assertion failure on the worker is not shown on the result page and
+// handled as timeout. See registration-attribute-newer-worker.js for example.
+
+assert_equals(
+  self.registration.scope,
+  normalizeURL('scope/registration-attribute'),
+  'On worker script evaluation, registration attribute should be set');
+assert_equals(
+  self.registration.installing,
+  null,
+  'On worker script evaluation, installing worker should be null');
+assert_equals(
+  self.registration.waiting,
+  null,
+  'On worker script evaluation, waiting worker should be null');
+assert_equals(
+  self.registration.active,
+  null,
+  'On worker script evaluation, active worker should be null');
+
+self.registration.addEventListener('updatefound', function() {
+    events_seen.push('updatefound');
+
+    assert_equals(
+      self.registration.scope,
+      normalizeURL('scope/registration-attribute'),
+      'On updatefound event, registration attribute should be set');
+    assert_equals(
+      self.registration.installing.scriptURL,
+      normalizeURL('registration-attribute-worker.js'),
+      'On updatefound event, installing worker should be set');
+    assert_equals(
+      self.registration.waiting,
+      null,
+      'On updatefound event, waiting worker should be null');
+    assert_equals(
+      self.registration.active,
+      null,
+      'On updatefound event, active worker should be null');
+
+    assert_equals(
+      self.registration.installing.state,
+      'installing',
+      'On updatefound event, worker should be in the installing state');
+
+    var worker = self.registration.installing;
+    self.registration.installing.addEventListener('statechange', function() {
+        events_seen.push('statechange(' + worker.state + ')');
+      });
+  });
+
+self.addEventListener('install', function(e) {
+    events_seen.push('install');
+
+    assert_equals(
+      self.registration.scope,
+      normalizeURL('scope/registration-attribute'),
+      'On install event, registration attribute should be set');
+    assert_equals(
+      self.registration.installing.scriptURL,
+      normalizeURL('registration-attribute-worker.js'),
+      'On install event, installing worker should be set');
+    assert_equals(
+      self.registration.waiting,
+      null,
+      'On install event, waiting worker should be null');
+    assert_equals(
+      self.registration.active,
+      null,
+      'On install event, active worker should be null');
+
+    assert_equals(
+      self.registration.installing.state,
+      'installing',
+      'On install event, worker should be in the installing state');
+  });
+
+self.addEventListener('activate', function(e) {
+    events_seen.push('activate');
+
+    assert_equals(
+      self.registration.scope,
+      normalizeURL('scope/registration-attribute'),
+      'On activate event, registration attribute should be set');
+    assert_equals(
+      self.registration.installing,
+      null,
+      'On activate event, installing worker should be null');
+    assert_equals(
+      self.registration.waiting,
+      null,
+      'On activate event, waiting worker should be null');
+    assert_equals(
+      self.registration.active.scriptURL,
+      normalizeURL('registration-attribute-worker.js'),
+      'On activate event, active worker should be set');
+
+    assert_equals(
+      self.registration.active.state,
+      'activating',
+      'On activate event, worker should be in the activating state');
+  });
+
+self.addEventListener('fetch', function(e) {
+    events_seen.push('fetch');
+
+    assert_equals(
+      self.registration.scope,
+      normalizeURL('scope/registration-attribute'),
+      'On fetch event, registration attribute should be set');
+    assert_equals(
+      self.registration.installing,
+      null,
+      'On fetch event, installing worker should be null');
+    assert_equals(
+      self.registration.waiting,
+      null,
+      'On fetch event, waiting worker should be null');
+    assert_equals(
+      self.registration.active.scriptURL,
+      normalizeURL('registration-attribute-worker.js'),
+      'On fetch event, active worker should be set');
+
+    assert_equals(
+      self.registration.active.state,
+      'activated',
+      'On fetch event, worker should be in the activated state');
+
+    e.respondWith(new Response(events_seen));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
new file mode 100644
index 0000000..6da397d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
@@ -0,0 +1,25 @@
+function matchQuery(query) {
+  return self.location.href.indexOf(query) != -1;
+}
+
+if (matchQuery('?evaluation'))
+  self.registration.unregister();
+
+self.addEventListener('install', function(e) {
+    if (matchQuery('?install')) {
+      // Don't do waitUntil(unregister()) as that would deadlock as specified.
+      self.registration.unregister();
+    }
+  });
+
+self.addEventListener('activate', function(e) {
+    if (matchQuery('?activate'))
+      e.waitUntil(self.registration.unregister());
+  });
+
+self.addEventListener('message', function(e) {
+    e.waitUntil(self.registration.unregister()
+      .then(function(result) {
+          e.data.port.postMessage({result: result});
+        }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
new file mode 100644
index 0000000..8be8a1f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
@@ -0,0 +1,22 @@
+var events_seen = [];
+
+self.registration.addEventListener('updatefound', function() {
+    events_seen.push('updatefound');
+  });
+
+self.addEventListener('activate', function(e) {
+    events_seen.push('activate');
+  });
+
+self.addEventListener('fetch', function(e) {
+    events_seen.push('fetch');
+    e.respondWith(new Response(events_seen));
+  });
+
+self.addEventListener('message', function(e) {
+    events_seen.push('message');
+    self.registration.update();
+  });
+
+// update() during the script evaluation should be ignored.
+self.registration.update();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
new file mode 100644
index 0000000..8a87e1b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
@@ -0,0 +1,16 @@
+import os
+import time
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+    # update() does not bypass cache so set the max-age to 0 such that update()
+    # can find a new version in the network.
+    headers = [(b'Cache-Control', b'max-age: 0'),
+               (b'Content-Type', b'application/javascript')]
+    with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+                           u'update-worker.js'), u'r') as file:
+        script = file.read()
+    # Return a different script for each access.
+    return headers, u'// %s\n%s' % (time.time(), script)
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
new file mode 100644
index 0000000..988f546
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: Error event error message</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+promise_test(t => {
+    var script = 'resources/error-worker.js';
+    var scope = 'resources/scope/service-worker-error-event';
+    var error_name = 'testError'
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          var worker = registration.installing;
+          add_completion_callback(() => { registration.unregister(); });
+          return new Promise(function(resolve) {
+              navigator.serviceWorker.onmessage = resolve;
+              worker.postMessage('');
+            });
+        })
+      .then(e => {
+          assert_equals(e.data.error, error_name, 'error type');
+          assert_greater_than(
+            e.data.message.indexOf(error_name), -1, 'error message');
+          assert_greater_than(
+            e.data.filename.indexOf(script), -1, 'filename');
+          assert_equals(e.data.lineno, 5, 'error line number');
+          assert_equals(e.data.colno, 3, 'error column number');
+        });
+  }, 'Error handlers inside serviceworker should see the attributes of ' +
+     'ErrorEvent');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
new file mode 100644
index 0000000..1a124d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: unregister</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/unregister-worker.js?evaluation';
+    var scope = 'resources/scope/unregister-on-script-evaluation';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'redundant');
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            undefined,
+            'After unregister(), the registration should not found');
+        });
+  }, 'Unregister on script evaluation');
+
+promise_test(function(t) {
+    var script = 'resources/unregister-worker.js?install';
+    var scope = 'resources/scope/unregister-on-install-event';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'redundant');
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            undefined,
+            'After unregister(), the registration should not found');
+        });
+  }, 'Unregister on installing event');
+
+promise_test(function(t) {
+    var script = 'resources/unregister-worker.js?activate';
+    var scope = 'resources/scope/unregister-on-activate-event';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'redundant');
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            undefined,
+            'After unregister(), the registration should not found');
+        });
+  }, 'Unregister on activate event');
+
+promise_test(function(t) {
+    var script = 'resources/unregister-worker.js';
+    var scope = 'resources/unregister-controlling-worker.html';
+
+    var controller;
+    var frame;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          controller = frame.contentWindow.navigator.serviceWorker.controller;
+
+          assert_equals(
+            controller.scriptURL,
+            normalizeURL(script),
+            'Service worker should control a new document')
+
+          // Wait for the completion of unregister() on the worker.
+          var channel = new MessageChannel();
+          var promise = new Promise(function(resolve) {
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_true(e.data.result,
+                              'unregister() should successfully finish');
+                  resolve();
+                });
+            });
+          controller.postMessage({port: channel.port2}, [channel.port2]);
+          return promise;
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            undefined,
+            'After unregister(), the registration should not found');
+          assert_equals(
+            frame.contentWindow.navigator.serviceWorker.controller,
+            controller,
+            'After unregister(), the worker should still control the document');
+          return with_iframe(scope);
+        })
+      .then(function(new_frame) {
+          assert_equals(
+            new_frame.contentWindow.navigator.serviceWorker.controller,
+            null,
+            'After unregister(), the worker should not control a new document');
+
+          frame.remove();
+          new_frame.remove();
+        })
+  }, 'Unregister controlling service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
new file mode 100644
index 0000000..a7dde22
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: update</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/update-worker.py';
+    var scope = 'resources/scope/update';
+    var registration;
+    var frame1;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame1 = f;
+          registration.active.postMessage('update');
+          return wait_for_update(t, registration);
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(frame2) {
+          var expected_events_seen = [
+            'updatefound',  // by register().
+            'activate',
+            'fetch',
+            'message',
+            'updatefound',  // by update() in the message handler.
+            'fetch',
+          ];
+          assert_equals(
+              frame2.contentDocument.body.textContent,
+              expected_events_seen.toString(),
+              'events seen by the worker');
+          frame1.remove();
+          frame2.remove();
+        });
+  }, 'Update a registration on ServiceWorkerGlobalScope');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html b/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html
new file mode 100644
index 0000000..b6efe3e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<title>Service Worker: about:blank replacement handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test attempts to verify various initial about:blank document
+// creation is accurately reflected via the Clients API.  The goal is
+// for Clients API to reflect what the browser actually does and not
+// to make special cases for the API.
+//
+// If your browser does not create an about:blank document in certain
+// cases then please just mark the test expected fail for now.  The
+// reuse of globals from about:blank documents to the final load document
+// has particularly bad interop at the moment.  Hopefully we can evolve
+// tests like this to eventually align browsers.
+
+const worker = 'resources/about-blank-replacement-worker.js';
+
+// Helper routine that creates an iframe that internally has some kind
+// of nested window.  The nested window could be another iframe or
+// it could be a popup window.
+function createFrameWithNestedWindow(url) {
+  return new Promise((resolve, reject) => {
+    let frame = document.createElement('iframe');
+    frame.src = url;
+    document.body.appendChild(frame);
+
+    window.addEventListener('message', function onMsg(evt) {
+      if (evt.data.type !== 'NESTED_LOADED') {
+        return;
+      }
+      window.removeEventListener('message', onMsg);
+      if (evt.data.result && evt.data.result.startsWith('failure:')) {
+        reject(evt.data.result);
+        return;
+      }
+      resolve(frame);
+    });
+  });
+}
+
+// Helper routine to request the given worker find the client with
+// the specified URL using the clients.matchAll() API.
+function getClientIdByURL(worker, url) {
+  return new Promise(resolve => {
+    navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+      if (evt.data.type !== 'GET_CLIENT_ID') {
+        return;
+      }
+      navigator.serviceWorker.removeEventListener('message', onMsg);
+      resolve(evt.data.result);
+    });
+    worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() });
+  });
+}
+
+async function doAsyncTest(t, scope) {
+  let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Load the scope as a frame.  We expect this in turn to have a nested
+  // iframe.  The service worker will intercept the load of the nested
+  // iframe and populate its body with the client ID of the initial
+  // about:blank document it sees via clients.matchAll().
+  let frame = await createFrameWithNestedWindow(scope);
+  let initialResult = frame.contentWindow.nested().document.body.textContent;
+  assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`);
+
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+                frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL,
+                'nested about:blank should have same controlling service worker');
+
+  // Next, ask the service worker to find the final client ID for the fully
+  // loaded nested frame.
+  let nestedURL = new URL(frame.contentWindow.nested().location);
+  let finalResult = await getClientIdByURL(reg.active, nestedURL);
+  assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`);
+
+  // If the nested frame doesn't have a URL to load, then there is no fetch
+  // event and the body should be empty.  We can't verify the final client ID
+  // against anything.
+  if (nestedURL.href === 'about:blank' ||
+      nestedURL.href === 'about:srcdoc') {
+    assert_equals('', initialResult, 'about:blank text content should be blank');
+  }
+
+  // If the nested URL is not about:blank, though, then the fetch event handler
+  // should have populated the body with the client id of the initial about:blank.
+  // Verify the final client id matches.
+  else {
+    assert_equals(initialResult, finalResult, 'client ID values should match');
+  }
+
+  frame.remove();
+}
+
+promise_test(async function(t) {
+  // Execute a test where the nested frame is simply loaded normally.
+  await doAsyncTest(t, 'resources/about-blank-replacement-frame.py');
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+   'matches final Client.');
+
+promise_test(async function(t) {
+  // Execute a test where the nested frame is modified immediately by
+  // its parent.  In this case we add a message listener so the service
+  // worker can ping the client to verify its existence.  This ping-pong
+  // check is performed during the initial load and when verifying the
+  // final loaded client.
+  await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py');
+}, 'Initial about:blank modified by parent is controlled, exposed to ' +
+   'clients.matchAll(), and matches final Client.');
+
+promise_test(async function(t) {
+  // Execute a test where the nested window is a popup window instead of
+  // an iframe.  This should behave the same as the simple iframe case.
+  await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py');
+}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+   'matches final Client.');
+
+promise_test(async function(t) {
+  const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html';
+
+  let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Load the scope as a frame.  We expect this in turn to have a nested
+  // iframe.  Unlike the other tests in this file the nested iframe URL
+  // is not covered by a service worker scope.  It should end up as
+  // uncontrolled even though its initial about:blank is controlled.
+  let frame = await createFrameWithNestedWindow(scope);
+  let nested = frame.contentWindow.nested();
+  let initialResult = nested.document.body.textContent;
+
+  // The nested iframe should not have been intercepted by the service
+  // worker.  The empty.html nested frame has "hello world" for its body.
+  assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`);
+
+  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+                'outer frame should be controlled');
+
+  assert_equals(nested.navigator.serviceWorker.controller, null,
+                'nested frame should not be controlled');
+
+  frame.remove();
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+   'final Client is not controlled by a service worker.');
+
+promise_test(async function(t) {
+  // Execute a test where the nested frame is an iframe without a src
+  // attribute.  This simple nested about:blank should still inherit the
+  // controller and be visible to clients.matchAll().
+  await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html');
+}, 'Simple about:blank is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+  // Execute a test where the nested frame is an iframe using a non-empty
+  // srcdoc containing only a tag pair so its textContent is still empty.
+  // This nested iframe should still inherit the controller and be visible
+  // to clients.matchAll().
+  await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html');
+}, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+  // Execute a test where the nested frame is dynamically added without a src
+  // attribute.  This simple nested about:blank should still inherit the
+  // controller and be visible to clients.matchAll().
+  await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html');
+}, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
new file mode 100644
index 0000000..016a52c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+  var script = 'resources/empty-worker.js';
+  var scope = 'resources/blank.html';
+  var registration;
+
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(function(registration) {
+        t.add_cleanup(function() {
+            return service_worker_unregister(t, scope);
+          });
+
+        var sw = registration.installing;
+
+        return new Promise(t.step_func(function(resolve) {
+          sw.onstatechange = t.step_func(function() {
+            if (sw.state === 'installed') {
+              assert_equals(registration.active, null,
+                            'installed event should be fired before activating service worker');
+              resolve();
+            }
+          });
+        }));
+      })
+    .catch(unreached_rejection(t));
+  }, 'installed event should be fired before activating service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html
new file mode 100644
index 0000000..29f97e3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Service Worker: Activation occurs after registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+    var scope = 'resources/blank.html';
+    var registration;
+
+    return service_worker_unregister_and_register(
+        t, 'resources/empty-worker.js', scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          assert_equals(
+              r.installing.state,
+              'installing',
+              'worker should be in the "installing" state upon registration');
+          return wait_for_state(t, r.installing, 'activated');
+        });
+}, 'activation occurs after registration');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activation.https.html b/third_party/web_platform_tests/service-workers/service-worker/activation.https.html
new file mode 100644
index 0000000..278454d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activation.https.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>service worker: activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Returns {registration, iframe}, where |registration| has an active and
+// waiting worker. The active worker controls |iframe| and has an inflight
+// message event that can be finished by calling
+// |registration.active.postMessage('go')|.
+function setup_activation_test(t, scope, worker_url) {
+  var registration;
+  var iframe;
+  return navigator.serviceWorker.getRegistration(scope)
+    .then(r => {
+        if (r)
+          return r.unregister();
+      })
+    .then(() => {
+        // Create an in-scope iframe. Do this prior to registration to avoid
+        // racing between an update triggered by navigation and the update()
+        // call below.
+        return with_iframe(scope);
+      })
+    .then(f => {
+        iframe = f;
+        // Create an active worker.
+        return navigator.serviceWorker.register(worker_url, { scope: scope });
+      })
+    .then(r => {
+        registration = r;
+        add_result_callback(() => registration.unregister());
+        return wait_for_state(t, r.installing, 'activated');
+      })
+    .then(() => {
+        // Check that the frame was claimed.
+        assert_not_equals(
+            iframe.contentWindow.navigator.serviceWorker.controller, null);
+        // Create an in-flight request.
+        registration.active.postMessage('wait');
+        // Now there is both a controllee and an in-flight request.
+        // Initiate an update.
+        return registration.update();
+      })
+    .then(() => {
+        // Wait for a waiting worker.
+        return wait_for_state(t, registration.installing, 'installed');
+      })
+    .then(() => {
+        return wait_for_activation_on_sample_scope(t, self);
+      })
+    .then(() => {
+        assert_not_equals(registration.waiting, null);
+        assert_not_equals(registration.active, null);
+        return Promise.resolve({registration: registration, iframe: iframe});
+      });
+}
+promise_test(t => {
+    var scope = 'resources/no-controllee';
+    var worker_url = 'resources/mint-new-worker.py';
+    var registration;
+    var iframe;
+    var new_worker;
+    return setup_activation_test(t, scope, worker_url)
+      .then(result => {
+          registration = result.registration;
+          iframe = result.iframe;
+          // Finish the in-flight request.
+          registration.active.postMessage('go');
+          return wait_for_activation_on_sample_scope(t, self);
+        })
+      .then(() => {
+          // The new worker is still waiting. Remove the frame and it should
+          // activate.
+          new_worker = registration.waiting;
+          assert_equals(new_worker.state, 'installed');
+          var reached_active = wait_for_state(t, new_worker, 'activating');
+          iframe.remove();
+          return reached_active;
+        })
+      .then(() => {
+          assert_equals(new_worker, registration.active);
+        });
+  }, 'loss of controllees triggers activation');
+promise_test(t => {
+    var scope = 'resources/no-request';
+    var worker_url = 'resources/mint-new-worker.py';
+    var registration;
+    var iframe;
+    var new_worker;
+    return setup_activation_test(t, scope, worker_url)
+      .then(result => {
+          registration = result.registration;
+          iframe = result.iframe;
+          // Remove the iframe.
+          iframe.remove();
+          return new Promise(resolve => setTimeout(resolve, 0));
+        })
+      .then(() => {
+          // Finish the request.
+          new_worker = registration.waiting;
+          var reached_active = wait_for_state(t, new_worker, 'activating');
+          registration.active.postMessage('go');
+          return reached_active;
+        })
+      .then(() => {
+          assert_equals(registration.active, new_worker);
+        });
+  }, 'finishing a request triggers activation');
+promise_test(t => {
+    var scope = 'resources/skip-waiting';
+    var worker_url = 'resources/mint-new-worker.py?skip-waiting';
+    var registration;
+    var iframe;
+    var new_worker;
+    return setup_activation_test(t, scope, worker_url)
+      .then(result => {
+          registration = result.registration;
+          iframe = result.iframe;
+          // Finish the request. The iframe does not need to be removed because
+          // skipWaiting() was called.
+          new_worker = registration.waiting;
+          var reached_active = wait_for_state(t, new_worker, 'activating');
+          registration.active.postMessage('go');
+          return reached_active;
+        })
+      .then(() => {
+          assert_equals(registration.active, new_worker);
+          // Remove the iframe.
+          iframe.remove();
+        });
+  }, 'skipWaiting bypasses no controllee requirement');
+
+// This test is not really about activation, but otherwise is very
+// similar to the other tests here.
+promise_test(t => {
+    var scope = 'resources/unregister';
+    var worker_url = 'resources/mint-new-worker.py';
+    var registration;
+    var iframe;
+    var new_worker;
+    return setup_activation_test(t, scope, worker_url)
+      .then(result => {
+          registration = result.registration;
+          iframe = result.iframe;
+          // Remove the iframe.
+          iframe.remove();
+          return registration.unregister();
+        })
+      .then(() => {
+          // The unregister operation should wait for the active worker to
+          // finish processing its events before clearing the registration.
+          new_worker = registration.waiting;
+          var reached_redundant = wait_for_state(t, new_worker, 'redundant');
+          registration.active.postMessage('go');
+          return reached_redundant;
+        })
+      .then(() => {
+          assert_equals(registration.installing, null);
+          assert_equals(registration.waiting, null);
+          assert_equals(registration.active, null);
+        });
+  }, 'finishing a request triggers unregister');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/active.https.html b/third_party/web_platform_tests/service-workers/service-worker/active.https.html
new file mode 100644
index 0000000..350a34b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/active.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.active</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "active" is set
+promise_test(async t => {
+
+  t.add_cleanup(async() => {
+    if (frame)
+      frame.remove();
+    if (registration)
+      await registration.unregister();
+  });
+
+  await service_worker_unregister(t, SCOPE);
+  const frame = await with_iframe(SCOPE);
+  const registration =
+      await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+  await wait_for_state(t, registration.installing, 'activating');
+  const container = frame.contentWindow.navigator.serviceWorker;
+  assert_equals(container.controller, null, 'controller');
+  assert_equals(registration.active.state, 'activating',
+                'registration.active');
+  assert_equals(registration.waiting, null, 'registration.waiting');
+  assert_equals(registration.installing, null, 'registration.installing');
+
+  // FIXME: Add a test for a frame created after installation.
+  // Should the existing frame ("frame") block activation?
+}, 'active is set');
+
+// Tests that the ServiceWorker objects returned from active attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+  const registration1 =
+      await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+  assert_equals(registration1.active, registration2.active,
+                'ServiceWorkerRegistration.active should return the same ' +
+                'object');
+  await registration1.unregister();
+}, 'The ServiceWorker objects returned from active attribute getter that ' +
+   'represent the same service worker are the same objects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html
new file mode 100644
index 0000000..52555ac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html
@@ -0,0 +1,136 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: claim() should affect other registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+  var frame;
+
+  var init_scope = 'resources/blank.html?affect-other';
+  var init_worker_url = 'resources/empty.js';
+  var init_registration;
+  var init_workers = [undefined, undefined];
+
+  var claim_scope = 'resources/blank.html?affect-other-registration';
+  var claim_worker_url = 'resources/claim-worker.js';
+  var claim_registration;
+  var claim_worker;
+
+  return Promise.resolve()
+    // Register the first service worker to init_scope.
+    .then(() => navigator.serviceWorker.register(init_worker_url + '?v1',
+                                                 {scope: init_scope}))
+    .then(r => {
+      init_registration = r;
+      init_workers[0] = r.installing;
+      return Promise.resolve()
+        .then(() => wait_for_state(t, init_workers[0], 'activated'))
+        .then(() => assert_array_equals([init_registration.active,
+                                         init_registration.waiting,
+                                         init_registration.installing],
+                                        [init_workers[0],
+                                         null,
+                                         null],
+                                        'Wrong workers.'));
+    })
+
+    // Create an iframe as the client of the first service worker of init_scope.
+    .then(() => with_iframe(claim_scope))
+    .then(f => frame = f)
+
+    // Check the controller.
+    .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+                  normalizeURL(init_scope)))
+    .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                             r.active,
+                             '.controller should belong to init_scope.'))
+
+    // Register the second service worker to init_scope.
+    .then(() => navigator.serviceWorker.register(init_worker_url + '?v2',
+                                                 {scope: init_scope}))
+    .then(r => {
+      assert_equals(r, init_registration, 'Should be the same registration');
+      init_workers[1] = r.installing;
+      return Promise.resolve()
+        .then(() => wait_for_state(t, init_workers[1], 'installed'))
+        .then(() => assert_array_equals([init_registration.active,
+                                         init_registration.waiting,
+                                         init_registration.installing],
+                                        [init_workers[0],
+                                         init_workers[1],
+                                         null],
+                                        'Wrong workers.'));
+    })
+
+    // Register a service worker to claim_scope.
+    .then(() => navigator.serviceWorker.register(claim_worker_url,
+                                                 {scope: claim_scope}))
+    .then(r => {
+      claim_registration = r;
+      claim_worker = r.installing;
+      return wait_for_state(t, claim_worker, 'activated')
+    })
+
+    // Let claim_worker claim the created iframe.
+    .then(function() {
+      var channel = new MessageChannel();
+      var saw_message = new Promise(function(resolve) {
+        channel.port1.onmessage = t.step_func(function(e) {
+          assert_equals(e.data, 'PASS',
+                        'Worker call to claim() should fulfill.');
+          resolve();
+        });
+      });
+
+      claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+      return saw_message;
+    })
+
+    // Check the controller.
+    .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+                  normalizeURL(claim_scope)))
+    .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                             r.active,
+                             '.controller should belong to claim_scope.'))
+
+    // Check the status of created registrations and service workers.
+    .then(() => wait_for_state(t, init_workers[1], 'activated'))
+    .then(() => {
+      assert_array_equals([claim_registration.active,
+                           claim_registration.waiting,
+                           claim_registration.installing],
+                          [claim_worker,
+                           null,
+                           null],
+                          'claim_worker should be the only worker.')
+
+      assert_array_equals([init_registration.active,
+                           init_registration.waiting,
+                           init_registration.installing],
+                          [init_workers[1],
+                           null,
+                           null],
+                          'The waiting sw should become the active worker.')
+
+      assert_array_equals([init_workers[0].state,
+                           init_workers[1].state,
+                           claim_worker.state],
+                          ['redundant',
+                           'activated',
+                           'activated'],
+                          'Wrong worker states.');
+    })
+
+    // Cleanup and finish testing.
+    .then(() => frame.remove())
+    .then(() => Promise.all([
+      init_registration.unregister(),
+      claim_registration.unregister()
+    ]))
+    .then(() => t.done());
+}, 'claim() should affect the originally controlling registration.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html
new file mode 100644
index 0000000..ae0082d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+async function tryFetch(fetchFunc, path) {
+  let response;
+  try {
+   response = await fetchFunc(path);
+  } catch (err) {
+    throw (`fetch() threw: ${err}`);
+  }
+
+  let responseText;
+  try {
+   responseText = await response.text();
+  } catch (err) {
+   throw (`text() threw: ${err}`);
+  }
+
+  return responseText;
+}
+
+promise_test(async function(t) {
+  const scope = 'resources/';
+  const script = 'resources/claim-worker.js';
+  const resource = 'simple.txt';
+
+  // Create the test frame.
+  const frame = await with_iframe('resources/blank.html');
+  t.add_cleanup(() => frame.remove());
+
+  // Check the controller and test with fetch.
+  assert_equals(frame.contentWindow.navigator.controller, undefined,
+                'Should have no controller.');
+  let response;
+  try {
+    response = await tryFetch(frame.contentWindow.fetch, resource);
+  } catch (err) {
+    assert_unreached(`uncontrolled fetch failed: ${err}`);
+  }
+  assert_equals(response, 'a simple text file\n',
+                'fetch() should not be intercepted.');
+
+  // Register a service worker.
+  const registration =
+    await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+
+  // Register a controllerchange event to wait until the controller is updated
+  // and check if the frame is controlled by a service worker.
+  const controllerChanged = new Promise((resolve) => {
+    frame.contentWindow.navigator.serviceWorker.oncontrollerchange = () => {
+      resolve(frame.contentWindow.navigator.serviceWorker.controller);
+    };
+  });
+
+  // Tell the service worker to claim the iframe.
+  const sawMessage = new Promise((resolve) => {
+    const channel = new MessageChannel();
+    channel.port1.onmessage = t.step_func((event) => {
+      resolve(event.data);
+    });
+    worker.postMessage({port: channel.port2}, [channel.port2]);
+  });
+  const data = await sawMessage;
+  assert_equals(data, 'PASS', 'Worker call to claim() should fulfill.');
+
+  // Check if the controller is updated after claim() and test with fetch.
+  const controller = await controllerChanged;
+  assert_true(controller instanceof frame.contentWindow.ServiceWorker,
+              'iframe should be controlled.');
+  try {
+    response = await tryFetch(frame.contentWindow.fetch, resource);
+  } catch (err) {
+    assert_unreached(`controlled fetch failed: ${err}`);
+  }
+  assert_equals(response, 'Intercepted!',
+                'fetch() should be intercepted.');
+}, 'fetch() should be intercepted after the client is claimed.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html
new file mode 100644
index 0000000..fd61d05
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client not using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+    var init_scope = 'resources/blank.html?not-using-init';
+    var claim_scope = 'resources/blank.html?not-using';
+    var init_worker_url = 'resources/empty.js';
+    var claim_worker_url = 'resources/claim-worker.js';
+    var claim_worker, claim_registration, frame1, frame2;
+    return service_worker_unregister_and_register(
+        t, init_worker_url, init_scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, init_scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return Promise.all(
+              [with_iframe(init_scope), with_iframe(claim_scope)]);
+        })
+      .then(function(frames) {
+          frame1 = frames[0];
+          frame2 = frames[1];
+          assert_equals(
+              frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+              normalizeURL(init_worker_url),
+              'Frame1 controller should not be null');
+          assert_equals(
+              frame2.contentWindow.navigator.serviceWorker.controller, null,
+              'Frame2 controller should be null');
+          return navigator.serviceWorker.register(claim_worker_url,
+                                                  {scope: claim_scope});
+        })
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, claim_scope);
+            });
+
+          claim_worker = registration.installing;
+          claim_registration = registration;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var saw_controllerchanged = new Promise(function(resolve) {
+              frame2.contentWindow.navigator.serviceWorker.oncontrollerchange =
+                  function() { resolve(); }
+            });
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_equals(e.data, 'PASS',
+                                'Worker call to claim() should fulfill.');
+                  resolve();
+                });
+            });
+          claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+          return Promise.all([saw_controllerchanged, saw_message]);
+        })
+      .then(function() {
+          assert_equals(
+              frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+              normalizeURL(init_worker_url),
+              'Frame1 should not be influenced');
+          assert_equals(
+              frame2.contentWindow.navigator.serviceWorker.controller.scriptURL,
+              normalizeURL(claim_worker_url),
+              'Frame2 should be controlled by the new registration');
+          frame1.remove();
+          frame2.remove();
+          return claim_registration.unregister();
+        });
+  }, 'Test claim client which is not using registration');
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?longer-matched';
+    var claim_scope = 'resources/blank.html?longer';
+    var claim_worker_url = 'resources/claim-worker.js';
+    var installing_worker_url = 'resources/empty-worker.js';
+    var frame, claim_worker;
+    return with_iframe(scope)
+      .then(function(f) {
+          frame = f;
+          return navigator.serviceWorker.register(
+              claim_worker_url, {scope: claim_scope});
+        })
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, claim_scope);
+            });
+
+          claim_worker = registration.installing;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return navigator.serviceWorker.register(
+              installing_worker_url, {scope: scope});
+        })
+      .then(function() {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_equals(e.data, 'PASS',
+                                'Worker call to claim() should fulfill.');
+                  resolve();
+                });
+            });
+          claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+          return saw_message;
+        })
+      .then(function() {
+          assert_equals(
+              frame.contentWindow.navigator.serviceWorker.controller, null,
+              'Frame should not be claimed when a longer-matched ' +
+              'registration exists');
+          frame.remove();
+        });
+  }, 'Test claim client when there\'s a longer-matched registration not ' +
+     'already used by the page');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
new file mode 100644
index 0000000..f5f4488
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+  var frame;
+  var resource = 'simple.txt';
+
+  var worker;
+  var scope = 'resources/';
+  var script = 'resources/claim-worker.js';
+
+  return Promise.resolve()
+    // Create the test iframe with a shared worker.
+    .then(() => with_iframe('resources/claim-shared-worker-fetch-iframe.html'))
+    .then(f => frame = f)
+
+    // Check the controller and test with fetch in the shared worker.
+    .then(() => assert_equals(frame.contentWindow.navigator.controller,
+                              undefined,
+                              'Should have no controller.'))
+    .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+    .then(response_text => assert_equals(response_text,
+                                         'a simple text file\n',
+                                         'fetch() should not be intercepted.'))
+    // Register a service worker.
+    .then(() => service_worker_unregister_and_register(t, script, scope))
+    .then(r => {
+        t.add_cleanup(() => service_worker_unregister(t, scope));
+
+        worker = r.installing;
+
+        return wait_for_state(t, worker, 'activated')
+      })
+    // Let the service worker claim the iframe and the shared worker.
+    .then(() => {
+      var channel = new MessageChannel();
+      var saw_message = new Promise(function(resolve) {
+        channel.port1.onmessage = t.step_func(function(e) {
+          assert_equals(e.data, 'PASS',
+                        'Worker call to claim() should fulfill.');
+          resolve();
+        });
+      });
+      worker.postMessage({port: channel.port2}, [channel.port2]);
+      return saw_message;
+    })
+
+    // Check the controller and test with fetch in the shared worker.
+    .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(scope))
+    .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                             r.active,
+                             'Test iframe should be claimed.'))
+    // TODO(horo): Check the SharedWorker's navigator.seviceWorker.controller.
+    .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+    .then(response_text =>
+          assert_equals(response_text,
+                        'Intercepted!',
+                        'fetch() in the shared worker should be intercepted.'))
+
+    // Cleanup this testcase.
+    .then(() => frame.remove());
+}, 'fetch() in SharedWorker should be intercepted after the client is claimed.')
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html
new file mode 100644
index 0000000..8a2a6ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var scope = 'resources/';
+    var frame_url = 'resources/blank.html?using-different-registration';
+    var url1 = 'resources/empty.js';
+    var url2 = 'resources/claim-worker.js';
+    var worker, sw_registration, frame;
+    return service_worker_unregister_and_register(t, url1, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(frame_url);
+        })
+      .then(function(f) {
+          frame = f;
+          return navigator.serviceWorker.register(url2, {scope: frame_url});
+        })
+      .then(function(registration) {
+          worker = registration.installing;
+          sw_registration = registration;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var saw_controllerchanged = new Promise(function(resolve) {
+              frame.contentWindow.navigator.serviceWorker.oncontrollerchange =
+                  function() { resolve(); }
+            });
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_equals(e.data, 'PASS',
+                                'Worker call to claim() should fulfill.');
+                  resolve();
+                });
+            });
+          worker.postMessage({port: channel.port2}, [channel.port2]);
+          return Promise.all([saw_controllerchanged, saw_message]);
+        })
+      .then(function() {
+          assert_equals(
+              frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+              normalizeURL(url2),
+              'Frame1 controller scriptURL should be changed to url2');
+          frame.remove();
+          return sw_registration.unregister();
+        });
+  }, 'Test worker claims client which is using another registration');
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?using-same-registration';
+    var url1 = 'resources/empty.js';
+    var url2 = 'resources/claim-worker.js';
+    var frame, worker;
+    return service_worker_unregister_and_register(t, url1, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          return navigator.serviceWorker.register(url2, {scope: scope});
+        })
+      .then(function(registration) {
+          worker = registration.installing;
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_equals(e.data, 'FAIL: exception: InvalidStateError',
+                                'Worker call to claim() should reject with ' +
+                                'InvalidStateError');
+                  resolve();
+                });
+            });
+          worker.postMessage({port: channel.port2}, [channel.port2]);
+          return saw_message;
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Test for the waiting worker claims a client which is using the the ' +
+     'same registration');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html
new file mode 100644
index 0000000..fd89cb9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: Claim() when update happens after redirect</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+var WORKER_URL = OTHER_BASE_URL + 'resources/update-claim-worker.py'
+var SCOPE_URL = OTHER_BASE_URL + 'resources/redirect.py'
+var OTHER_IFRAME_URL = OTHER_BASE_URL +
+                       'resources/claim-with-redirect-iframe.html';
+
+// Different origin from the registration
+var REDIRECT_TO_URL = BASE_URL +
+                      'resources/claim-with-redirect-iframe.html?redirected';
+
+var REGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?register=' +
+                          encodeURIComponent(WORKER_URL) + '&' +
+                          'scope=' + encodeURIComponent(SCOPE_URL);
+var REDIRECT_IFRAME_URL = SCOPE_URL + '?Redirect=' +
+                          encodeURIComponent(REDIRECT_TO_URL);
+var UPDATE_IFRAME_URL = OTHER_IFRAME_URL + '?update=' +
+                        encodeURIComponent(SCOPE_URL);
+var UNREGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?unregister=' +
+                            encodeURIComponent(SCOPE_URL);
+
+var waiting_resolver = undefined;
+
+addEventListener('message', e => {
+    if (waiting_resolver !== undefined) {
+      waiting_resolver(e.data);
+    }
+  });
+
+function assert_with_iframe(url, expected_message) {
+  return new Promise(resolve => {
+        waiting_resolver = resolve;
+        with_iframe(url);
+      })
+    .then(data => assert_equals(data.message, expected_message));
+}
+
+// This test checks behavior when browser got a redirect header from in-scope
+// page and navigated to out-of-scope page which has a different origin from any
+// registrations.
+promise_test(t => {
+  return assert_with_iframe(REGISTER_IFRAME_URL, 'registered')
+    .then(() => assert_with_iframe(REDIRECT_IFRAME_URL, 'redirected'))
+    .then(() => assert_with_iframe(UPDATE_IFRAME_URL, 'updated'))
+    .then(() => assert_with_iframe(UNREGISTER_IFRAME_URL, 'unregistered'));
+  }, 'Claim works after redirection to another origin');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html
new file mode 100644
index 0000000..7cb26c7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test((t) => {
+  return runTest(t, 'resources/claim-worker-fetch-iframe.html');
+}, 'fetch() in Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/claim-nested-worker-fetch-iframe.html');
+}, 'fetch() in nested Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/claim-blob-url-worker-fetch-iframe.html');
+}, 'fetch() in blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'fetch() in nested blob URL Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'fetch() in nested Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'fetch() in nested blob URL Worker created from a Worker should be intercepted after the client is claimed.');
+
+async function runTest(t, iframe_url) {
+  const resource = 'simple.txt';
+  const scope = 'resources/';
+  const script = 'resources/claim-worker.js';
+
+  // Create the test iframe with a dedicated worker.
+  const frame = await with_iframe(iframe_url);
+  t.add_cleanup(_ => frame.remove());
+
+  // Check the controller and test with fetch in the worker.
+  assert_equals(frame.contentWindow.navigator.controller,
+                undefined, 'Should have no controller.');
+  {
+    const response_text = await frame.contentWindow.fetch_in_worker(resource);
+    assert_equals(response_text, 'a simple text file\n',
+                  'fetch() should not be intercepted.');
+  }
+
+  // Register a service worker.
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Let the service worker claim the iframe and the worker.
+  const channel = new MessageChannel();
+  const saw_message = new Promise(function(resolve) {
+    channel.port1.onmessage = t.step_func(function(e) {
+      assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+      resolve();
+    });
+  });
+  reg.active.postMessage({port: channel.port2}, [channel.port2]);
+  await saw_message;
+
+  // Check the controller and test with fetch in the worker.
+  const reg2 =
+    await frame.contentWindow.navigator.serviceWorker.getRegistration(scope);
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                reg2.active, 'Test iframe should be claimed.');
+
+  {
+    // TODO(horo): Check the worker's navigator.seviceWorker.controller.
+    const response_text = await frame.contentWindow.fetch_in_worker(resource);
+    assert_equals(response_text, 'Intercepted!',
+                  'fetch() in the worker should be intercepted.');
+  }
+}
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html
new file mode 100644
index 0000000..b93b341
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Service Worker: Client.id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?client-id';
+var frame1, frame2;
+
+promise_test(function(t) {
+    return service_worker_unregister_and_register(
+        t, 'resources/client-id-worker.js', scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope + '#1'); })
+      .then(function(f) {
+          frame1 = f;
+          // To be sure Clients.matchAll() iterates in the same order.
+          f.focus();
+          return with_iframe(scope + '#2');
+        })
+      .then(function(f) {
+          frame2 = f;
+          var channel = new MessageChannel();
+
+          return new Promise(function(resolve, reject) {
+              channel.port1.onmessage = resolve;
+              channel.port1.onmessageerror = reject;
+              f.contentWindow.navigator.serviceWorker.controller.postMessage(
+                  {port:channel.port2}, [channel.port2]);
+            });
+        })
+      .then(on_message);
+  }, 'Client.id returns the client\'s ID.');
+
+function on_message(e) {
+  // The result of two sequential clients.matchAll() calls in the SW.
+  // 1st matchAll() results in e.data[0], e.data[1].
+  // 2nd matchAll() results in e.data[2], e.data[3].
+  assert_equals(e.data.length, 4);
+  // All should be string values.
+  assert_equals(typeof e.data[0], 'string');
+  assert_equals(typeof e.data[1], 'string');
+  assert_equals(typeof e.data[2], 'string');
+  assert_equals(typeof e.data[3], 'string');
+  // Different clients should have different ids.
+  assert_not_equals(e.data[0], e.data[1]);
+  assert_not_equals(e.data[2], e.data[3]);
+  // Same clients should have an identical id.
+  assert_equals(e.data[0], e.data[2]);
+  assert_equals(e.data[1], e.data[3]);
+  frame1.remove();
+  frame2.remove();
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html
new file mode 100644
index 0000000..f40a086
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Service Worker: WindowClient.navigate</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+  function wait_for_message(msg) {
+    return new Promise(function(resolve, reject) {
+      var get_message_data = function get_message_data(e) {
+        window.removeEventListener("message", get_message_data);
+        resolve(e.data);
+      }
+      window.addEventListener("message", get_message_data, false);
+    });
+  }
+
+  function run_test(controller, clientId, test) {
+    return new Promise(function(resolve, reject) {
+      var channel = new MessageChannel();
+      channel.port1.onmessage = function(e) {
+        resolve(e.data);
+      };
+      var message = {
+        port: channel.port2,
+        test: test,
+        clientId: clientId,
+      };
+      controller.postMessage(
+        message, [channel.port2]);
+    });
+  }
+
+  async function with_controlled_iframe_and_url(t, name, f) {
+    const SCRIPT = "resources/client-navigate-worker.js";
+    const SCOPE = "resources/client-navigate-frame.html";
+
+    // Register service worker and wait for it to become activated
+    const registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+    t.add_cleanup(() => registration.unregister());
+    const worker = registration.installing;
+    await wait_for_state(t, worker, 'activated');
+
+    // Create child iframe and make sure we register a listener for the message
+    // it sends before it's created
+    const client_id_promise = wait_for_message();
+    const iframe = await with_iframe(SCOPE);
+    t.add_cleanup(() => iframe.remove());
+    const { id } = await client_id_promise;
+
+    // Run the test in the service worker and fetch it
+    const { result, url } = await run_test(worker, id, name);
+    fetch_tests_from_worker(worker);
+    assert_equals(result, name);
+
+    // Hand over the iframe and URL from the service worker to the callback
+    await f(iframe, url);
+  }
+
+  promise_test(function(t) {
+    return with_controlled_iframe_and_url(t, 'test_client_navigate_success', async (iframe, url) => {
+      assert_equals(
+        url, new URL("resources/client-navigated-frame.html",
+                      location).toString());
+      assert_equals(
+        iframe.contentWindow.location.href,
+        new URL("resources/client-navigated-frame.html",
+                location).toString());
+    });
+  }, "Frame location should update on successful navigation");
+
+  promise_test(function(t) {
+    return with_controlled_iframe_and_url(t, 'test_client_navigate_redirect', async (iframe, url) => {
+      assert_equals(url, "");
+      assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+    });
+  }, "Frame location should not be accessible after redirect");
+
+  promise_test(function(t) {
+    return with_controlled_iframe_and_url(t, 'test_client_navigate_cross_origin', async (iframe, url) => {
+      assert_equals(url, "");
+      assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+    });
+  }, "Frame location should not be accessible after cross-origin navigation");
+
+  promise_test(function(t) {
+    return with_controlled_iframe_and_url(t, 'test_client_navigate_about_blank', async (iframe, url) => {
+      assert_equals(
+          iframe.contentWindow.location.href,
+          new URL("resources/client-navigate-frame.html",
+                  location).toString());
+      iframe.contentWindow.document.body.style = "background-color: green"
+    });
+  }, "Frame location should not update on failed about:blank navigation");
+
+  promise_test(function(t) {
+    return with_controlled_iframe_and_url(t, 'test_client_navigate_mixed_content', async (iframe, url) => {
+      assert_equals(
+          iframe.contentWindow.location.href,
+          new URL("resources/client-navigate-frame.html",
+                  location).toString());
+      iframe.contentWindow.document.body.style = "background-color: green"
+    });
+  }, "Frame location should not update on failed mixed-content navigation");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
new file mode 100644
index 0000000..97a2fcf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Service Worker: client.url of a worker created from a blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCRIPT = 'resources/client-url-of-blob-url-worker.js';
+const SCOPE = 'resources/client-url-of-blob-url-worker.html';
+
+promise_test(async (t) => {
+  const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  t.add_cleanup(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  const frame = await with_iframe(SCOPE);
+  t.add_cleanup(_ => frame.remove());
+  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                    null, 'frame should be controlled');
+
+  const response = await frame.contentWindow.createAndFetchFromBlobWorker();
+
+  assert_not_equals(response.result, 'one worker client should exist',
+                    'worker client should exist');
+  assert_equals(response.result, response.expected,
+                'client.url and worker location href should be the same');
+
+}, 'Client.url of a blob URL worker should be a blob URL.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html
new file mode 100644
index 0000000..63e3e51
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get with window and worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-get-client-types';
+var frame_url = scope + '-frame.html';
+var shared_worker_url = scope + '-shared-worker.js';
+var worker_url = scope + '-worker.js';
+var client_ids = [];
+var registration;
+var frame;
+promise_test(function(t) {
+    return service_worker_unregister_and_register(
+        t, 'resources/clients-get-worker.js', scope)
+      .then(function(r) {
+          registration = r;
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(frame_url);
+        })
+      .then(function(f) {
+          frame = f;
+          add_completion_callback(function() { frame.remove(); });
+          frame.focus();
+          return wait_for_clientId();
+        })
+      .then(function(client_id) {
+          client_ids.push(client_id);
+          return new Promise(function(resolve) {
+              var w = new SharedWorker(shared_worker_url);
+              w.port.onmessage = function(e) {
+                resolve(e.data.clientId);
+              };
+            });
+        })
+      .then(function(client_id) {
+          client_ids.push(client_id);
+          var channel = new MessageChannel();
+          var w = new Worker(worker_url);
+          w.postMessage({cmd:'GetClientId', port:channel.port2},
+              [channel.port2]);
+          return new Promise(function(resolve) {
+              channel.port1.onmessage = function(e) {
+                resolve(e.data.clientId);
+              };
+            });
+        })
+      .then(function(client_id) {
+          client_ids.push(client_id);
+          var channel = new MessageChannel();
+          frame.contentWindow.postMessage('StartWorker', '*', [channel.port2]);
+          return new Promise(function(resolve) {
+              channel.port1.onmessage = function(e) {
+                resolve(e.data.clientId);
+              };
+            });
+        })
+      .then(function(client_id) {
+          client_ids.push(client_id);
+          var saw_message = new Promise(function(resolve) {
+              navigator.serviceWorker.onmessage = resolve;
+            });
+          registration.active.postMessage({clientIds: client_ids});
+          return saw_message;
+        })
+      .then(function(e) {
+          assert_equals(e.data.length, expected.length);
+          // We use these assert_not_equals because assert_array_equals doesn't
+          // print the error description when passed an undefined value.
+          assert_not_equals(e.data[0], undefined,
+              'Window client should not be undefined');
+          assert_array_equals(e.data[0], expected[0], 'Window client');
+          assert_not_equals(e.data[1], undefined,
+              'Shared worker client should not be undefined');
+          assert_array_equals(e.data[1], expected[1], 'Shared worker client');
+          assert_not_equals(e.data[2], undefined,
+              'Worker(Started by main frame) client should not be undefined');
+          assert_array_equals(e.data[2], expected[2],
+              'Worker(Started by main frame) client');
+          assert_not_equals(e.data[3], undefined,
+              'Worker(Started by sub frame) client should not be undefined');
+          assert_array_equals(e.data[3], expected[3],
+              'Worker(Started by sub frame) client');
+        });
+  }, 'Test Clients.get() with window and worker clients');
+
+function wait_for_clientId() {
+  return new Promise(function(resolve) {
+      function get_client_id(e) {
+        window.removeEventListener('message', get_client_id);
+        resolve(e.data.clientId);
+      }
+      window.addEventListener('message', get_client_id, false);
+    });
+}
+
+var expected = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', true, normalizeURL(scope) + '-frame.html', 'window', 'nested'],
+    [undefined, undefined, normalizeURL(scope) + '-shared-worker.js', 'sharedworker', 'none'],
+    [undefined, undefined, normalizeURL(scope) + '-worker.js', 'worker', 'none'],
+    [undefined, undefined, normalizeURL(scope) + '-frame-worker.js', 'worker', 'none']
+];
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html
new file mode 100644
index 0000000..1e4acfb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get across origins</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+
+var scope = 'resources/clients-get-frame.html';
+var other_origin_iframe = host_info['HTTPS_REMOTE_ORIGIN'] + base_path() +
+                          'resources/clients-get-cross-origin-frame.html';
+// The ID of a client from the same origin as us.
+var my_origin_client_id;
+// This test asserts the behavior of the Client API in cases where the client
+// belongs to a foreign origin. It does this by creating an iframe with a
+// foreign origin which connects to a server worker in the current origin.
+promise_test(function(t) {
+    return service_worker_unregister_and_register(
+        t, 'resources/clients-get-worker.js', scope)
+      .then(function(registration) {
+          add_completion_callback(function() { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      // Create a same-origin client and use it to populate |my_origin_client_id|.
+      .then(function(frame1) {
+          add_completion_callback(function() { frame1.remove(); });
+          return new Promise(function(resolve, reject) {
+            function get_client_id(e) {
+              window.removeEventListener('message', get_client_id);
+              resolve(e.data.clientId);
+            }
+            window.addEventListener('message', get_client_id, false);
+          });
+        })
+      // Create a cross-origin client. We'll communicate with this client to
+      // test the cross-origin service worker's behavior.
+      .then(function(client_id) {
+          my_origin_client_id = client_id;
+          return with_iframe(other_origin_iframe);
+        })
+      // Post the 'getClientId' message to the cross-origin client. The client
+      // will then ask its service worker to look up |my_origin_client_id| via
+      // Clients#get. Since this client ID is for a different origin, we expect
+      // the client to not be found.
+      .then(function(frame2) {
+          add_completion_callback(function() { frame2.remove(); });
+
+          frame2.contentWindow.postMessage(
+            {clientId: my_origin_client_id, type: 'getClientId'},
+            host_info['HTTPS_REMOTE_ORIGIN']
+          );
+
+          return new Promise(function(resolve) {
+              window.addEventListener('message', function(e) {
+                  if (e.data && e.data.type === 'clientId') {
+                    resolve(e.data.value);
+                  }
+                });
+            });
+        })
+      .then(function(client_id) {
+          assert_equals(client_id, undefined, 'iframe client ID');
+        });
+  }, 'Test Clients.get() cross origin');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html
new file mode 100644
index 0000000..3419cf1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test clients.get(resultingClientId)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = "resources/";
+let worker;
+
+// Setup. Keep this as the first promise_test.
+promise_test(async (t) => {
+  const registration = await service_worker_unregister_and_register(
+      t, 'resources/get-resultingClientId-worker.js',
+      scope);
+  worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Sends |command| to the worker and returns a promise that resolves to its
+// response. There should only be one inflight command at a time.
+async function sendCommand(command) {
+  const saw_message = new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+  });
+  worker.postMessage(command);
+  return saw_message;
+}
+
+// Wrapper for 'startTest' command. Tells the worker a test is starting,
+// so it resets state and keeps itself alive until 'finishTest'.
+async function startTest(t) {
+  const result = await sendCommand({command: 'startTest'});
+  assert_equals(result, 'ok', 'startTest');
+
+  t.add_cleanup(async () => {
+    return finishTest();
+  });
+}
+
+// Wrapper for 'finishTest' command.
+async function finishTest() {
+  const result = await sendCommand({command: 'finishTest'});
+  assert_equals(result, 'ok', 'finishTest');
+}
+
+// Wrapper for 'getResultingClient' command. Tells the worker to return
+// clients.get(event.resultingClientId) for the navigation that occurs
+// during this test.
+//
+// The return value describes how clients.get() settled. It also includes
+// |queriedId| which is the id passed to clients.get() (the resultingClientId
+// in this case).
+//
+// Example value:
+// {
+//   queriedId: 'abc',
+//   promiseState: fulfilled,
+//   promiseValue: client,
+//   client: {
+//     id: 'abc',
+//     url: '//example.com/client'
+//   }
+// }
+async function getResultingClient() {
+  return sendCommand({command: 'getResultingClient'});
+}
+
+// Wrapper for 'getClient' command. Tells the worker to return
+// clients.get(|id|). The return value is as in the getResultingClient()
+// documentation.
+async function getClient(id) {
+  return sendCommand({command: 'getClient', id: id});
+}
+
+// Navigates to |url|. Returns the result of clients.get() on the
+// resultingClientId.
+async function navigateAndGetResultingClient(t, url) {
+  const resultPromise = getResultingClient();
+  const frame = await with_iframe(url);
+  t.add_cleanup(() => {
+    frame.remove();
+  });
+  const result = await resultPromise;
+  const resultingClientId = result.queriedId;
+
+  // First test clients.get(event.resultingClientId) inside the fetch event. The
+  // behavior of this is subtle due to the use of iframes and about:blank
+  // replacement. The spec probably requires that it resolve to the original
+  // about:blank client, and that later that client should be discarded after
+  // load if the load was to another origin. Implementations might differ. For
+  // now, this test just asserts that the promise resolves. See
+  // https://github.com/w3c/ServiceWorker/issues/1385.
+  assert_equals(result.promiseState, 'fulfilled',
+                'get(event.resultingClientId) in the fetch event should fulfill');
+
+  // Test clients.get() on the previous resultingClientId again. By this
+  // time the load finished, so it's more straightforward how this promise
+  // should settle. Return the result of this promise.
+  return await getClient(resultingClientId);
+}
+
+// Test get(resultingClientId) in the basic same-origin case.
+promise_test(async (t) => {
+  await startTest(t);
+
+  const url = new URL('resources/empty.html', window.location);
+  const result = await navigateAndGetResultingClient(t, url);
+  assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+  assert_equals(result.promiseValue, 'client', 'promiseValue');
+  assert_equals(result.client.url, url.href, 'client.url',);
+  assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for same-origin document');
+
+// Test get(resultingClientId) when the response redirects to another origin.
+promise_test(async (t) => {
+  await startTest(t);
+
+  // Navigate to a URL that redirects to another origin.
+  const base_url = new URL('.', window.location);
+  const host_info = get_host_info();
+  const other_origin_url = new URL(base_url.pathname + 'resources/empty.html',
+                                   host_info['HTTPS_REMOTE_ORIGIN']);
+  const url = new URL('resources/empty.html', window.location);
+  const pipe = `status(302)|header(Location, ${other_origin_url})`;
+  url.searchParams.set('pipe', pipe);
+
+  // The original reserved client should have been discarded on cross-origin
+  // redirect.
+  const result = await navigateAndGetResultingClient(t, url);
+  assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+  assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) on cross-origin redirect');
+
+// Test get(resultingClientId) when the document is sandboxed to a unique
+// origin using a CSP HTTP response header.
+promise_test(async (t) => {
+  await startTest(t);
+
+  // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+  const url = new URL('resources/empty.html', window.location);
+  const pipe = 'header(Content-Security-Policy, sandbox)';
+  url.searchParams.set('pipe', pipe);
+
+  // The original reserved client should have been discarded upon loading
+  // the sandboxed document.
+  const result = await navigateAndGetResultingClient(t, url);
+  assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+  assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) for document sandboxed by CSP header');
+
+// Test get(resultingClientId) when the document is sandboxed with
+// allow-same-origin.
+promise_test(async (t) => {
+  await startTest(t);
+
+  // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+  const url = new URL('resources/empty.html', window.location);
+  const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)';
+  url.searchParams.set('pipe', pipe);
+
+  // The client should be the original reserved client, as it's same-origin.
+  const result = await navigateAndGetResultingClient(t, url);
+  assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+  assert_equals(result.promiseValue, 'client', 'promiseValue');
+  assert_equals(result.client.url, url.href, 'client.url',);
+  assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin');
+
+// Cleanup. Keep this as the last promise_test.
+promise_test(async (t) => {
+  return service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html
new file mode 100644
index 0000000..4cfbf59
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_clientId() {
+  return new Promise(function(resolve, reject) {
+    window.onmessage = e => {
+      resolve(e.data.clientId);
+    };
+  });
+}
+
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/clients-get-frame.html';
+  const client_ids = [];
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-worker.js', scope);
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Prepare for test cases.
+  // Case 1: frame1 which is focused.
+  const frame1 = await with_iframe(scope + '#1');
+  t.add_cleanup(() => frame1.remove());
+  frame1.focus();
+  client_ids.push(await wait_for_clientId());
+  // Case 2: frame2 which is not focused.
+  const frame2 = await with_iframe(scope + '#2');
+  t.add_cleanup(() =>  frame2.remove());
+  client_ids.push(await wait_for_clientId());
+  // Case 3: invalid id.
+  client_ids.push('invalid-id');
+
+  // Call clients.get() for each id on the service worker.
+  const message_event = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = resolve;
+    registration.active.postMessage({clientIds: client_ids});
+  });
+
+  const expected = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'],
+    ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'],
+    undefined
+  ];
+  assert_equals(message_event.data.length, 3);
+  assert_array_equals(message_event.data[0], expected[0]);
+  assert_array_equals(message_event.data[1], expected[1]);
+  assert_equals(message_event.data[2], expected[2]);
+}, 'Test Clients.get()');
+
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/simple.html';
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope)
+  t.add_cleanup(() =>  registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const worker = registration.active;
+
+  // Load frame within the scope.
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+  frame.focus();
+
+  // Get resulting client id.
+  const resultingClientId = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getResultingClientId') {
+        resolve(e.data.resultingClientId);
+      }
+    };
+    worker.postMessage({msg: 'getResultingClientId'});
+  });
+
+  // Query service worker for clients.get(resultingClientId).
+  const isResultingClientUndefined = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getIsResultingClientUndefined') {
+        resolve(e.data.isResultingClientUndefined);
+      }
+    };
+    worker.postMessage({msg: 'getIsResultingClientUndefined',
+                        resultingClientId});
+  });
+
+  assert_false(
+    isResultingClientUndefined,
+    'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/simple.html?fail';
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope);
+  t.add_cleanup(() =>  registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Load frame, and destroy it while loading.
+  const worker = registration.active;
+  let frame = document.createElement('iframe');
+  frame.src = scope;
+  t.add_cleanup(() => {
+    if (frame) {
+      frame.remove();
+    }
+  });
+
+  await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      // The service worker posts a message to remove the iframe during fetch
+      // event.
+      if (e.data.msg == 'destroyResultingClient') {
+        frame.remove();
+        frame = null;
+        worker.postMessage({msg: 'resultingClientDestroyed'});
+        resolve();
+      }
+    };
+    document.body.appendChild(frame);
+  });
+
+  resultingDestroyedClientId = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      // The worker sends a message back when it receives the message
+      // 'resultingClientDestroyed' with the resultingClientId.
+      if (e.data.msg == 'resultingClientDestroyedAck') {
+        assert_equals(frame, null, 'Frame should be destroyed at this point.');
+        resolve(e.data.resultingDestroyedClientId);
+      }
+    };
+  });
+
+  // Query service worker for clients.get(resultingDestroyedClientId).
+  const isResultingClientUndefined = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getIsResultingClientUndefined') {
+        resolve(e.data.isResultingClientUndefined);
+      }
+    };
+    worker.postMessage({msg: 'getIsResultingClientUndefined',
+                        resultingClientId: resultingDestroyedClientId });
+  });
+
+  assert_true(
+    isResultingClientUndefined,
+    'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
new file mode 100644
index 0000000..c29bac8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with a blob URL worker client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const SCRIPT = 'resources/clients-matchall-worker.js';
+
+promise_test(async (t) => {
+  const scope = 'resources/clients-matchall-blob-url-worker.html';
+
+  const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+  t.add_cleanup(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  const frame = await with_iframe(scope);
+  t.add_cleanup(_ => frame.remove());
+
+  {
+    const message = await frame.contentWindow.waitForWorker();
+    assert_equals(message.data, 'Worker is ready.',
+                  'Worker should reply to the message.');
+  }
+
+  const channel = new MessageChannel();
+  const message = await new Promise(resolve => {
+    channel.port1.onmessage = resolve;
+    frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+      {port: channel.port2, options: {type: 'worker'}}, [channel.port2]);
+  });
+
+  checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with a blob URL worker client.');
+
+promise_test(async (t) => {
+  const scope = 'resources/blank.html';
+
+  const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+  t.add_cleanup(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  const workerScript = `
+    self.onmessage = (e) => {
+      self.postMessage("Worker is ready.");
+    };
+  `;
+  const blob = new Blob([workerScript], { type: 'text/javascript' });
+  const blobUrl = URL.createObjectURL(blob);
+  const worker = new Worker(blobUrl);
+
+  {
+    const message = await new Promise(resolve => {
+      worker.onmessage = resolve;
+      worker.postMessage("Ping to worker.");
+    });
+    assert_equals(message.data, 'Worker is ready.',
+                  'Worker should reply to the message.');
+  }
+
+  const channel = new MessageChannel();
+  const message = await new Promise(resolve => {
+    channel.port1.onmessage = resolve;
+    reg.active.postMessage(
+      {port: channel.port2,
+       options: {includeUncontrolled: true, type: 'worker'}},
+      [channel.port2]
+    );
+  });
+
+  checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with an uncontrolled blob URL worker client.');
+
+function checkMessageEvent(e) {
+  assert_equals(e.data.length, 1);
+
+  const workerClient = e.data[0];
+  assert_equals(workerClient[0], undefined); // visibilityState
+  assert_equals(workerClient[1], undefined); // focused
+  assert_true(workerClient[2].includes('blob:')); // url
+  assert_equals(workerClient[3], 'worker'); // type
+  assert_equals(workerClient[4], 'none'); // frameType
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html
new file mode 100644
index 0000000..54f182b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with various clientTypes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/clients-matchall-client-types';
+const iframe_url = scope + '-iframe.html';
+const shared_worker_url = scope + '-shared-worker.js';
+const dedicated_worker_url = scope + '-dedicated-worker.js';
+
+/* visibilityState, focused, url, type, frameType */
+const expected_only_window = [
+    ['visible', true, new URL(iframe_url, location).href, 'window', 'nested']
+];
+const expected_only_shared_worker = [
+    [undefined, undefined, new URL(shared_worker_url, location).href, 'sharedworker', 'none']
+];
+const expected_only_dedicated_worker = [
+    [undefined, undefined, new URL(dedicated_worker_url, location).href, 'worker', 'none']
+];
+
+// These are explicitly sorted by URL in the service worker script.
+const expected_all_clients = [
+    expected_only_dedicated_worker[0],
+    expected_only_window[0],
+    expected_only_shared_worker[0],
+];
+
+async function test_matchall(frame, expected, query_options) {
+  // Make sure the frame gets focus.
+  frame.focus();
+  const data = await new Promise(resolve => {
+    const channel = new MessageChannel();
+    channel.port1.onmessage = e => resolve(e.data);
+    frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+        {port:channel.port2, options:query_options},
+        [channel.port2]);
+  });
+
+  if (typeof data === 'string') {
+    throw new Error(data);
+  }
+
+  assert_equals(data.length, expected.length, 'result count');
+
+  for (let i = 0; i < data.length; ++i) {
+    assert_array_equals(data[i], expected[i]);
+  }
+}
+
+promise_test(async t => {
+  const registration = await service_worker_unregister_and_register(
+      t, 'resources/clients-matchall-worker.js', scope);
+  t.add_cleanup(_ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(iframe_url);
+  t.add_cleanup(_ => frame.remove());
+  await test_matchall(frame, expected_only_window, {});
+  await test_matchall(frame, expected_only_window, {type:'window'});
+}, 'Verify matchAll() with window client type');
+
+promise_test(async t => {
+  const registration = await service_worker_unregister_and_register(
+      t, 'resources/clients-matchall-worker.js', scope);
+  t.add_cleanup(_ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(iframe_url);
+  t.add_cleanup(_ => frame.remove());
+
+  // Set up worker clients.
+  const shared_worker = await new Promise((resolve, reject) => {
+    const w = new SharedWorker(shared_worker_url);
+    w.onerror = e => reject(e.message);
+    w.port.onmessage = _ => resolve(w);
+  });
+  const dedicated_worker = await new Promise((resolve, reject) => {
+    const w = new Worker(dedicated_worker_url);
+    w.onerror = e => reject(e.message);
+    w.onmessage = _ => resolve(w);
+    w.postMessage('Start');
+  });
+
+  await test_matchall(frame, expected_only_window, {});
+  await test_matchall(frame, expected_only_window, {type:'window'});
+  await test_matchall(frame, expected_only_shared_worker,
+                      {type:'sharedworker'});
+  await test_matchall(frame, expected_only_dedicated_worker, {type:'worker'});
+  await test_matchall(frame, expected_all_clients, {type:'all'});
+}, 'Verify matchAll() with {window, sharedworker, worker} client types');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
new file mode 100644
index 0000000..a61c8af
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with exact controller</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/blank.html?clients-matchAll';
+let frames = [];
+
+function checkWorkerClients(worker, expected) {
+  return new Promise((resolve, reject) => {
+    let channel = new MessageChannel();
+    channel.port1.onmessage = evt => {
+      try {
+        assert_equals(evt.data.length, expected.length);
+        for (let i = 0; i < expected.length; ++i) {
+          assert_array_equals(evt.data[i], expected[i]);
+        }
+        resolve();
+      } catch (e) {
+        reject(e);
+      }
+    };
+
+    worker.postMessage({port:channel.port2}, [channel.port2]);
+  });
+}
+
+let expected = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+    ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+promise_test(t => {
+    let script = 'resources/clients-matchall-worker.js';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          t.add_cleanup(() => service_worker_unregister(t, scope));
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(_ => with_iframe(scope + '#1') )
+      .then(frame1 => {
+          frames.push(frame1);
+          frame1.focus();
+          return with_iframe(scope + '#2');
+        })
+      .then(frame2 => {
+          frames.push(frame2);
+          return navigator.serviceWorker.register(script + '?updated', { scope: scope });
+        })
+      .then(registration => {
+          return wait_for_state(t, registration.installing, 'installed')
+            .then(_ => registration);
+        })
+      .then(registration => {
+          return Promise.all([
+            checkWorkerClients(registration.waiting, []),
+            checkWorkerClients(registration.active, expected),
+          ]);
+        })
+      .then(_ => {
+          frames.forEach(f => f.remove() );
+        });
+}, 'Test Clients.matchAll() with exact controller');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html
new file mode 100644
index 0000000..479c28a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-frame-freeze.html';
+var windows = [];
+var expected_window_1 =
+    {visibilityState: 'visible', focused: false, lifecycleState: "frozen", url: new URL(scope + '#1', location).toString(), type: 'window', frameType: 'top-level'};
+var expected_window_2 =
+    {visibilityState: 'visible', focused: false, lifecycleState: "active", url: new URL(scope + '#2', location).toString(), type: 'window', frameType: 'top-level'};
+function with_window(url, name) {
+  return new Promise(function(resolve) {
+    var child = window.open(url, name);
+    window.onmessage = () => {resolve(child)};
+  });
+}
+
+promise_test(function(t) {
+    return service_worker_unregister_and_register(
+        t, 'resources/clients-matchall-worker.js', scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_window(scope + '#1', 'Child 1'); })
+      .then(function(window1) {
+          windows.push(window1);
+          return with_window(scope + '#2', 'Child 2');
+        })
+      .then(function(window2) {
+          windows.push(window2);
+          return new Promise(function(resolve) {
+              window.onmessage = resolve;
+              windows[0].postMessage('freeze');
+            });
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+
+          return new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+              windows[1].navigator.serviceWorker.controller.postMessage(
+                  {port:channel.port2, includeLifecycleState: true}, [channel.port2]);
+            });
+        })
+      .then(function(e) {
+          assert_equals(e.data.length, 2);
+          // No specific order is required, so support inversion.
+          if (e.data[0][3] == new URL(scope + '#2', location)) {
+            assert_object_equals(e.data[0], expected_window_2);
+            assert_object_equals(e.data[1], expected_window_1);
+          } else {
+            assert_object_equals(e.data[0], expected_window_1);
+            assert_object_equals(e.data[1], expected_window_2);
+          }
+      });
+}, 'Test Clients.matchAll()');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
new file mode 100644
index 0000000..9f34e57
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with includeUncontrolled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function test_matchall(service_worker, expected, query_options) {
+  expected.sort((a, b) => a[2] > b[2] ? 1 : -1);
+  return new Promise((resolve, reject) => {
+    const channel = new MessageChannel();
+    channel.port1.onmessage = e => {
+      const data = e.data.filter(info => {
+        return info[2].indexOf('clients-matchall') > -1;
+      });
+      data.sort((a, b) => a[2] > b[2] ? 1 : -1);
+      assert_equals(data.length, expected.length);
+      for (let i = 0; i < data.length; i++)
+        assert_array_equals(data[i], expected[i]);
+      resolve();
+    };
+    service_worker.postMessage({port:channel.port2, options:query_options},
+                               [channel.port2]);
+  });
+}
+
+// Run clients.matchAll without and with includeUncontrolled=true.
+// (We want to run the two tests sequentially in the same promise_test
+// so that we can use the same set of iframes without intefering each other.
+promise_test(async t => {
+  // |base_url| is out-of-scope.
+  const base_url = 'resources/blank.html?clients-matchall';
+  const scope = base_url + '-includeUncontrolled';
+
+  const registration =
+      await service_worker_unregister_and_register(
+          t, 'resources/clients-matchall-worker.js', scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+  const service_worker = registration.installing;
+  await wait_for_state(t, service_worker, 'activated');
+
+  // Creates 3 iframes, 2 for in-scope and 1 for out-of-scope.
+  let frames = [];
+  frames.push(await with_iframe(base_url));
+  frames.push(await with_iframe(scope + '#1'));
+  frames.push(await with_iframe(scope + '#2'));
+
+  // Make sure we have focus for '#2' frame and its parent window.
+  frames[2].focus();
+  frames[2].contentWindow.focus();
+
+  const expected_without_include_uncontrolled = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+    ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested']
+  ];
+  const expected_with_include_uncontrolled = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', true, location.href, 'window', 'top-level'],
+    ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+    ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'],
+    ['visible', false, new URL(base_url, location).toString(), 'window', 'nested']
+  ];
+
+  await test_matchall(service_worker, expected_without_include_uncontrolled);
+  await test_matchall(service_worker, expected_with_include_uncontrolled,
+                      { includeUncontrolled: true });
+}, 'Verify matchAll() with windows respect includeUncontrolled');
+
+// TODO: Add tests for clients.matchAll for dedicated workers.
+
+async function create_shared_worker(script_url) {
+  const shared_worker = new SharedWorker(script_url);
+  const msgEvent = await new Promise(r => shared_worker.port.onmessage = r);
+  assert_equals(msgEvent.data, 'started');
+  return shared_worker;
+}
+
+// Run clients.matchAll for shared workers without and with
+// includeUncontrolled=true.
+promise_test(async t => {
+  const script_url = 'resources/clients-matchall-client-types-shared-worker.js';
+  const uncontrolled_script_url =
+      new URL(script_url + '?uncontrolled', location).toString();
+  const controlled_script_url =
+      new URL(script_url + '?controlled', location).toString();
+
+  // Start a shared worker that is not controlled by a service worker.
+  const uncontrolled_shared_worker =
+      await create_shared_worker(uncontrolled_script_url);
+
+  // Register a service worker.
+  const registration =
+      await service_worker_unregister_and_register(
+          t, 'resources/clients-matchall-worker.js', script_url);
+  t.add_cleanup(() => service_worker_unregister(t, script_url));
+  const service_worker = registration.installing;
+  await wait_for_state(t, service_worker, 'activated');
+
+  // Start another shared worker controlled by the service worker.
+  await create_shared_worker(controlled_script_url);
+
+  const expected_without_include_uncontrolled = [
+    // visibilityState, focused, url, type, frameType
+    [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+  ];
+  const expected_with_include_uncontrolled = [
+    // visibilityState, focused, url, type, frameType
+    [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+    [undefined, undefined, uncontrolled_script_url, 'sharedworker', 'none'],
+  ];
+
+  await test_matchall(service_worker, expected_without_include_uncontrolled,
+                      { type: 'sharedworker' });
+  await test_matchall(service_worker, expected_with_include_uncontrolled,
+                      { includeUncontrolled: true, type: 'sharedworker' });
+}, 'Verify matchAll() with shared workers respect includeUncontrolled');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
new file mode 100644
index 0000000..8705f85
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll on script evaluation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var script = 'resources/clients-matchall-on-evaluation-worker.js';
+    var scope = 'resources/blank.html?clients-matchAll-on-evaluation';
+
+    var saw_message = new Promise(function(resolve) {
+        navigator.serviceWorker.onmessage = function(e) {
+            assert_equals(e.data, 'matched');
+            resolve();
+          };
+      });
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          add_completion_callback(function() { registration.unregister(); });
+          return saw_message;
+        });
+  }, 'Test Clients.matchAll() on script evaluation');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html
new file mode 100644
index 0000000..ec650f2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll ordering</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Utility function for URLs this test will open.
+function makeURL(name, num, type) {
+  let u = new URL('resources/empty.html', location);
+  u.searchParams.set('name', name);
+  if (num !== undefined) {
+    u.searchParams.set('q', num);
+  }
+  if (type === 'nested') {
+    u.searchParams.set('nested', true);
+  }
+  return u.href;
+}
+
+// Non-test URLs that will be open during each test.  The harness URLs
+// are from the WPT harness.  The "extra" URL is a final window opened
+// by the test.
+const EXTRA_URL = makeURL('extra');
+const TEST_HARNESS_URL = location.href;
+const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href;
+
+// Utility function to open an iframe in the target parent window.  We
+// can't just use with_iframe() because it does not support a configurable
+// parent window.
+function openFrame(parentWindow, url) {
+  return new Promise(resolve => {
+    let frame = parentWindow.document.createElement('iframe');
+    frame.src = url;
+    parentWindow.document.body.appendChild(frame);
+
+    frame.contentWindow.addEventListener('load', evt => {
+      resolve(frame);
+    }, { once: true });
+  });
+}
+
+// Utility function to open a window and wait for it to load.  The
+// window may optionally have a nested iframe as well.  Returns
+// a result like `{ top: <frame ref> nested: <nested frame ref if present> }`.
+function openFrameConfig(opts) {
+  let url = new URL(opts.url, location.href);
+  return openFrame(window, url.href).then(top => {
+    if (!opts.withNested) {
+      return { top: top };
+    }
+
+    url.searchParams.set('nested', true);
+    return openFrame(top.contentWindow, url.href).then(nested => {
+      return { top: top, nested: nested };
+    });
+  });
+}
+
+// Utility function that takes a list of configurations and opens the
+// corresponding windows in sequence.  An array of results is returned.
+function openFrameConfigList(optList) {
+  let resultList = [];
+  function openNextWindow(optList, nextWindow) {
+    if (nextWindow >= optList.length) {
+      return resultList;
+    }
+    return openFrameConfig(optList[nextWindow]).then(result => {
+      resultList.push(result);
+      return openNextWindow(optList, nextWindow + 1);
+    });
+  }
+  return openNextWindow(optList, 0);
+}
+
+// Utility function that focuses the given entry in window result list.
+function executeFocus(frameResultList, opts) {
+  return new Promise(resolve => {
+    let w = frameResultList[opts.index][opts.type];
+    let target = w.contentWindow ? w.contentWindow : w;
+    target.addEventListener('focus', evt => {
+      resolve();
+    }, { once: true });
+    target.focus();
+  });
+}
+
+// Utility function that performs a list of focus commands in sequence
+// based on the window result list.
+function executeFocusList(frameResultList, optList) {
+  function executeNextCommand(frameResultList, optList, nextCommand) {
+    if (nextCommand >= optList.length) {
+      return;
+    }
+    return executeFocus(frameResultList, optList[nextCommand]).then(_ => {
+      return executeNextCommand(frameResultList, optList, nextCommand + 1);
+    });
+  }
+  return executeNextCommand(frameResultList, optList, 0);
+}
+
+// Perform a `clients.matchAll()` in the service worker with the given
+// options dictionary.
+function doMatchAll(worker, options) {
+  return new Promise(resolve => {
+    let channel = new MessageChannel();
+    channel.port1.onmessage = evt => {
+      resolve(evt.data);
+    };
+    worker.postMessage({ port: channel.port2, options: options, disableSort: true },
+                       [channel.port2]);
+  });
+}
+
+// Function that performs a single test case.  It takes a configuration object
+// describing the windows to open, how to focus them, the matchAll options,
+// and the resulting expectations.  See the test cases for examples of how to
+// use this.
+function matchAllOrderTest(t, opts) {
+  let script = 'resources/clients-matchall-worker.js';
+  let worker;
+  let frameResultList;
+  let extraWindowResult;
+  return service_worker_unregister_and_register(t, script, opts.scope).then(swr => {
+    t.add_cleanup(() => service_worker_unregister(t, opts.scope));
+
+    worker = swr.installing;
+    return wait_for_state(t, worker, 'activated');
+  }).then(_ => {
+    return openFrameConfigList(opts.frameConfigList);
+  }).then(results => {
+    frameResultList = results;
+    return openFrameConfig({ url: EXTRA_URL });
+  }).then(result => {
+    extraWindowResult = result;
+    return executeFocusList(frameResultList, opts.focusConfigList);
+  }).then(_ => {
+    return doMatchAll(worker, opts.matchAllOptions);
+  }).then(data => {
+    assert_equals(data.length, opts.expected.length);
+    for (let i = 0; i < data.length; ++i) {
+      assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i);
+    }
+  }).then(_ => {
+    frameResultList.forEach(result => result.top.remove());
+    extraWindowResult.top.remove();
+  }).catch(e => {
+    if (frameResultList) {
+      frameResultList.forEach(result => result.top.remove());
+    }
+    if (extraWindowResult) {
+      extraWindowResult.top.remove();
+    }
+    throw(e);
+  });
+}
+
+// ----------
+// Test cases
+// ----------
+
+promise_test(t => {
+  let name = 'no-focus-controlled-windows';
+  let opts = {
+    scope: makeURL(name),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      // no focus commands
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: false
+    },
+
+    expected: [
+      makeURL(name, 0),
+      makeURL(name, 1),
+      makeURL(name, 2),
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused controlled windows in creation order.');
+
+promise_test(t => {
+  let name = 'focus-controlled-windows-1';
+  let opts = {
+    scope: makeURL(name),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      { index: 0, type: 'top' },
+      { index: 1, type: 'top' },
+      { index: 2, type: 'top' },
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: false
+    },
+
+    expected: [
+      makeURL(name, 2),
+      makeURL(name, 1),
+      makeURL(name, 0),
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order.  Case 1.');
+
+promise_test(t => {
+  let name = 'focus-controlled-windows-2';
+  let opts = {
+    scope: makeURL(name),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      { index: 2, type: 'top' },
+      { index: 1, type: 'top' },
+      { index: 0, type: 'top' },
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: false
+    },
+
+    expected: [
+      makeURL(name, 0),
+      makeURL(name, 1),
+      makeURL(name, 2),
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order.  Case 2.');
+
+promise_test(t => {
+  let name = 'no-focus-uncontrolled-windows';
+  let opts = {
+    scope: makeURL(name + '-outofscope'),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      // no focus commands
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: true
+    },
+
+    expected: [
+      // The harness windows have been focused, so appear first
+      TEST_HARNESS_URL,
+      TOP_HARNESS_URL,
+
+      // Test frames have not been focused, so appear in creation order
+      makeURL(name, 0),
+      makeURL(name, 1),
+      makeURL(name, 2),
+      EXTRA_URL,
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.');
+
+promise_test(t => {
+  let name = 'focus-uncontrolled-windows-1';
+  let opts = {
+    scope: makeURL(name + '-outofscope'),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      { index: 0, type: 'top' },
+      { index: 1, type: 'top' },
+      { index: 2, type: 'top' },
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: true
+    },
+
+    expected: [
+      // The test harness window is a parent of all test frames.  It will
+      // always have the same focus time or later as its frames.  So it
+      // appears first.
+      TEST_HARNESS_URL,
+
+      makeURL(name, 2),
+      makeURL(name, 1),
+      makeURL(name, 0),
+
+      // The overall harness has been focused
+      TOP_HARNESS_URL,
+
+      // The extra frame was never focused
+      EXTRA_URL,
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order.  Case 1.');
+
+promise_test(t => {
+  let name = 'focus-uncontrolled-windows-2';
+  let opts = {
+    scope: makeURL(name + '-outofscope'),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: false },
+      { url: makeURL(name, 1), withNested: false },
+      { url: makeURL(name, 2), withNested: false },
+    ],
+
+    focusConfigList: [
+      { index: 2, type: 'top' },
+      { index: 1, type: 'top' },
+      { index: 0, type: 'top' },
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: true
+    },
+
+    expected: [
+      // The test harness window is a parent of all test frames.  It will
+      // always have the same focus time or later as its frames.  So it
+      // appears first.
+      TEST_HARNESS_URL,
+
+      makeURL(name, 0),
+      makeURL(name, 1),
+      makeURL(name, 2),
+
+      // The overall harness has been focused
+      TOP_HARNESS_URL,
+
+      // The extra frame was never focused
+      EXTRA_URL,
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order.  Case 2.');
+
+promise_test(t => {
+  let name = 'focus-controlled-nested-windows';
+  let opts = {
+    scope: makeURL(name),
+
+    frameConfigList: [
+      { url: makeURL(name, 0), withNested: true },
+      { url: makeURL(name, 1), withNested: true },
+      { url: makeURL(name, 2), withNested: true },
+    ],
+
+    focusConfigList: [
+      { index: 0, type: 'top' },
+
+      // Note, some browsers don't let programmatic focus of a frame unless
+      // an ancestor window is already focused.  So focus the window and
+      // then the frame.
+      { index: 1, type: 'top' },
+      { index: 1, type: 'nested' },
+
+      { index: 2, type: 'top' },
+    ],
+
+    matchAllOptions: {
+      includeUncontrolled: false
+    },
+
+    expected: [
+      // Focus order for window 2, but not its frame.  We only focused
+      // the window.
+      makeURL(name, 2),
+
+      // Window 1 is next via focus order, but the window is always
+      // shown first here.  The window gets its last focus time updated
+      // when the frame is focused.  Since the times match between the
+      // two it falls back to creation order.  The window was created
+      // before the frame.  This behavior is being discussed in:
+      // https://github.com/w3c/ServiceWorker/issues/1080
+      makeURL(name, 1),
+      makeURL(name, 1, 'nested'),
+
+      // Focus order for window 0, but not its frame.  We only focused
+      // the window.
+      makeURL(name, 0),
+
+      // Creation order of the frames since they are not focused by
+      // default when they are created.
+      makeURL(name, 0, 'nested'),
+      makeURL(name, 2, 'nested'),
+    ],
+  };
+
+  return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows and frames in focus order.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html
new file mode 100644
index 0000000..ce44f19
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?clients-matchAll';
+var frames = [];
+promise_test(function(t) {
+    return service_worker_unregister_and_register(
+        t, 'resources/clients-matchall-worker.js', scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope + '#1'); })
+      .then(function(frame1) {
+          frames.push(frame1);
+          frame1.focus();
+          return with_iframe(scope + '#2');
+        })
+      .then(function(frame2) {
+          frames.push(frame2);
+          var channel = new MessageChannel();
+
+          return new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+              frame2.contentWindow.navigator.serviceWorker.controller.postMessage(
+                  {port:channel.port2}, [channel.port2]);
+            });
+        })
+      .then(onMessage);
+}, 'Test Clients.matchAll()');
+
+var expected = [
+    // visibilityState, focused, url, type, frameType
+    ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+    ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+function onMessage(e) {
+  assert_equals(e.data.length, 2);
+  assert_array_equals(e.data[0], expected[0]);
+  assert_array_equals(e.data[1], expected[1]);
+  frames.forEach(function(f) { f.remove(); });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html
new file mode 100644
index 0000000..f23dfe7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+    var url = 'resources/empty-worker.js';
+    var scope = 'resources/blank.html';
+    var registration;
+    var controller;
+    var frame;
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(function(swr) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = swr;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope)
+        })
+      .then(function(f) {
+          frame = f;
+          var w = frame.contentWindow;
+          var swc = w.navigator.serviceWorker;
+          assert_true(swc.controller instanceof w.ServiceWorker,
+                      'controller should be a ServiceWorker object');
+
+          frame.remove();
+
+          assert_equals(swc.controller, null,
+                        'disconnected frame should not be controlled');
+        });
+}, 'controller is cleared on disconnected window');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html
new file mode 100644
index 0000000..e4c5e5f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+    var url = 'resources/empty-worker.js';
+    var scope = 'resources/blank.html';
+    var registration;
+    var controller;
+    var frame;
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(function(swr) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = swr;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          var w = frame.contentWindow;
+          controller = w.navigator.serviceWorker.controller;
+          assert_true(controller instanceof w.ServiceWorker,
+                      'controller should be a ServiceWorker object');
+          assert_equals(controller.scriptURL, normalizeURL(url));
+
+          // objects from different windows should not be equal
+          assert_not_equals(controller, registration.active);
+
+          return w.navigator.serviceWorker.getRegistration();
+        })
+      .then(function(frameRegistration) {
+          // SW objects from same window should be equal
+          assert_equals(frameRegistration.active, controller);
+          frame.remove();
+        });
+}, 'controller is set for a controlled document');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html
new file mode 100644
index 0000000..2e966d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on reload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+    const iframe_scope = 'blank.html';
+    const scope = 'resources/' + iframe_scope;
+    var frame;
+    var registration;
+    var controller;
+    return service_worker_unregister(t, scope)
+      .then(function() {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          return frame.contentWindow.navigator.serviceWorker.register(
+              'empty-worker.js', {scope: iframe_scope});
+        })
+      .then(function(swr) {
+          registration = swr;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var w = frame.contentWindow;
+          assert_equals(w.navigator.serviceWorker.controller, null,
+                        'controller should be null until the document is ' +
+                        'reloaded');
+          return new Promise(function(resolve) {
+              frame.onload = function() { resolve(); }
+              w.location.reload();
+            });
+        })
+      .then(function() {
+          var w = frame.contentWindow;
+          controller = w.navigator.serviceWorker.controller;
+          assert_true(controller instanceof w.ServiceWorker,
+                      'controller should be a ServiceWorker object upon reload');
+
+          // objects from separate windows should not be equal
+          assert_not_equals(controller, registration.active);
+
+          return w.navigator.serviceWorker.getRegistration(iframe_scope);
+        })
+      .then(function(frameRegistration) {
+          assert_equals(frameRegistration.active, controller);
+          frame.remove();
+        });
+  }, 'controller is set upon reload after registration');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
new file mode 100644
index 0000000..d947139
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: controller without a fetch event handler</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+let frame;
+const host_info = get_host_info();
+const remote_base_url =
+    new URL(`${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/`);
+
+promise_test(async t => {
+  const script = 'resources/empty.js'
+  const scope = 'resources/';
+
+  promise_test(async t => {
+    if (frame)
+      frame.remove();
+
+    if (registration)
+      await registration.unregister();
+  }, 'cleanup global state');
+
+  registration = await
+      service_worker_unregister_and_register(t, script, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+  frame = await with_iframe(scope + 'blank.html');
+}, 'global setup');
+
+promise_test(async t => {
+    const url = new URL('cors-approved.txt', remote_base_url);
+    const response = await frame.contentWindow.fetch(url, {mode:'no-cors'});
+    const text = await response.text();
+    assert_equals(text, '');
+}, 'cross-origin request, no-cors mode');
+
+
+promise_test(async t => {
+    const url = new URL('cors-denied.txt', remote_base_url);
+    const response = frame.contentWindow.fetch(url);
+    await promise_rejects_js(t, frame.contentWindow.TypeError, response);
+}, 'cross-origin request, cors denied');
+
+promise_test(async t => {
+    const url = new URL('cors-approved.txt', remote_base_url);
+    response = await frame.contentWindow.fetch(url);
+    let text = await response.text();
+    text = text.trim();
+    assert_equals(text, 'plaintext');
+}, 'cross-origin request, cors approved');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html b/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html
new file mode 100644
index 0000000..0a90dc2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Credentials for service worker scripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Check if the service worker's script has appropriate credentials for a new
+// worker and byte-for-byte checking.
+
+const SCOPE = 'resources/in-scope';
+const COOKIE_NAME = `service-worker-credentials-${Math.random()}`;
+
+promise_test(async t => {
+  // Set-Cookies for path=/.
+  await fetch(
+      `/cookies/resources/set-cookie.py?name=${COOKIE_NAME}&path=%2F`);
+}, 'Set cookies as initialization');
+
+async function get_cookies(worker) {
+  worker.postMessage('get cookie');
+  const message = await new Promise(resolve =>
+      navigator.serviceWorker.addEventListener('message', resolve));
+  return message.data;
+}
+
+promise_test(async t => {
+  const key = token();
+  const registration = await service_worker_unregister_and_register(
+      t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE);
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+
+  const cookies = await get_cookies(worker);
+  assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+  await registration.update();
+  const updated_worker = registration.installing;
+  const updated_cookies = await get_cookies(updated_worker);
+  assert_equals(updated_cookies[COOKIE_NAME], '1',
+                'updated worker has credentials');
+}, 'Main script should have credentials');
+
+promise_test(async t => {
+  const key = token();
+  const registration = await service_worker_unregister_and_register(
+      t, `resources/import-echo-cookie-worker.js?key=${key}`, SCOPE);
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+
+  const cookies = await get_cookies(worker);
+  assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+  await registration.update();
+  const updated_worker = registration.installing;
+  const updated_cookies = await get_cookies(updated_worker);
+  assert_equals(updated_cookies[COOKIE_NAME], '1',
+                'updated worker has credentials');
+}, 'Imported script should have credentials');
+
+promise_test(async t => {
+  const key = token();
+  const registration = await service_worker_unregister_and_register(
+      t, `resources/import-echo-cookie-worker-module.py?key=${key}`, SCOPE, {type: 'module'});
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+
+  const cookies = await get_cookies(worker);
+  assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+  await registration.update();
+  const updated_worker = registration.installing;
+  const updated_cookies = await get_cookies(updated_worker);
+  assert_equals(updated_cookies[COOKIE_NAME], undefined,
+                'updated worker should not have credentials');
+}, 'Module with an imported statement should not have credentials');
+
+promise_test(async t => {
+  const key = token();
+  const registration = await service_worker_unregister_and_register(
+t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE, {type: 'module'});
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+
+  const cookies = await get_cookies(worker);
+  assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+  await registration.update();
+  const updated_worker = registration.installing;
+  const updated_cookies = await get_cookies(updated_worker);
+  assert_equals(updated_cookies[COOKIE_NAME], undefined,
+                'updated worker should not have credentials');
+}, 'Script with service worker served as modules should not have credentials');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html
new file mode 100644
index 0000000..d767d57
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Workers in data iframes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+'use strict';
+
+promise_test(t => {
+  const url = encodeURI(`data:text/html,<!DOCTYPE html>
+  <script>
+    parent.postMessage({ isDefined: 'serviceWorker' in navigator }, '*');
+  </` + `script>`);
+  var p = new Promise((resolve, reject) => {
+    window.addEventListener('message', event => {
+      resolve(event.data.isDefined);
+    });
+  });
+  with_iframe(url);
+  return p.then(isDefined => {
+    assert_false(isDefined, 'navigator.serviceWorker should not be defined in iframe');
+  });
+}, 'navigator.serviceWorker is not available in a data: iframe');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html b/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html
new file mode 100644
index 0000000..c503a28
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Post a file in a navigation controlled by a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<iframe id=testframe name=testframe></iframe>
+<form id=testform method=post action="/html/semantics/forms/form-submission-0/resources/file-submission.py" target=testframe enctype="multipart/form-data">
+<input name=testinput id=testinput type=file>
+</form>
+<script>
+// Test that DataTransfer with a File entry works when posted to a
+// service worker that falls back to network. Regression test for
+// https://crbug.com/944145.
+promise_test(async (t) => {
+  const scope = '/html/semantics/forms/form-submission-0/resources/';
+  const header = `pipe=header(Service-Worker-Allowed,${scope})`;
+  const script = `resources/fetch-event-network-fallback-worker.js?${header}`;
+
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const dataTransfer = new DataTransfer();
+  dataTransfer.items.add(new File(['foobar'], 'name'));
+  assert_equals(1, dataTransfer.files.length);
+
+  testinput.files = dataTransfer.files;
+  testform.submit();
+
+  const data = await new Promise(resolve => {
+    onmessage = e => {
+      if (e.source !== testframe) return;
+      resolve(e.data);
+    };
+  });
+  assert_equals(data, "foobar");
+}, 'Posting a File in a navigation handled by a service worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
new file mode 100644
index 0000000..2144f48
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>DedicatedWorker: ServiceWorker interception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Note that Chrome cannot pass these tests because of https://crbug.com/731599.
+
+function service_worker_interception_test(url, description) {
+  promise_test(async t => {
+    // Register a service worker whose scope includes |url|.
+    const kServiceWorkerScriptURL =
+        'resources/service-worker-interception-service-worker.js';
+    const registration = await service_worker_unregister_and_register(
+        t, kServiceWorkerScriptURL, url);
+    add_result_callback(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activated');
+
+    // Start a dedicated worker for |url|. The top-level script request and any
+    // module imports should be intercepted by the service worker.
+    const worker = new Worker(url, { type: 'module' });
+    const msg_event = await new Promise(resolve => worker.onmessage = resolve);
+    assert_equals(msg_event.data, 'LOADED_FROM_SERVICE_WORKER');
+  }, description);
+}
+
+service_worker_interception_test(
+    'resources/service-worker-interception-network-worker.js',
+    'Top-level module loading should be intercepted by a service worker.');
+
+service_worker_interception_test(
+    'resources/service-worker-interception-static-import-worker.js',
+    'Static import should be intercepted by a service worker.');
+
+service_worker_interception_test(
+    'resources/service-worker-interception-dynamic-import-worker.js',
+    'Dynamic import should be intercepted by a service worker.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html b/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html
new file mode 100644
index 0000000..747a953
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service WorkerRegistration from a removed iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+</body>
+<script>
+// NOTE: This file tests corner case behavior that might not be defined in the
+// spec. See https://github.com/w3c/ServiceWorker/issues/1221
+
+promise_test(t => {
+    const url = 'resources/blank.html';
+    const scope_for_iframe = 'removed-registration'
+    const scope_for_main = 'resources/' + scope_for_iframe;
+    const script = 'resources/empty-worker.js';
+    let frame;
+    let resolvedCount = 0;
+
+    return service_worker_unregister(t, scope_for_main)
+      .then(() => {
+          return with_iframe(url);
+        })
+      .then(f => {
+          frame = f;
+          return navigator.serviceWorker.register(script,
+                                                  {scope: scope_for_main});
+        })
+      .then(r => {
+          add_completion_callback(() => { r.unregister(); });
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(() => {
+          return frame.contentWindow.navigator.serviceWorker.getRegistration(
+            scope_for_iframe);
+        })
+      .then(r => {
+          frame.remove();
+          assert_equals(r.installing, null);
+          assert_equals(r.waiting, null);
+          assert_equals(r.active.state, 'activated');
+          assert_equals(r.scope, normalizeURL(scope_for_main));
+          r.onupdatefound = () => { /* empty */ };
+
+          // We want to verify that unregister() and update() do not
+          // resolve on a detached registration.  We can't check for
+          // an explicit rejection, though, because not all browsers
+          // fire rejection callbacks on detached promises.  Instead
+          // we wait for a sample scope to install, activate, and
+          // unregister before declaring that the promises did not
+          // resolve.
+          r.unregister().then(() => resolvedCount += 1,
+                              () => {});
+          r.update().then(() => resolvedCount += 1,
+                          () => {});
+          return wait_for_activation_on_sample_scope(t, window);
+        })
+      .then(() => {
+          assert_equals(resolvedCount, 0,
+                        'methods called on a detached registration should not resolve');
+          frame.remove();
+        })
+  }, 'accessing a ServiceWorkerRegistration from a removed iframe');
+
+promise_test(t => {
+    const script = 'resources/empty-worker.js';
+    const scope = 'resources/scope/serviceworker-from-detached';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(() => { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => { return with_iframe(scope); })
+      .then(frame => {
+          const worker = frame.contentWindow.navigator.serviceWorker.controller;
+          const ctor = frame.contentWindow.DOMException;
+          frame.remove();
+          assert_equals(worker.scriptURL, normalizeURL(script));
+          assert_equals(worker.state, 'activated');
+          worker.onstatechange = () => { /* empty */ };
+          assert_throws_dom(
+              'InvalidStateError',
+               ctor,
+              () => { worker.postMessage(''); },
+              'postMessage on a detached client should throw an exception.');
+        });
+  }, 'accessing a ServiceWorker object from a removed iframe');
+
+promise_test(t => {
+    const iframe = document.createElement('iframe');
+    iframe.src = 'resources/blank.html';
+    document.body.appendChild(iframe);
+    const f = iframe.contentWindow.Function;
+    function get_navigator() {
+      return f('return navigator')();
+    }
+    return new Promise(resolve => {
+        assert_equals(iframe.contentWindow.navigator, get_navigator());
+        iframe.src = 'resources/blank.html?navigate-to-new-url';
+        iframe.onload = resolve;
+      }).then(function() {
+        assert_not_equals(get_navigator().serviceWorker, null);
+        assert_equals(
+            get_navigator().serviceWorker,
+            iframe.contentWindow.navigator.serviceWorker);
+        iframe.remove();
+      });
+  }, 'accessing navigator.serviceWorker on a detached iframe');
+
+test(t => {
+    const iframe = document.createElement('iframe');
+    iframe.src = 'resources/blank.html';
+    document.body.appendChild(iframe);
+    const f = iframe.contentWindow.Function;
+    function get_navigator() {
+      return f('return navigator')();
+    }
+    assert_not_equals(get_navigator().serviceWorker, null);
+    iframe.remove();
+    assert_throws_js(TypeError, () => get_navigator());
+  }, 'accessing navigator on a removed frame');
+
+// It seems weird that about:blank and blank.html (the test above) have
+// different behavior. These expectations are based on Chromium behavior, which
+// might not be right.
+test(t => {
+    const iframe = document.createElement('iframe');
+    iframe.src = 'about:blank';
+    document.body.appendChild(iframe);
+    const f = iframe.contentWindow.Function;
+    function get_navigator() {
+      return f('return navigator')();
+    }
+    assert_not_equals(get_navigator().serviceWorker, null);
+    iframe.remove();
+    assert_equals(get_navigator().serviceWorker, null);
+  }, 'accessing navigator.serviceWorker on a removed about:blank frame');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
new file mode 100644
index 0000000..581dbec
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed and object are not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+
+const kScript = 'resources/embed-and-object-are-not-intercepted-worker.js';
+const kScope = 'resources/';
+
+promise_test(t => {
+    return service_worker_unregister_and_register(t, kScript, kScope)
+      .then(registration => {
+          promise_test(() => {
+              return registration.unregister();
+            }, 'restore global state');
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+  }, 'initialize global state');
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/embed-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request for embedded content was not intercepted');
+        });
+  }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/object-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request for embedded content was not intercepted');
+        });
+  }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/embed-image-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request was not intercepted');
+        });
+  }, 'requests for EMBED elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/object-image-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request was not intercepted');
+        });
+  }, 'requests for OBJECT elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/object-navigation-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request for embedded content was not intercepted');
+        });
+  }, 'post-load navigation of OBJECT elements should not be intercepted by service workers');
+
+
+promise_test(t => {
+    let frame;
+    return with_iframe('resources/embed-navigation-is-not-intercepted-iframe.html')
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.test_promise;
+        })
+      .then(result => {
+          assert_equals(result, 'request for embedded content was not intercepted');
+        });
+  }, 'post-load navigation of EMBED elements should not be intercepted by service workers');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
new file mode 100644
index 0000000..04e9826
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function sync_message(worker, message, transfer) {
+  let wait = new Promise((res, rej) => {
+    navigator.serviceWorker.addEventListener('message', function(e) {
+        if (e.data === 'ACK') {
+          res();
+        } else {
+          rej();
+        }
+      });
+    });
+  worker.postMessage(message, transfer);
+  return wait;
+}
+
+function runTest(test, step, testBody) {
+  var scope = './resources/' + step;
+  var script = 'resources/extendable-event-async-waituntil.js?' + scope;
+  return service_worker_unregister_and_register(test, script, scope)
+    .then(function(registration) {
+        test.add_cleanup(function() {
+            return service_worker_unregister(test, scope);
+          });
+
+        let worker = registration.installing;
+        var channel = new MessageChannel();
+        var saw_message = new Promise(function(resolve) {
+          channel.port1.onmessage = function(e) { resolve(e.data); }
+        });
+
+        return wait_for_state(test, worker, 'activated')
+          .then(function() {
+              return sync_message(worker, { step: 'init', port: channel.port2 },
+                [channel.port2]);
+            })
+          .then(function() { return testBody(worker); })
+          .then(function() { return saw_message; })
+          .then(function(output) {
+              assert_equals(output.result, output.expected);
+            })
+          .then(function() { return sync_message(worker, { step: 'done' }); });
+      });
+}
+
+function msg_event_test(scope, test) {
+  var testBody = function(worker) {
+    return sync_message(worker, { step: scope });
+  };
+  return runTest(test, scope, testBody);
+}
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'),
+  'Test calling waitUntil in a task at the end of the event handler without an existing extension throws');
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-microtask'),
+  'Test calling waitUntil in a microtask at the end of the event handler without an existing extension suceeds');
+
+promise_test(msg_event_test.bind(this, 'current-extension-different-task'),
+  'Test calling waitUntil in a different task an existing extension succeeds');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn'),
+  'Test calling waitUntil at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+  'Test calling waitUntil in a microtask at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn'),
+  'Test calling waitUntil in an existing extension promise handler succeeds (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+  'Test calling waitUntil in a microtask at the end of an existing extension promise handler throws (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'current-extension-expired-different-task'),
+  'Test calling waitUntil after the current extension expired in a different task fails');
+
+promise_test(msg_event_test.bind(this, 'script-extendable-event'),
+  'Test calling waitUntil on a script constructed ExtendableEvent throws exception');
+
+promise_test(function(t) {
+    var testBody = function(worker) {
+      return with_iframe('./resources/pending-respondwith-async-waituntil');
+    }
+    return runTest(t, 'pending-respondwith-async-waituntil', testBody);
+  }, 'Test calling waitUntil asynchronously with pending respondWith promise.');
+
+promise_test(function(t) {
+    var testBody = function(worker) {
+      return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil');
+    }
+    return runTest(t, 'during-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+  }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+    var testBody = function(worker) {
+      return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil');
+    }
+    return runTest(t, 'during-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+  }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+    var testBody = function(worker) {
+      return with_iframe('./resources/after-event-dispatch-respondwith-microtask-sync-waituntil');
+    }
+    return runTest(t, 'after-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+  }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is not being dispatched).');
+
+promise_test(function(t) {
+    var testBody = function(worker) {
+      return with_iframe('./resources/after-event-dispatch-respondwith-microtask-async-waituntil');
+    }
+    return runTest(t, 'after-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+  }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is not being dispatched).');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html
new file mode 100644
index 0000000..33b4eac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<title>ExtendableEvent: waitUntil</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function runTest(test, scope, onRegister) {
+  var script = 'resources/extendable-event-waituntil.js?' + scope;
+  return service_worker_unregister_and_register(test, script, scope)
+    .then(function(registration) {
+        test.add_cleanup(function() {
+            return service_worker_unregister(test, scope);
+          });
+
+        return onRegister(registration.installing);
+      });
+}
+
+// Sends a SYN to the worker and asynchronously listens for an ACK; sets
+// |obj.synced| to true once ack'd.
+function syncWorker(worker, obj) {
+  var channel = new MessageChannel();
+  worker.postMessage({port: channel.port2}, [channel.port2]);
+  return new Promise(function(resolve) {
+      channel.port1.onmessage = resolve;
+    }).then(function(e) {
+      var message = e.data;
+      assert_equals(message, 'SYNC',
+                    'Should receive sync message from worker.');
+      obj.synced = true;
+      channel.port1.postMessage('ACK');
+    });
+}
+
+promise_test(function(t) {
+    // Passing scope as the test switch for worker script.
+    var scope = 'resources/install-fulfilled';
+    var onRegister = function(worker) {
+        var obj = {};
+
+        return Promise.all([
+            syncWorker(worker, obj),
+            wait_for_state(t, worker, 'installed')
+          ]).then(function() {
+              assert_true(
+                obj.synced,
+                'state should be "installed" after the waitUntil promise ' +
+                    'for "oninstall" is fulfilled.');
+              service_worker_unregister_and_done(t, scope);
+            });
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test install event waitUntil fulfilled');
+
+promise_test(function(t) {
+    var scope = 'resources/install-multiple-fulfilled';
+    var onRegister = function(worker) {
+        var obj1 = {};
+        var obj2 = {};
+
+        return Promise.all([
+            syncWorker(worker, obj1),
+            syncWorker(worker, obj2),
+            wait_for_state(t, worker, 'installed')
+          ]).then(function() {
+              assert_true(
+                obj1.synced && obj2.synced,
+                'state should be "installed" after all waitUntil promises ' +
+                    'for "oninstall" are fulfilled.');
+            });
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test ExtendableEvent multiple waitUntil fulfilled.');
+
+promise_test(function(t) {
+    var scope = 'resources/install-reject-precedence';
+    var onRegister = function(worker) {
+        var obj1 = {};
+        var obj2 = {};
+
+        return Promise.all([
+            syncWorker(worker, obj1)
+              .then(function() {
+                  syncWorker(worker, obj2);
+                }),
+            wait_for_state(t, worker, 'redundant')
+          ]).then(function() {
+              assert_true(
+                obj1.synced,
+                'The "redundant" state was entered after the first "extend ' +
+                  'lifetime promise" resolved.'
+              );
+              assert_true(
+                obj2.synced,
+                'The "redundant" state was entered after the third "extend ' +
+                  'lifetime promise" resolved.'
+              );
+            });
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test ExtendableEvent waitUntil reject precedence.');
+
+promise_test(function(t) {
+    var scope = 'resources/activate-fulfilled';
+    var onRegister = function(worker) {
+        var obj = {};
+        return wait_for_state(t, worker, 'activating')
+          .then(function() {
+              return Promise.all([
+                syncWorker(worker, obj),
+                wait_for_state(t, worker, 'activated')
+              ]);
+            })
+          .then(function() {
+              assert_true(
+                obj.synced,
+                'state should be "activated" after the waitUntil promise ' +
+                    'for "onactivate" is fulfilled.');
+            });
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test activate event waitUntil fulfilled');
+
+promise_test(function(t) {
+    var scope = 'resources/install-rejected';
+    var onRegister = function(worker) {
+        return wait_for_state(t, worker, 'redundant');
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test install event waitUntil rejected');
+
+promise_test(function(t) {
+    var scope = 'resources/activate-rejected';
+    var onRegister = function(worker) {
+        return wait_for_state(t, worker, 'activated');
+      };
+    return runTest(t, scope, onRegister);
+  }, 'Test activate event waitUntil rejected.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html
new file mode 100644
index 0000000..9821759
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(async (t) => {
+    const SCOPE = 'resources/empty.html';
+    const SCRIPT = 'resources/fetch-rewrite-worker.js';
+    const host_info = get_host_info();
+    const REMOTE_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+
+    const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+    await wait_for_state(t, reg.installing, 'activated');
+    const frame = await with_iframe(SCOPE);
+
+    const doc = frame.contentDocument;
+    const win = frame.contentWindow;
+
+    const context = new win.AudioContext();
+    try {
+      context.suspend();
+      const audio = doc.createElement('audio');
+      audio.autoplay = true;
+      const source = context.createMediaElementSource(audio);
+      const spn = context.createScriptProcessor(16384, 1, 1);
+      source.connect(spn).connect(context.destination);
+      const url = `${REMOTE_ORIGIN}/webaudio/resources/sin_440Hz_-6dBFS_1s.wav`;
+      audio.src = '/test?url=' + encodeURIComponent(url);
+      doc.body.appendChild(audio);
+
+      await new Promise((resolve) => {
+        audio.addEventListener('playing', resolve);
+      });
+      await context.resume();
+      const event = await new Promise((resolve) => {
+        spn.addEventListener('audioprocess', resolve);
+      });
+      const data = event.inputBuffer.getChannelData(0);
+      for (const e of data) {
+        assert_equals(e, 0);
+      }
+    } finally {
+      context.close();
+    }
+  }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
new file mode 100644
index 0000000..dab2153
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<title>canvas tainting when written twice</title>
+<script>
+function loadImage(doc, url) {
+  return new Promise((resolve, reject) => {
+    const image = doc.createElement('img');
+    image.onload = () => { resolve(image); }
+    image.onerror = () => { reject('failed to load: ' + url); };
+    image.src = url;
+  });
+}
+
+// Tests that a canvas is tainted after it's written to with both a clear image
+// and opaque image from the same URL. A bad implementation might cache the
+// info of the clear image and assume the opaque image is also clear because
+// it's from the same URL. See https://crbug.com/907047 for details.
+promise_test(async (t) => {
+  // Set up a service worker and a controlled iframe.
+  const script = 'resources/fetch-canvas-tainting-double-write-worker.js';
+  const scope = 'resources/fetch-canvas-tainting-double-write-iframe.html';
+  const registration = await service_worker_unregister_and_register(
+      t, script, scope);
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const iframe = await with_iframe(scope);
+  t.add_cleanup(() => iframe.remove());
+
+  // Load the same cross-origin image URL through the controlled iframe and
+  // this uncontrolled frame. The service worker responds with a same-origin
+  // image for the controlled iframe, so it is cleartext.
+  const imagePath = base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+  const imageUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + imagePath;
+  const clearImage = await loadImage(iframe.contentDocument, imageUrl);
+  const opaqueImage = await loadImage(document, imageUrl);
+
+  // Set up a canvas for testing tainting.
+  const canvas = document.createElement('canvas');
+  const context = canvas.getContext('2d');
+  canvas.width = clearImage.width;
+  canvas.height = clearImage.height;
+
+  // The clear image and the opaque image have the same src URL. But...
+
+  // ... the clear image doesn't taint the canvas.
+  context.drawImage(clearImage, 0, 0);
+  assert_true(canvas.toDataURL().length > 0);
+
+  // ... the opaque image taints the canvas.
+  context.drawImage(opaqueImage, 0, 0);
+  assert_throws_dom('SecurityError', () => { canvas.toDataURL(); });
+}, 'canvas is tainted after writing both a non-opaque image and an opaque image from the same URL');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
new file mode 100644
index 0000000..2132381
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+  resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+  cache: true
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
new file mode 100644
index 0000000..57dc7d9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+  resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+  cache: false
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
new file mode 100644
index 0000000..c37e8e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video using cache responses</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+  resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+  cache: true
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
new file mode 100644
index 0000000..28c3071
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Canvas tainting due to video whose responses are fetched via a service worker including range requests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// These tests try to test canvas tainting due to a <video> element. The video
+// src URL is same-origin as the page, but the response is fetched via a service
+// worker that does tricky things like returning opaque responses from another
+// origin. Furthermore, this tests range requests so there are multiple
+// responses.
+//
+// We test range requests by having the server return 206 Partial Content to the
+// first request (which doesn't necessarily have a "Range" header or one with a
+// byte range). Then the <video> element automatically makes ranged requests
+// (the "Range" HTTP request header specifies a byte range). The server responds
+// to these with 206 Partial Content for the given range.
+function range_request_test(script, expected, description) {
+  promise_test(t => {
+      let frame;
+      let registration;
+      add_result_callback(() => {
+          if (frame) frame.remove();
+          if (registration) registration.unregister();
+        });
+
+      const scope = 'resources/fetch-canvas-tainting-iframe.html';
+      return service_worker_unregister_and_register(t, script, scope)
+        .then(r => {
+            registration = r;
+            return wait_for_state(t, registration.installing, 'activated');
+          })
+        .then(() => {
+            return with_iframe(scope);
+          })
+        .then(f => {
+            frame = f;
+            // Add "?VIDEO&PartialContent" to get a video resource from the
+            // server using range requests.
+            const video_url = 'fetch-access-control.py?VIDEO&PartialContent';
+            return frame.contentWindow.create_test_case_promise(video_url);
+          })
+        .then(result => {
+            assert_equals(result, expected);
+          });
+    }, description);
+}
+
+// We want to consider a number of scenarios:
+// (1) Range responses come from a single origin, the same-origin as the page.
+//     The canvas should not be tainted.
+range_request_test(
+  'resources/fetch-event-network-fallback-worker.js',
+  'NOT_TAINTED',
+  'range responses from single origin (same-origin)');
+
+// (2) Range responses come from a single origin, cross-origin from the page
+//     (and without CORS sharing). This is not possible to test, since service
+//     worker can't make a request with a "Range" HTTP header in no-cors mode.
+
+// (3) Range responses come from multiple origins. The first response comes from
+//     cross-origin (and without CORS sharing, so is opaque). Subsequent
+//     responses come from same-origin. This should result in a load error, as regardless of canvas
+//     loading range requests from multiple opaque origins can reveal information across those origins.
+range_request_test(
+  'resources/range-request-to-different-origins-worker.js',
+  'LOAD_ERROR',
+  'range responses from multiple origins (cross-origin first)');
+
+// (4) Range responses come from multiple origins. The first response comes from
+//     same-origin. Subsequent responses come from cross-origin (and without
+//     CORS sharing). Like (2) this is not possible since the service worker
+//     cannot make range requests cross-origin.
+
+// (5) Range responses come from a single origin, with a mix of opaque and
+//     non-opaque responses. The first request uses 'no-cors' mode to
+//     receive an opaque response, and subsequent range requests use 'cors'
+//     to receive non-opaque responses. The canvas should be tainted.
+range_request_test(
+  'resources/range-request-with-different-cors-modes-worker.js',
+  'TAINTED',
+  'range responses from single origin with both opaque and non-opaque responses');
+
+// (6) Range responses come from a single origin, with a mix of opaque and
+//     non-opaque responses. The first request uses 'cors' mode to
+//     receive an non-opaque response, and subsequent range requests use
+//     'no-cors' to receive non-opaque responses. Like (2) this is not possible.
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
new file mode 100644
index 0000000..e8c23a2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+  resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+  cache: false
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
new file mode 100644
index 0000000..317b021
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS-exposed header names should be transferred correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async function(t) {
+    const SCOPE = 'resources/simple.html';
+    const SCRIPT = 'resources/fetch-cors-exposed-header-names-worker.js';
+    const host_info = get_host_info();
+
+    const URL = get_host_info().HTTPS_REMOTE_ORIGIN +
+      '/service-workers/service-worker/resources/simple.txt?pipe=' +
+      'header(access-control-allow-origin,*)|' +
+      'header(access-control-expose-headers,*)|' +
+      'header(foo,bar)|' +
+      'header(set-cookie,X)';
+
+    const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+    await wait_for_state(t, reg.installing, 'activated');
+    const frame = await with_iframe(SCOPE);
+
+    const response = await frame.contentWindow.fetch(URL);
+    const headers = response.headers;
+    assert_equals(headers.get('foo'), 'bar');
+    assert_equals(headers.get('set-cookie'), null);
+    assert_equals(headers.get('access-control-expose-headers'), '*');
+  }, 'CORS-exposed header names for a response from sw');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html
new file mode 100644
index 0000000..f8ff445
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS XHR of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/fetch-cors-xhr-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var host_info = get_host_info();
+
+    return login_https(t)
+      .then(function() {
+          return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+        })
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          return new Promise(function(resolve, reject) {
+              var channel = new MessageChannel();
+              channel.port1.onmessage = (event) => {
+                  if (event.data === 'done') {
+                    resolve();
+                    return;
+                  }
+                  test(() => {
+                    assert_true(event.data.result);
+                  }, event.data.testName);
+              };
+              frame.contentWindow.postMessage({},
+                                              host_info['HTTPS_ORIGIN'],
+                                              [channel.port2]);
+            });
+        });
+  }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html
new file mode 100644
index 0000000..9e7b242
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP control of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+function assert_resolves(promise, description) {
+  return promise.catch(function(reason) {
+      throw new Error(description + ' - ' + reason.message);
+  });
+}
+
+function assert_rejects(promise, description) {
+  return promise.then(
+      function() { throw new Error(description); },
+      function() {});
+}
+
+promise_test(function(t) {
+    var SCOPE = 'resources/fetch-csp-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var host_info = get_host_info();
+    var IMAGE_PATH =
+        base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+    var IMAGE_URL = host_info['HTTPS_ORIGIN'] + IMAGE_PATH;
+    var REMOTE_IMAGE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_PATH;
+    var REDIRECT_URL =
+        host_info['HTTPS_ORIGIN'] + base_path() + 'resources/redirect.py';
+    var frame;
+
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(
+              SCOPE + '?' +
+              encodeURIComponent('img-src ' + host_info['HTTPS_ORIGIN'] +
+                                 '; script-src \'unsafe-inline\''));
+        })
+      .then(function(f) {
+          frame = f;
+          return assert_resolves(
+              frame.contentWindow.load_image(IMAGE_URL),
+              'Allowed scope image resource should be loaded.');
+        })
+      .then(function() {
+          return assert_rejects(
+              frame.contentWindow.load_image(REMOTE_IMAGE_URL),
+              'Disallowed scope image resource should not be loaded.');
+        })
+      .then(function() {
+          return assert_resolves(
+              frame.contentWindow.load_image(
+                  // The request for IMAGE_URL will be fetched in SW.
+                  './sample?url=' + encodeURIComponent(IMAGE_URL)),
+              'Allowed scope image resource which was fetched via SW should ' +
+              'be loaded.');
+        })
+      .then(function() {
+          return assert_rejects(
+              frame.contentWindow.load_image(
+                  // The request for REMOTE_IMAGE_URL will be fetched in SW.
+                  './sample?mode=no-cors&url=' +
+                  encodeURIComponent(REMOTE_IMAGE_URL)),
+              'Disallowed scope image resource which was fetched via SW ' +
+              'should not be loaded.');
+        })
+      .then(function() {
+          frame.remove();
+          return with_iframe(
+              SCOPE + '?' +
+              encodeURIComponent(
+                  'img-src ' + REDIRECT_URL +
+                  '; script-src \'unsafe-inline\''));
+        })
+      .then(function(f) {
+          frame = f;
+          return assert_resolves(
+              frame.contentWindow.load_image(
+                  // Set 'ignore' not to call respondWith() in the SW.
+                  REDIRECT_URL + '?ignore&Redirect=' +
+                  encodeURIComponent(IMAGE_URL)),
+              'When the request was redirected, CSP match algorithm should ' +
+              'ignore the path component of the URL.');
+        })
+      .then(function() {
+          return assert_resolves(
+              frame.contentWindow.load_image(
+                  // This request will be fetched via SW and redirected by
+                  // redirect.php.
+                  REDIRECT_URL + '?Redirect=' + encodeURIComponent(IMAGE_URL)),
+              'When the request was redirected via SW, CSP match algorithm ' +
+              'should ignore the path component of the URL.');
+        })
+      .then(function() {
+          return assert_resolves(
+              frame.contentWindow.load_image(
+                  // The request for IMAGE_URL will be fetched in SW.
+                  REDIRECT_URL + '?url=' + encodeURIComponent(IMAGE_URL)),
+              'When the request was fetched via SW, CSP match algorithm ' +
+              'should ignore the path component of the URL.');
+        })
+      .then(function() {
+          return assert_resolves(
+              frame.contentWindow.fetch(IMAGE_URL + "&fetch1", { mode: 'no-cors'}),
+              'Allowed scope fetch resource should be loaded.');
+        })
+      .then(function() {
+          return assert_resolves(
+              frame.contentWindow.fetch(
+                  // The request for IMAGE_URL will be fetched in SW.
+                  './sample?url=' + encodeURIComponent(IMAGE_URL + '&fetch2'), { mode: 'no-cors'}),
+              'Allowed scope fetch resource which was fetched via SW should be loaded.');
+        })
+      .then(function() {
+          return assert_rejects(
+              frame.contentWindow.fetch(REMOTE_IMAGE_URL + "&fetch3", { mode: 'no-cors'}),
+              'Disallowed scope fetch resource should not be loaded.');
+        })
+      .then(function() {
+          return assert_rejects(
+              frame.contentWindow.fetch(
+                  // The request for REMOTE_IMAGE_URL will be fetched in SW.
+                  './sample?url=' + encodeURIComponent(REMOTE_IMAGE_URL + '&fetch4'), { mode: 'no-cors'}),
+              'Disallowed scope fetch resource which was fetched via SW should not be loaded.');
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Verify CSP control of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html
new file mode 100644
index 0000000..ca2f884
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+const scope = "./resources/in-scope";
+
+promise_test(async (test) => {
+    const registration = await service_worker_unregister_and_register(
+        test, "./resources/fetch-error-worker.js", scope);
+    promise_test(async () => registration.unregister(),
+                 "Unregister service worker");
+    await wait_for_state(test, registration.installing, 'activated');
+}, "Setup service worker");
+
+promise_test(async (test) => {
+    const iframe = await with_iframe(scope);
+    test.add_cleanup(() => iframe.remove());
+    const response = await iframe.contentWindow.fetch("fetch-error-test");
+    try {
+      await response.text();
+      assert_unreached();
+    } catch (error) {
+      assert_true(error.message.includes("Sorry"));
+    }
+}, "Make sure a load that makes progress does not time out");
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html
new file mode 100644
index 0000000..ac13e4f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event added asynchronously doesn't throw</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+service_worker_test(
+  'resources/fetch-event-add-async-worker.js');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
new file mode 100644
index 0000000..4812d8a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+    var scope =
+        'resources/fetch-event-after-navigation-within-page-iframe.html' +
+        '?hashchange';
+    var worker = 'resources/simple-intercept-worker.js';
+    var frame;
+
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          return frame.contentWindow.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'intercepted by service worker');
+          frame.contentWindow.location.hash = 'foo';
+          return frame.contentWindow.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'intercepted by service worker');
+          frame.remove();
+        })
+  }, 'Service Worker should respond to fetch event after the hash changes');
+
+promise_test(function(t) {
+    var scope =
+        'resources/fetch-event-after-navigation-within-page-iframe.html' +
+        '?pushState';
+    var worker = 'resources/simple-intercept-worker.js';
+    var frame;
+
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          return frame.contentWindow.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'intercepted by service worker');
+          frame.contentWindow.history.pushState('', '', 'bar');
+          return frame.contentWindow.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'intercepted by service worker');
+          frame.remove();
+        })
+  }, 'Service Worker should respond to fetch event after the pushState');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
new file mode 100644
index 0000000..d9147f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<title>respondWith cannot be called asynchronously</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This file has tests that call respondWith() asynchronously.
+
+let frame;
+let worker;
+const script = 'resources/fetch-event-async-respond-with-worker.js';
+const scope = 'resources/simple.html';
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+  frame = await with_iframe(scope);
+}, 'global setup');
+
+// Waits for a single message from the service worker and then removes the
+// message handler. Not safe for concurrent use.
+function wait_for_message() {
+  return new Promise((resolve) => {
+    const handler = (event) => {
+      navigator.serviceWorker.removeEventListener('message', handler);
+      resolve(event.data);
+    };
+    navigator.serviceWorker.addEventListener('message', handler);
+  });
+}
+
+// Does one test case. It fetches |url|. The service worker gets a fetch event
+// for |url| and attempts to call respondWith() asynchronously. It reports back
+// to the test whether an exception was thrown.
+async function do_test(url) {
+  // Send a message to tell the worker a new test case is starting.
+  const message = wait_for_message();
+  worker.postMessage('initializeMessageHandler');
+  const response = await message;
+  assert_equals(response, 'messageHandlerInitialized');
+
+  // Start a fetch.
+  const fetchPromise = frame.contentWindow.fetch(url);
+
+  // Receive the test result from the service worker.
+  const result = wait_for_message();
+  await fetchPromise.then(()=> {}, () => {});
+  return result;
+};
+
+promise_test(async (t) => {
+  const result = await do_test('respondWith-in-task');
+  assert_true(result.didThrow, 'should throw');
+  assert_equals(result.error, 'InvalidStateError');
+}, 'respondWith in a task throws InvalidStateError');
+
+promise_test(async (t) => {
+  const result = await do_test('respondWith-in-microtask');
+  assert_equals(result.didThrow, false, 'should not throw');
+}, 'respondWith in a microtask does not throw');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+  if (frame)
+    frame.remove();
+  await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html
new file mode 100644
index 0000000..08b88ce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<title>Service Worker: FetchEvent.handled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+let frame = null;
+let worker = null;
+const script = 'resources/fetch-event-handled-worker.js';
+const scope = 'resources/simple.html';
+const channel = new MessageChannel();
+
+// Wait for a message from the service worker and removes the message handler.
+function wait_for_message_from_worker() {
+  return new Promise((resolve) => channel.port2.onmessage = (event) => resolve(event.data));
+}
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  worker = registration.installing;
+  if (!worker)
+      worker = registration.active;
+  worker.postMessage({port:channel.port1}, [channel.port1]);
+  await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+promise_test(async (t) => {
+  const promise = with_iframe(scope);
+  const message = await wait_for_message_from_worker();
+  frame = await promise;
+  assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+    ' navigation request');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch('sample.txt?respondWith-not-called');
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+    ' sub-resource request');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'sample.txt?respondWith-not-called-and-event-canceled').catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when respondWith() is not called and the' +
+    ' event is canceled');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'sample.txt?respondWith-called-and-promise-resolved');
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when the promise provided' +
+    ' to respondWith() is resolved');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'sample.txt?respondWith-called-and-promise-resolved-to-invalid-response')
+      .catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided' +
+    ' to respondWith() is resolved to an invalid response');
+
+promise_test(async (t) => {
+  frame.contentWindow.fetch(
+      'sample.txt?respondWith-called-and-promise-rejected').catch((e) => {});
+  const message = await wait_for_message_from_worker();
+  assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided to' +
+    ' respondWith() is rejected');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+  if (frame)
+    frame.remove();
+  await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
new file mode 100644
index 0000000..3cf5922
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+   Once you see &quot;method = GET,...&quot; in the page, go to another page, and then go back to the page using the Backward button.
+   You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
new file mode 100644
index 0000000..401939b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+   Once you see &quot;method = GET,...&quot; in the page, go back to this page using the Backward button, and then go to the second page using the Forward button.
+   You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
new file mode 100644
index 0000000..cf1fecc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+promise_test(async (t) => {
+  const scope = 'resources/simple.html?isReloadNavigation';
+
+  const reg = await service_worker_unregister_and_register(t, worker, scope);
+  await wait_for_state(t, reg.installing, 'activated');
+  const frame = await with_iframe(scope);
+  assert_equals(frame.contentDocument.body.textContent,
+                'method = GET, isReloadNavigation = false');
+  await new Promise((resolve) => {
+    frame.addEventListener('load', resolve);
+    frame.contentDocument.body.innerText =
+      'Reload this frame manually!';
+  });
+  assert_equals(frame.contentDocument.body.textContent,
+      'method = GET, isReloadNavigation = true');
+  frame.remove();
+  await reg.unregister();
+}, 'FetchEvent#request.isReloadNavigation is true for manual reload.');
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
new file mode 100644
index 0000000..a349f07
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>.
+   Once you see &quot;method = GET,...&quot; in the page, reload the page.
+   You will see &quot;method = GET, isReloadNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html
new file mode 100644
index 0000000..fea2ad1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event network error</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+    resolve_test_done = resolve;
+  });
+
+// Called by the child frame.
+function notify_test_done(result) {
+  resolve_test_done(result);
+}
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-event-network-error-controllee-iframe.html';
+    var script = 'resources/fetch-event-network-error-worker.js';
+    var frame;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          return test_done_promise;
+        })
+      .then(function(result) {
+          frame.remove();
+          assert_equals(result, 'PASS');
+        });
+  }, 'Rejecting the fetch event or using preventDefault() causes a network ' +
+     'error');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html
new file mode 100644
index 0000000..5229284
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html
@@ -0,0 +1,1038 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Redirect Handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ------------------------
+// Utilities for testing non-navigation requests that are intercepted with
+// a redirect.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+const kScope = host_info['HTTPS_ORIGIN'] + base_path() +
+               'resources/blank.html?fetch-event-redirect';
+let frame;
+
+function redirect_fetch_test(t, test) {
+  const hostKeySuffix = test['url_credentials'] ? '_WITH_CREDS' : '';
+  const successPath = base_path() + 'resources/success.py';
+
+  let acaOrigin = '';
+  let host = host_info['HTTPS_ORIGIN' + hostKeySuffix];
+  if (test['redirect_dest'] === 'no-cors') {
+    host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+  } else if (test['redirect_dest'] === 'cors') {
+    acaOrigin = '?ACAOrigin=' + encodeURIComponent(host_info['HTTPS_ORIGIN']);
+    host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+  }
+
+  const dest = '?Redirect=' + encodeURIComponent(host + successPath + acaOrigin);
+  const expectedTypeParam =
+      test['expected_type']
+          ? '&expected_type=' + test['expected_type']
+          : '';
+  const expectedRedirectedParam =
+      test['expected_redirected']
+          ? '&expected_redirected=' + test['expected_redirected']
+          : '';
+  const url = '/' + test.name +
+            '?url=' + encodeURIComponent('redirect.py' + dest) +
+            expectedTypeParam + expectedRedirectedParam
+  const request = new Request(url, test.request_init);
+
+  if (test.should_reject) {
+    return promise_rejects_js(
+      t,
+      frame.contentWindow.TypeError,
+      frame.contentWindow.fetch(request),
+      'Must fail to fetch: url=' + url);
+  }
+  return frame.contentWindow.fetch(request).then((response) => {
+      assert_equals(response.type, test.expected_type,
+                    'response.type');
+      assert_equals(response.redirected, test.expected_redirected,
+                    'response.redirected');
+      if (response.type === 'opaque' || response.type === 'opaqueredirect') {
+        return;
+      }
+      return response.json().then((json) => {
+        assert_equals(json.result, 'success', 'JSON result must be "success".');
+      });
+    });
+}
+
+// Set up the service worker and the frame.
+promise_test(t => {
+    return service_worker_unregister_and_register(t, kScript, kScope)
+      .then(registration => {
+          promise_test(() => {
+              return registration.unregister();
+            }, 'restore global state');
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => {
+          return with_iframe(kScope);
+        })
+      .then(f => {
+          frame = f;
+          add_completion_callback(() => { f.remove(); });
+        });
+  }, 'initialize global state');
+
+// ------------------------
+// Test every combination of:
+//  - RequestMode (same-origin, cors, no-cors)
+//  - RequestRedirect (manual, follow, error)
+//  - redirect destination origin (same-origin, cors, no-cors)
+//  - redirect destination credentials (no user/pass, user/pass)
+//
+// TODO: add navigation requests
+// TODO: add redirects to data URI and verify same-origin data-URL flag behavior
+// TODO: add test where original redirect URI is cross-origin
+// TODO: verify final method is correct for 301, 302, and 303
+// TODO: verify CORS redirect results in all further redirects being
+//       considered cross origin
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'same-origin without credentials should succeed opaqueredirect ' +
+   'interception and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'no-cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'same-origin without credentials should succeed opaqueredirect ' +
+   'interception and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'no-cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'same-origin without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'no-cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'cors without credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'same-origin with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'no-cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-cors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+   'cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'same-origin with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'no-cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-sameorigin-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+   'cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'same-origin with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'no-cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-manual-nocors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    expected_type: 'opaqueredirect',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'manual',
+      mode: 'no-cors'
+    },
+    // This should succeed because its redirecting from same-origin to
+    // cross-origin.  Since the same-origin URL provides the location
+    // header the manual redirect mode should result in an opaqueredirect
+    // response.
+    should_reject: false
+  });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+   'cors with credentials should succeed opaqueredirect interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'same-origin without credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    // should reject because CORS requests require CORS headers on cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'no-cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    expected_type: 'cors',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'cors without credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'same-origin without credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    // should reject because same-origin requests cannot load cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'no-cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    // should reject because same-origin requests cannot load cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'same-origin without credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    expected_type: 'opaque',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'no-cors without credentials should succeed interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    expected_type: 'opaque',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'cors without credentials should succeed interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'same-origin with credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    // should reject because CORS requests require CORS headers on cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'no-cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-cors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'cors'
+    },
+    // should reject because CORS requests do not allow user/pass entries in
+    // cross-origin URLs
+    // NOTE: https://github.com/whatwg/fetch/issues/112
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+   'cors with credentials should fail interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'same-origin with credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    // should reject because same-origin requests cannot load cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'no-cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-sameorigin-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'same-origin'
+    },
+    // should reject because same-origin requests cannot load cross-origin
+    // resources
+    should_reject: true
+  });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+   'cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    expected_type: 'basic',
+    expected_redirected: true,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'same-origin with credentials should succeed interception ' +
+   'and response should be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    expected_type: 'opaque',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'no-cors with credentials should succeed interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-follow-nocors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    expected_type: 'opaque',
+    expected_redirected: false,
+    request_init: {
+      redirect: 'follow',
+      mode: 'no-cors'
+    },
+    should_reject: false
+  });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+   'cors with credentials should succeed interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'same-origin without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'no-cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'same-origin without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'no-cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-sameorigin-nocreds',
+    redirect_dest: 'same-origin',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'same-origin without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-nocors-nocreds',
+    redirect_dest: 'no-cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'no-cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-cors-nocreds',
+    redirect_dest: 'cors',
+    url_credentials: false,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'cors without credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'same-origin with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'no-cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-cors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+   'cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'same-origin with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'no-cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-sameorigin-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'same-origin'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+   'cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-sameorigin-creds',
+    redirect_dest: 'same-origin',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'same-origin with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-nocors-creds',
+    redirect_dest: 'no-cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'no-cors with credentials should fail interception ' +
+   'and response should not be redirected');
+
+promise_test(function(t) {
+  return redirect_fetch_test(t, {
+    name: 'nonav-error-nocors-redirects-to-cors-creds',
+    redirect_dest: 'cors',
+    url_credentials: true,
+    request_init: {
+      redirect: 'error',
+      mode: 'no-cors'
+    },
+    // should reject because requests with 'error' RequestRedirect cannot be
+    // redirected.
+    should_reject: true
+  });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+   'cors with credentials should fail interception and response should not ' +
+   'be redirected');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
new file mode 100644
index 0000000..af4b20a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+
+function do_test(referrer, value, expected, name)
+{
+    test(() => {
+          assert_equals(value, expected);
+    }, name + (referrer ? " - Custom Referrer" : " - Default Referrer"));
+}
+
+function run_referrer_policy_tests(frame, referrer, href, origin) {
+    return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                     {method: "GET", referrer: referrer})
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer URL when a member of RequestInit is present');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {method: "GET", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with no referrer when a member of RequestInit is present with an HTTP request');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer with ""');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with no referrer with ""');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: origin',
+            'Service Worker should respond to fetch with the referrer origin with "origin" and a same origin request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: origin',
+            'Service Worker should respond to fetch with the referrer origin with "origin" and a cross origin request');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer URL with "origin-when-cross-origin" and a same origin request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer origin with "origin-when-cross-origin" and a cross origin request');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: no-referrer-when-downgrade',
+            'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and a same origin request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: no-referrer-when-downgrade',
+            'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and an HTTP request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url, {referrerPolicy: "unsafe-url", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: unsafe-url',
+            'Service Worker should respond to fetch with no referrer with "unsafe-url"');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "no-referrer", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: no-referrer',
+            'Service Worker should respond to fetch with no referrer URL with "no-referrer"');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "same-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: same-origin',
+            'Service Worker should respond to fetch with referrer URL with "same-origin" and a same origin request');
+          var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "same-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: same-origin',
+            'Service Worker should respond to fetch with no referrer with "same-origin" and cross origin request');
+          var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "strict-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: strict-origin',
+            'Service Worker should respond to fetch with the referrer origin  with "strict-origin" and a HTTPS cross origin request');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "strict-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: strict-origin',
+            'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a same origin request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "strict-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: strict-origin',
+            'Service Worker should respond to fetch with no referrer with "strict-origin" and a HTTP request');
+          return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+                                           {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + href + '\n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer URL with "strict-origin-when-cross-origin" and a same origin request');
+          var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: ' + origin + '/' + '\n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with the referrer origin with "strict-origin-when-cross-origin" and a HTTPS cross origin request');
+          var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+                         '/resources/simple.html?referrerFull';
+          return frame.contentWindow.fetch(http_url,
+                                           {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          do_test(referrer,
+            response_text,
+            'Referrer: \n' +
+            'ReferrerPolicy: strict-origin-when-cross-origin',
+            'Service Worker should respond to fetch with no referrer with "strict-origin-when-cross-origin" and a HTTP request');
+        });
+}
+
+promise_test(function(t) {
+    var scope = 'resources/simple.html?referrerPolicy';
+    var frame;
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          test(() => {
+            assert_equals(frame.contentDocument.body.textContent, 'ReferrerPolicy: strict-origin-when-cross-origin');
+          }, 'Service Worker should respond to fetch with the default referrer policy');
+          // First, run the referrer policy tests without passing a referrer in RequestInit.
+          return run_referrer_policy_tests(frame, undefined, frame.contentDocument.location.href,
+                                           frame.contentDocument.location.origin);
+        })
+      .then(function() {
+          // Now, run the referrer policy tests while passing a referrer in RequestInit.
+          var referrer = get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/fake-referrer';
+          return run_referrer_policy_tests(frame, 'fake-referrer', referrer,
+                                           frame.contentDocument.location.origin);
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Service Worker responds to fetch event with the referrer policy');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
new file mode 100644
index 0000000..05e2210
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.respondWith() argument type test.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+    resolve_test_done = resolve;
+  });
+
+// Called by the child frame.
+function notify_test_done(result) {
+  resolve_test_done(result);
+}
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-event-respond-with-argument-iframe.html';
+    var script = 'resources/fetch-event-respond-with-argument-worker.js';
+    var frame;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          return test_done_promise;
+        })
+      .then(function(result) {
+          frame.remove();
+          assert_equals(result, 'PASS');
+        });
+  }, 'respondWith() takes either a Response or a promise that resolves ' +
+     'with a Response. Other values should raise a network error.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
new file mode 100644
index 0000000..932f903
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response whose body is being loaded from the network by chunks</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-body-loaded-in-chunk-iframe.html';
+
+promise_test(async t => {
+    var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+    add_completion_callback(() => reg.unregister());
+    await wait_for_state(t, reg.installing, 'activated');
+    let iframe = await with_iframe(SCOPE);
+    t.add_cleanup(() => iframe.remove());
+
+    let response = await iframe.contentWindow.fetch('body-in-chunk');
+    assert_equals(await response.text(), 'TEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\n');
+}, 'Respond by chunks with a Response being loaded');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
new file mode 100644
index 0000000..645a29c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a new Response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+  'resources/fetch-event-respond-with-custom-response-worker.js';
+const SCOPE =
+  'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+  return promise_test(async t => {
+    const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+    add_completion_callback(() => reg.unregister());
+    await wait_for_state(t, reg.installing, 'activated');
+    const iframe = await with_iframe(url);
+    const iwin = iframe.contentWindow;
+    t.add_cleanup(() => iframe.remove());
+    await callback(t, iwin);
+  }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=string');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a string');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=blob');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a blob');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=buffer');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=buffer-view');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer-view');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=form-data');
+  const data = await response.formData();
+  assert_equals(data.get('result'), 'PASS');
+}, 'Subresource built from form-data');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?type=search-params');
+  assert_equals(await response.text(), 'result=PASS');
+}, 'Subresource built from search-params');
+
+// As above, but navigations
+
+iframeTest(SCOPE + '?type=string', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a string');
+
+iframeTest(SCOPE + '?type=blob', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a blob');
+
+iframeTest(SCOPE + '?type=buffer', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer');
+
+iframeTest(SCOPE + '?type=buffer-view', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer-view');
+
+// Note: not testing form data for a navigation as the boundary header is lost.
+
+iframeTest(SCOPE + '?type=search-params', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'result=PASS');
+}, 'Navigation resource built from search-params');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
new file mode 100644
index 0000000..505cef2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith streams data to an intercepted fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+  'resources/fetch-event-respond-with-partial-stream-worker.js';
+const SCOPE =
+  'resources/fetch-event-respond-with-partial-stream-iframe.html';
+
+promise_test(async t => {
+  let reg = await service_worker_unregister_and_register(t, WORKER, SCOPE)
+  add_completion_callback(() => reg.unregister());
+
+  await wait_for_state(t, reg.installing, 'activated');
+
+  let frame = await with_iframe(SCOPE);
+  t.add_cleanup(_ => frame.remove());
+
+  let response = await frame.contentWindow.fetch('partial-stream.txt');
+
+  let reader = response.body.getReader();
+
+  let encoder = new TextEncoder();
+  let decoder = new TextDecoder();
+
+  let expected = 'partial-stream-content';
+  let encodedExpected = encoder.encode(expected);
+  let received = '';
+  let encodedReceivedLength = 0;
+
+  // Accumulate response data from the service worker.  We do this as a loop
+  // to allow the browser the flexibility of rebuffering if it chooses.  We
+  // do expect to get the partial data within the test timeout period, though.
+  // The spec is a bit vague at the moment about this, but it seems reasonable
+  // that the browser should not stall the response stream when the service
+  // worker has only written a partial result, but not closed the stream.
+  while (encodedReceivedLength < encodedExpected.length) {
+    let chunk = await reader.read();
+    assert_false(chunk.done, 'partial body stream should not be closed yet');
+
+    encodedReceivedLength += chunk.value.length;
+    received += decoder.decode(chunk.value);
+  }
+
+  // Note, the spec may allow some re-buffering between the service worker
+  // and the outer intercepted fetch.  We could relax this exact chunk value
+  // match if necessary.  The goal, though, is to ensure the outer fetch is
+  // not completely blocked until the service worker body is closed.
+  assert_equals(received, expected,
+                'should receive partial content through service worker interception');
+
+  reg.active.postMessage('done');
+
+  await reader.closed;
+
+  }, 'respondWith() streams data to an intercepted fetch()');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
new file mode 100644
index 0000000..4544a9e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-readable-stream-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-readable-stream-chunk-iframe.html';
+
+promise_test(async t => {
+    var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+    add_completion_callback(() => reg.unregister());
+    await wait_for_state(t, reg.installing, 'activated');
+    let iframe = await with_iframe(SCOPE);
+    t.add_cleanup(() => iframe.remove());
+
+    let response = await iframe.contentWindow.fetch('body-stream');
+    assert_equals(await response.text(), 'chunk #1 chunk #2 chunk #3 chunk #4');
+}, 'Respond by chunks with a Response built from a ReadableStream');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
new file mode 100644
index 0000000..439e547
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+  'resources/fetch-event-respond-with-readable-stream-worker.js';
+const SCOPE =
+  'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+  return promise_test(async t => {
+    const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+    add_completion_callback(() => reg.unregister());
+    await wait_for_state(t, reg.installing, 'activated');
+    const iframe = await with_iframe(url);
+    const iwin = iframe.contentWindow;
+    t.add_cleanup(() => iframe.remove());
+    await callback(t, iwin);
+  }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?stream');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream');
+
+iframeTest(SCOPE + '?stream', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?stream&delay');
+  assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE + '?stream&delay', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const response = await iwin.fetch('?stream&use-fetch-stream');
+  assert_equals(await response.text(), 'PASS\n');
+}, 'Subresource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE + '?stream&use-fetch-stream', (t, iwin) => {
+  assert_equals(iwin.document.body.textContent, 'PASS\n');
+}, 'Main resource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+  const id = token();
+  let response = await iwin.fetch('?stream&observe-cancel&id=${id}');
+  response.body.cancel();
+
+  // Wait for a while to avoid a race between the cancel handling and the
+  // second fetch request.
+  await new Promise(r => step_timeout(r, 10));
+
+  response = await iwin.fetch('?stream&query-cancel&id=${id}');
+  assert_equals(await response.text(), 'cancelled');
+}, 'Cancellation in the page should be observable in the service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
new file mode 100644
index 0000000..2a44811
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with response body having invalid chunks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+  'resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js';
+const SCOPE =
+  'resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html';
+
+// Called by the iframe when it has the reader promise we should watch.
+var set_reader_promise;
+let reader_promise = new Promise(resolve => set_reader_promise = resolve);
+
+var set_fetch_promise;
+let fetch_promise = new Promise(resolve => set_fetch_promise = resolve);
+
+// This test creates an controlled iframe that makes a fetch request. The
+// service worker returns a response with a body stream containing an invalid
+// chunk.
+promise_test(async t => {
+    // Start off the process.
+    let errorConstructor;
+    await service_worker_unregister_and_register(t, WORKER, SCOPE)
+      .then(reg => {
+           add_completion_callback(() => reg.unregister());
+           return wait_for_state(t, reg.installing, 'activated');
+         })
+      .then(() => with_iframe(SCOPE))
+      .then(frame => {
+          t.add_cleanup(() => frame.remove())
+          errorConstructor = frame.contentWindow.TypeError;
+        });
+
+    await promise_rejects_js(t, errorConstructor, reader_promise,
+                             "read() should be rejected");
+    // Fetch should complete properly, because the reader error is caught in
+    // the subframe.  That is, there should be no errors _other_ than the
+    // reader!
+    return fetch_promise;
+  }, 'Response with a ReadableStream having non-Uint8Array chunks should be transferred as errored');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
new file mode 100644
index 0000000..31fd616
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var script =
+        'resources/fetch-event-respond-with-stops-propagation-worker.js';
+    var scope = 'resources/simple.html';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          t.add_cleanup(function() { frame.remove(); });
+          var channel = new MessageChannel();
+          var saw_message = new Promise(function(resolve) {
+              channel.port1.onmessage = function(e) { resolve(e.data); }
+            });
+          var worker = frame.contentWindow.navigator.serviceWorker.controller;
+
+          worker.postMessage({port: channel.port2}, [channel.port2]);
+          return saw_message;
+        })
+      .then(function(message) {
+          assert_equals(message, 'PASS');
+        })
+  }, 'respondWith() invokes stopImmediatePropagation()');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
new file mode 100644
index 0000000..d98fb22
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-event-throws-after-respond-with-iframe.html';
+    var workerscript = 'resources/respond-then-throw-worker.js';
+    var iframe;
+    return service_worker_unregister_and_register(t, workerscript, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated')
+            .then(() => reg.active);
+        })
+      .then(function(worker) {
+          var channel = new MessageChannel();
+          channel.port1.onmessage = function(e) {
+              assert_equals(e.data, 'SYNC', ' Should receive sync message.');
+              channel.port1.postMessage('ACK');
+            }
+          worker.postMessage({port: channel.port2}, [channel.port2]);
+          // The iframe will only be loaded after the sync is completed.
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+        assert_true(frame.contentDocument.body.innerHTML.includes("intercepted"));
+      })
+  }, 'Fetch event handler throws after a successful respondWith()');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
new file mode 100644
index 0000000..15a2e95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+function wait(ms) {
+  return new Promise(r => setTimeout(r, ms));
+}
+
+function reset() {
+  for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
+    iframe.remove();
+  }
+  return navigator.serviceWorker.getRegistrations().then(registrations => {
+    return Promise.all(registrations.map(r => r.unregister()));
+  }).then(() => caches.keys()).then(cacheKeys => {
+    return Promise.all(cacheKeys.map(c => caches.delete(c)));
+  });
+}
+
+add_completion_callback(reset);
+
+function regReady(reg) {
+  return new Promise((resolve, reject) => {
+    if (reg.active) {
+      resolve();
+      return;
+    }
+    const nextWorker = reg.waiting || reg.installing;
+
+    nextWorker.addEventListener('statechange', () => {
+      if (nextWorker.state == 'redundant') {
+        reject(Error(`Service worker failed to install`));
+        return;
+      }
+      if (nextWorker.state == 'activated') {
+        resolve();
+      }
+    });
+  });
+}
+
+function getCookies() {
+  return new Map(
+    document.cookie
+      .split(/;/g)
+      .map(c => c.trim().split('=').map(s => s.trim()))
+  );
+}
+
+function registerSwAndOpenFrame() {
+  return reset().then(() => navigator.serviceWorker.register(worker, {scope: 'resources/'}))
+    .then(reg => regReady(reg))
+    .then(() => with_iframe('resources/simple.html'));
+}
+
+function raceBroadcastAndCookie(channel, cookie) {
+  const initialCookie = getCookies().get(cookie);
+  let done = false;
+
+  return Promise.race([
+    new Promise(resolve => {
+      const bc = new BroadcastChannel(channel);
+      bc.onmessage = () => {
+        bc.close();
+        resolve('broadcast');
+      };
+    }),
+    (function checkCookie() {
+      // Stop polling if the broadcast channel won
+      if (done == true) return;
+      if (getCookies().get(cookie) != initialCookie) return 'cookie';
+
+      return wait(200).then(checkCookie);
+    }())
+  ]).then(val => {
+    done = true;
+    return val;
+  });
+}
+
+promise_test(() => {
+  return Notification.requestPermission().then(permission => {
+    if (permission != "granted") {
+      throw Error('You must allow notifications for this origin before running this test.');
+    }
+    return registerSwAndOpenFrame();
+  }).then(iframe => {
+    return Promise.resolve().then(() => {
+      // In this test, the service worker will ping the 'icon-request' channel
+      // if it intercepts a request for 'notification_icon.py'. If the request
+      // reaches the server it sets the 'notification' cookie to the value given
+      // in the URL. "raceBroadcastAndCookie" monitors both and returns which
+      // happens first.
+      const race = raceBroadcastAndCookie('icon-request', 'notification');
+      const notification = new iframe.contentWindow.Notification('test', {
+        icon: `notification_icon.py?set-cookie-notification=${Math.random()}`
+      });
+      notification.close();
+
+      return race.then(winner => {
+        assert_equals(winner, 'broadcast', 'The service worker intercepted the from-window notification icon request');
+      });
+    }).then(() => {
+      // Similar race to above, but this time the service worker requests the
+      // notification.
+      const race = raceBroadcastAndCookie('icon-request', 'notification');
+      iframe.contentWindow.fetch(`show-notification?set-cookie-notification=${Math.random()}`);
+
+      return race.then(winner => {
+        assert_equals(winner, 'broadcast', 'The service worker intercepted the from-service-worker notification icon request');
+      });
+    })
+  });
+}, `Notification requests intercepted both from window and SW`);
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html
new file mode 100644
index 0000000..0b52b18
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+async function registerSwAndOpenFrame(t) {
+  const registration = await navigator.serviceWorker.register(
+      worker, { scope: 'resources/' });
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const frame = await with_iframe('resources/simple.html');
+  t.add_cleanup(() => frame.remove());
+  return frame;
+}
+
+async function deleteCaches() {
+  const cacheKeys = await caches.keys();
+  await Promise.all(cacheKeys.map(c => caches.delete(c)));
+}
+
+promise_test(async t => {
+  t.add_cleanup(deleteCaches);
+
+  const iframe = await registerSwAndOpenFrame(t);
+  const fetchText =
+      await iframe.contentWindow.fetch('sample.txt').then(r => r.text());
+
+  const cache = await iframe.contentWindow.caches.open('test');
+  await cache.add('sample.txt');
+
+  const response = await cache.match('sample.txt');
+  const cacheText = await (response ? response.text() : 'cache match failed');
+  assert_equals(fetchText, 'intercepted', 'fetch intercepted');
+  assert_equals(cacheText, 'intercepted', 'cache.add intercepted');
+}, 'Service worker intercepts requests from window');
+
+promise_test(async t => {
+  const iframe = await registerSwAndOpenFrame(t);
+  const [fetchText, cacheText] = await Promise.all([
+    iframe.contentWindow.fetch('sample.txt-inner-fetch').then(r => r.text()),
+    iframe.contentWindow.fetch('sample.txt-inner-cache').then(r => r.text())
+  ]);
+  assert_equals(fetchText, 'Hello world\n', 'fetch within SW not intercepted');
+  assert_equals(cacheText, 'Hello world\n',
+                'cache.add within SW not intercepted');
+}, 'Service worker does not intercept fetch/cache requests within service ' +
+   'worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html
new file mode 100644
index 0000000..5cd381e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+const method = 'POST';
+const duplex = 'half';
+
+function createBody(t) {
+  const rs = new ReadableStream({start(c) {
+    c.enqueue('i a');
+    c.enqueue('m the request');
+    step_timeout(t.step_func(() => {
+      c.enqueue(' body');
+      c.close();
+    }, 10));
+  }});
+  return rs.pipeThrough(new TextEncoderStream());
+}
+
+promise_test(async t => {
+  const scope = 'resources/';
+  const registration =
+    await service_worker_unregister_and_register(t, worker, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // This will happen after all other tests
+  promise_test(t => {
+    return registration.unregister();
+  }, 'restore global state');
+}, 'global setup');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+  const body = createBody(t);
+  // Set page_url to "?ignore" so the service worker falls back to network
+  // for the main resource request, and add a suffix to avoid colliding
+  // with other tests.
+  const page_url = `resources/simple.html?ignore&id=${token()}`;
+  const frame = await with_iframe(page_url);
+  t.add_cleanup(() => { frame.remove(); });
+  const response = await frame.contentWindow.fetch('simple.html?request-body', {
+    method, body, duplex});
+  assert_equals(response.status, 200, 'status');
+  const text = await response.text();
+  assert_equals(text, 'i am the request body', 'body');
+}, 'The streaming request body is readable in the service worker.');
+
+// Network fallback
+promise_test(async t => {
+  const body = createBody(t);
+  // Set page_url to "?ignore" so the service worker falls back to network
+  // for the main resource request, and add a suffix to avoid colliding
+  // with other tests.
+  const page_url = `resources/simple.html?ignore&id=${token()}`;
+  const frame = await with_iframe(page_url);
+  t.add_cleanup(() => { frame.remove(); });
+  // Add "?ignore" so that the service worker falls back to
+  // echo-content.h2.py.
+  const echo_url = '/fetch/api/resources/echo-content.h2.py?ignore';
+  const response =
+    await frame.contentWindow.fetch(echo_url, { method, body, duplex});
+  assert_equals(response.status, 200, 'status');
+  const text = await response.text();
+  assert_equals(text, 'i am the request body', 'body');
+}, 'Network fallback for streaming upload.');
+
+// When the streaming body is used in the service worker, network fallback
+// fails.
+promise_test(async t => {
+  const body = createBody(t);
+  // Set page_url to "?ignore" so the service worker falls back to network
+  // for the main resource request, and add a suffix to avoid colliding
+  // with other tests.
+  const page_url = `resources/simple.html?ignore&id=${token()}`;
+  const frame = await with_iframe(page_url);
+  t.add_cleanup(() => { frame.remove(); });
+  const echo_url = '/fetch/api/resources/echo-content.h2.py?use-and-ignore';
+  const w = frame.contentWindow;
+  await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+    method, body, duplex}));
+}, 'When the streaming request body is used, network fallback fails.');
+
+// When the streaming body is used by clone() in the service worker, network
+// fallback succeeds.
+promise_test(async t => {
+  const body = createBody(t);
+  // Set page_url to "?ignore" so the service worker falls back to network
+  // for the main resource request, and add a suffix to avoid colliding
+  // with other tests.
+  const page_url = `resources/simple.html?ignore&id=${token()}`;
+  const frame = await with_iframe(page_url);
+  t.add_cleanup(() => { frame.remove(); });
+  // Add "?clone-and-ignore" so that the service worker falls back to
+  // echo-content.h2.py.
+  const echo_url = '/fetch/api/resources/echo-content.h2.py?clone-and-ignore';
+  const response = await frame.contentWindow.fetch(echo_url, {
+    method, body, duplex});
+  assert_equals(response.status, 200, 'status');
+  const text = await response.text();
+  assert_equals(text, 'i am the request body', 'body');
+}, 'Running clone() in the service worker does not prevent network fallback.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html
new file mode 100644
index 0000000..ce53f3c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html
@@ -0,0 +1,1000 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+function wait(ms) {
+    return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async t => {
+    const scope = 'resources/';
+    const registration =
+        await service_worker_unregister_and_register(t, worker, scope);
+    await wait_for_state(t, registration.installing, 'activated');
+
+    // This will happen after all other tests
+    promise_test(t => {
+        return registration.unregister();
+      }, 'restore global state');
+  }, 'global setup');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?headers';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          const headers = JSON.parse(frame.contentDocument.body.textContent);
+          const header_names = {};
+          for (const [name, value] of headers) {
+            header_names[name] = true;
+          }
+
+          assert_true(
+            header_names.hasOwnProperty('accept'),
+            'request includes "Accept" header as inserted by Fetch'
+          );
+        });
+  }, 'Service Worker headers in the request of a fetch event');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?string';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'Test string',
+            'Service Worker should respond to fetch with a test string');
+          assert_equals(
+            frame.contentDocument.contentType,
+            'text/plain',
+            'The content type of the response created with a string should be text/plain');
+          assert_equals(
+            frame.contentDocument.characterSet,
+            'UTF-8',
+            'The character set of the response created with a string should be UTF-8');
+        });
+  }, 'Service Worker responds to fetch event with string');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?string';
+    var frame;
+    return with_iframe(page_url)
+      .then(function(f) {
+        frame = f;
+        t.add_cleanup(() => { frame.remove(); });
+        return frame.contentWindow.fetch(page_url + "#foo")
+      })
+      .then(function(response) { return response.text() })
+      .then(function(text) {
+          assert_equals(
+            text,
+            'Test string',
+            'Service Worker should respond to fetch with a test string');
+        });
+  }, 'Service Worker responds to fetch event using request fragment with string');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?blob';
+    return with_iframe(page_url)
+      .then(frame => {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'Test blob',
+            'Service Worker should respond to fetch with a test string');
+        });
+  }, 'Service Worker responds to fetch event with blob body');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?referrer';
+      return with_iframe(page_url)
+      .then(frame => {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'Referrer: ' + document.location.href,
+            'Service Worker should respond to fetch with the referrer URL');
+        });
+  }, 'Service Worker responds to fetch event with the referrer URL');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?clientId';
+    var frame;
+    return with_iframe(page_url)
+      .then(function(f) {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'Client ID Not Found',
+            'Service Worker should respond to fetch with a client id');
+          return frame.contentWindow.fetch('resources/other.html?clientId');
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          assert_equals(
+            response_text.substr(0, 15),
+            'Client ID Found',
+            'Service Worker should respond to fetch with an existing client id');
+        });
+  }, 'Service Worker responds to fetch event with an existing client id');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?resultingClientId';
+    const expected_found = 'Resulting Client ID Found';
+    const expected_not_found = 'Resulting Client ID Not Found';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent.substr(0, expected_found.length),
+            expected_found,
+            'Service Worker should respond with an existing resulting client id for non-subresource requests');
+          return frame.contentWindow.fetch('resources/other.html?resultingClientId');
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          assert_equals(
+            response_text.substr(0),
+            expected_not_found,
+            'Service Worker should respond with an empty resulting client id for subresource requests');
+        });
+  }, 'Service Worker responds to fetch event with the correct resulting client id');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?ignore';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentDocument.body.textContent,
+                        'Here\'s a simple html file.\n',
+                        'Response should come from fallback to native fetch');
+        });
+  }, 'Service Worker does not respond to fetch event');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?null';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentDocument.body.textContent,
+                        '',
+                        'Response should be the empty string');
+        });
+  }, 'Service Worker responds to fetch event with null response body');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?fetch';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentDocument.body.textContent,
+                        'Here\'s an other html file.\n',
+                        'Response should come from fetched other file');
+        });
+  }, 'Service Worker fetches other file in fetch event');
+
+// Creates a form and an iframe and does a form submission that navigates the
+// frame to |action_url|. Returns the frame after navigation.
+function submit_form(action_url) {
+  return new Promise(resolve => {
+      const frame = document.createElement('iframe');
+      frame.name = 'post-frame';
+      document.body.appendChild(frame);
+      const form = document.createElement('form');
+      form.target = frame.name;
+      form.action = action_url;
+      form.method = 'post';
+      const input1 = document.createElement('input');
+      input1.type = 'text';
+      input1.value = 'testValue1';
+      input1.name = 'testName1'
+      form.appendChild(input1);
+      const input2 = document.createElement('input');
+      input2.type = 'text';
+      input2.value = 'testValue2';
+      input2.name = 'testName2'
+      form.appendChild(input2);
+      document.body.appendChild(form);
+      frame.onload = function() {
+        form.remove();
+        resolve(frame);
+      };
+      form.submit();
+    });
+}
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?form-post';
+    return submit_form(page_url)
+      .then(frame => {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentDocument.body.textContent,
+                        'POST:application/x-www-form-urlencoded:' +
+                        'testName1=testValue1&testName2=testValue2');
+        });
+  }, 'Service Worker responds to fetch event with POST form');
+
+promise_test(t => {
+    // Add '?ignore' so the service worker falls back to network.
+    const page_url = 'resources/echo-content.py?ignore';
+    return submit_form(page_url)
+      .then(frame => {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentDocument.body.textContent,
+                        'testName1=testValue1&testName2=testValue2');
+        });
+  }, 'Service Worker falls back to network in fetch event with POST form');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?multiple-respond-with';
+    return with_iframe(page_url)
+      .then(frame => {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            '(0)(1)[InvalidStateError](2)[InvalidStateError]',
+            'Multiple calls of respondWith must throw InvalidStateErrors.');
+        });
+  }, 'Multiple calls of respondWith must throw InvalidStateErrors');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?used-check';
+    var first_frame;
+    return with_iframe(page_url)
+      .then(function(frame) {
+          assert_equals(frame.contentDocument.body.textContent,
+                        'Here\'s an other html file.\n',
+                        'Response should come from fetched other file');
+          first_frame = frame;
+          t.add_cleanup(() => { first_frame.remove(); });
+          return with_iframe(page_url);
+        })
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          // When we access to the page_url in the second time, the content of the
+          // response is generated inside the ServiceWorker. The body contains
+          // the value of bodyUsed of the first response which is already
+          // consumed by FetchEvent.respondWith method.
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'bodyUsed: true',
+            'event.respondWith must set the used flag.');
+        });
+  }, 'Service Worker event.respondWith must set the used flag');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?fragment-check';
+    var fragment = '#/some/fragment';
+    var first_frame;
+    return with_iframe(page_url + fragment)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'Fragment Found :' + fragment,
+            'Service worker should expose URL fragments in request.');
+        });
+  }, 'Service Worker should expose FetchEvent URL fragments.');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?cache';
+    var frame;
+    var cacheTypes = [
+      undefined, 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'
+    ];
+    return with_iframe(page_url)
+      .then(function(f) {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(frame.contentWindow.document.body.textContent, 'default');
+          var tests = cacheTypes.map(function(type) {
+            return new Promise(function(resolve, reject) {
+                var init = {cache: type};
+                if (type === 'only-if-cached') {
+                  // For privacy reasons, for the time being, only-if-cached
+                  // requires the mode to be same-origin.
+                  init.mode = 'same-origin';
+                }
+                return frame.contentWindow.fetch(page_url + '=' + type, init)
+                  .then(function(response) { return response.text(); })
+                  .then(function(response_text) {
+                      var expected = (type === undefined) ? 'default' : type;
+                      assert_equals(response_text, expected,
+                                    'Service Worker should respond to fetch with the correct type');
+                    })
+                  .then(resolve)
+                  .catch(reject);
+              });
+          });
+          return Promise.all(tests);
+        })
+      .then(function() {
+          return new Promise(function(resolve, reject) {
+            frame.addEventListener('load', function onLoad() {
+              frame.removeEventListener('load', onLoad);
+              try {
+                assert_equals(frame.contentWindow.document.body.textContent,
+                              'no-cache');
+                resolve();
+              } catch (e) {
+                reject(e);
+              }
+            });
+            frame.contentWindow.location.reload();
+          });
+        });
+  }, 'Service Worker responds to fetch event with the correct cache types');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?eventsource';
+    var frame;
+
+    function test_eventsource(opts) {
+      return new Promise(function(resolve, reject) {
+        var eventSource = new frame.contentWindow.EventSource(page_url, opts);
+        eventSource.addEventListener('message', function(msg) {
+          eventSource.close();
+          try {
+            var data = JSON.parse(msg.data);
+            assert_equals(data.mode, 'cors',
+                          'EventSource should make CORS requests.');
+            assert_equals(data.cache, 'no-store',
+                          'EventSource should bypass the http cache.');
+            var expectedCredentials = opts.withCredentials ? 'include'
+                                                           : 'same-origin';
+            assert_equals(data.credentials, expectedCredentials,
+                          'EventSource should pass correct credentials mode.');
+            resolve();
+          } catch (e) {
+            reject(e);
+          }
+        });
+        eventSource.addEventListener('error', function(e) {
+          eventSource.close();
+          reject('The EventSource fired an error event.');
+        });
+      });
+    }
+
+    return with_iframe(page_url)
+      .then(function(f) {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return test_eventsource({ withCredentials: false });
+        })
+      .then(function() {
+          return test_eventsource({ withCredentials: true });
+        });
+  }, 'Service Worker should intercept EventSource');
+
+promise_test(t => {
+    const page_url = 'resources/simple.html?integrity';
+    var frame;
+    var integrity_metadata = 'gs0nqru8KbsrIt5YToQqS9fYao4GQJXtcId610g7cCU=';
+
+    return with_iframe(page_url)
+      .then(function(f) {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          // A request has associated integrity metadata (a string).
+          // Unless stated otherwise, it is the empty string.
+          assert_equals(
+            frame.contentDocument.body.textContent, '');
+
+          return frame.contentWindow.fetch(page_url, {'integrity': integrity_metadata});
+        })
+      .then(response => {
+          return response.text();
+        })
+      .then(response_text => {
+          assert_equals(response_text, integrity_metadata, 'integrity');
+        });
+  }, 'Service Worker responds to fetch event with the correct integrity_metadata');
+
+// Test that the service worker can read FetchEvent#body when it is a string.
+// It responds with request body it read.
+promise_test(t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/simple.html?ignore-for-request-body-string';
+    let frame;
+
+    return with_iframe(page_url)
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          return frame.contentWindow.fetch('simple.html?request-body', {
+              method: 'POST',
+              body: 'i am the request body'
+            });
+        })
+      .then(response => {
+          return response.text();
+        })
+      .then(response_text => {
+          assert_equals(response_text, 'i am the request body');
+        });
+  }, 'FetchEvent#body is a string');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+    const rs = new ReadableStream({start(c) {
+      c.enqueue('i a');
+      c.enqueue('m the request');
+      step_timeout(t.step_func(() => {
+        c.enqueue(' body');
+        c.close();
+      }, 10));
+    }});
+
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = `resources/simple.html?ignore&id=${token()}`;
+
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    const res = await frame.contentWindow.fetch('simple.html?request-body', {
+      method: 'POST',
+      body: rs.pipeThrough(new TextEncoderStream()),
+      duplex: 'half',
+    });
+    assert_equals(await res.text(), 'i am the request body');
+  }, 'FetchEvent#body is a ReadableStream');
+
+// Test that the request body is sent to network upon network fallback,
+// for a string body.
+promise_test(t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/?ignore-for-request-body-fallback-string';
+    let frame;
+
+    return with_iframe(page_url)
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          // Add "?ignore" so the service worker falls back to echo-content.py.
+          const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+          return frame.contentWindow.fetch(echo_url, {
+              method: 'POST',
+              body: 'i am the request body'
+            });
+        })
+      .then(response => {
+          return response.text();
+        })
+      .then(response_text => {
+          assert_equals(
+              response_text,
+              'i am the request body',
+              'the network fallback request should include the request body');
+        });
+  }, 'FetchEvent#body is a string and is passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback,
+// for a ReadableStream body.
+promise_test(async t => {
+    const rs = new ReadableStream({start(c) {
+      c.enqueue('i a');
+      c.enqueue('m the request');
+      t.step_timeout(t.step_func(() => {
+        c.enqueue(' body');
+        c.close();
+      }, 10));
+    }});
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/?ignore-for-request-body-fallback-string';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    // Add "?ignore" so the service worker falls back to echo-content.py.
+    const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+    const w = frame.contentWindow;
+    await promise_rejects_js(t, w.TypeError,  w.fetch(echo_url, {
+        method: 'POST',
+        body: rs
+    }));
+  }, 'FetchEvent#body is a none Uint8Array ReadableStream and is passed to a service worker');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used in the service worker, for a string body.
+promise_test(async t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    // Add "?use-and-ignore" so the service worker falls back to echo-content.py.
+    const echo_url = '/fetch/api/resources/echo-content.py?use-and-ignore';
+    const response = await frame.contentWindow.fetch(echo_url, {
+        method: 'POST',
+        body: 'i am the request body'
+    });
+    const text = await response.text();
+    assert_equals(
+        text,
+        'i am the request body',
+        'the network fallback request should include the request body');
+  }, 'FetchEvent#body is a string, used and passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used by clone() in the service worker, for a string body.
+promise_test(async t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    // Add "?clone-and-ignore" so the service worker falls back to
+    // echo-content.py.
+    const echo_url = '/fetch/api/resources/echo-content.py?clone-and-ignore';
+    const response = await frame.contentWindow.fetch(echo_url, {
+        method: 'POST',
+        body: 'i am the request body'
+    });
+    const text = await response.text();
+    assert_equals(
+        text,
+        'i am the request body',
+        'the network fallback request should include the request body');
+  }, 'FetchEvent#body is a string, cloned and passed to network fallback');
+
+// Test that the service worker can read FetchEvent#body when it is a blob.
+// It responds with request body it read.
+promise_test(t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/simple.html?ignore-for-request-body-blob';
+    let frame;
+
+    return with_iframe(page_url)
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+          return frame.contentWindow.fetch('simple.html?request-body', {
+              method: 'POST',
+              body: blob
+            });
+        })
+      .then(response => {
+          return response.text();
+        })
+      .then(response_text => {
+          assert_equals(response_text, 'it\'s me the blob and more blob!');
+        });
+  }, 'FetchEvent#body is a blob');
+
+// Test that the request body is sent to network upon network fallback,
+// for a blob body.
+promise_test(t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/simple.html?ignore-for-request-body-fallback-blob';
+    let frame;
+
+    return with_iframe(page_url)
+      .then(f => {
+          frame = f;
+          t.add_cleanup(() => { frame.remove(); });
+          const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+          // Add "?ignore" so the service worker falls back to echo-content.py.
+          const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+          return frame.contentWindow.fetch(echo_url, {
+              method: 'POST',
+              body: blob
+            });
+        })
+      .then(response => {
+          return response.text();
+        })
+      .then(response_text => {
+          assert_equals(
+              response_text,
+              'it\'s me the blob and more blob!',
+              'the network fallback request should include the request body');
+        });
+  }, 'FetchEvent#body is a blob and is passed to network fallback');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?keepalive';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent, 'false');
+    const response = await frame.contentWindow.fetch(page_url, {keepalive: true});
+    const text = await response.text();
+    assert_equals(text, 'true');
+  }, 'Service Worker responds to fetch event with the correct keepalive value');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isReloadNavigation';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.location.reload();
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = true');
+  }, 'FetchEvent#request.isReloadNavigation is true (location.reload())');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isReloadNavigation';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(0);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = true');
+  }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isReloadNavigation';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      const form = frame.contentDocument.createElement('form');
+      form.method = 'POST';
+      form.name = 'form';
+      form.action = new Request(page_url).url;
+      frame.contentDocument.body.appendChild(form);
+      form.submit();
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = POST, isReloadNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.location.reload();
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = POST, isReloadNavigation = true');
+  }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isReloadNavigation';
+    const anotherUrl = new Request('resources/simple.html').url;
+    let frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = false');
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = anotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(0);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isReloadNavigation = true');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(1);
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+  }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = anotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+  }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-1))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const frame = await with_iframe(anotherUrl);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = page_url;
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+  }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(1))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const frame =  await with_iframe(anotherUrl);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = page_url;
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(0);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+  }, 'FetchEvent#request.isHistoryNavigation is false (with history.go(0))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const frame = await with_iframe(anotherUrl);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = page_url;
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.location.reload();
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+  }, 'FetchEvent#request.isHistoryNavigation is false (with location.reload)');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = anotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = oneAnotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-2);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+  }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-2))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+    const frame = await with_iframe(anotherUrl);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = oneAnotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = page_url;
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-2);
+    });
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(2);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = true');
+  }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(2))');
+
+promise_test(async (t) => {
+    const page_url = 'resources/simple.html?isHistoryNavigation';
+    const anotherUrl = new Request('resources/simple.html?ignore').url;
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = GET, isHistoryNavigation = false');
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      const form = frame.contentDocument.createElement('form');
+      form.method = 'POST';
+      form.name = 'form';
+      form.action = new Request(page_url).url;
+      frame.contentDocument.body.appendChild(form);
+      form.submit();
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = POST, isHistoryNavigation = false');
+    // Use step_timeout(0) to ensure the history entry is created for Blink
+    // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.src = anotherUrl;
+    });
+    assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+    await wait(0);
+    await new Promise((resolve) => {
+      frame.addEventListener('load', resolve);
+      frame.contentWindow.history.go(-1);
+    });
+    assert_equals(frame.contentDocument.body.textContent,
+                  'method = POST, isHistoryNavigation = true');
+  }, 'FetchEvent#request.isHistoryNavigation is true (POST + history.go(-1))');
+
+// When service worker responds with a Response, no XHR upload progress
+// events are delivered.
+promise_test(async t => {
+    const page_url = 'resources/simple.html?ignore-for-request-body-string';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+
+    const xhr = new frame.contentWindow.XMLHttpRequest();
+    xhr.open('POST', 'simple.html?request-body');
+    xhr.upload.addEventListener('progress', t.unreached_func('progress'));
+    xhr.upload.addEventListener('error', t.unreached_func('error'));
+    xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+    xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+    xhr.upload.addEventListener('load', t.unreached_func('load'));
+    xhr.upload.addEventListener('loadend', t.unreached_func('loadend'));
+    xhr.send('i am the request body');
+
+    await new Promise((resolve) => xhr.addEventListener('load', resolve));
+  }, 'XHR upload progress events for response coming from SW');
+
+// Upload progress events should be delivered for the network fallback case.
+promise_test(async t => {
+    const page_url = 'resources/simple.html?ignore-for-request-body-string';
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+
+    let progress = false;
+    let load = false;
+    let loadend = false;
+
+    const xhr = new frame.contentWindow.XMLHttpRequest();
+    xhr.open('POST', '/fetch/api/resources/echo-content.py?ignore');
+    xhr.upload.addEventListener('progress', () => progress = true);
+    xhr.upload.addEventListener('error', t.unreached_func('error'));
+    xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+    xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+    xhr.upload.addEventListener('load', () => load = true);
+    xhr.upload.addEventListener('loadend', () => loadend = true);
+    xhr.send('i am the request body');
+
+    await new Promise((resolve) => xhr.addEventListener('load', resolve));
+    assert_true(progress, 'progress');
+    assert_true(load, 'load');
+    assert_true(loadend, 'loadend');
+  }, 'XHR upload progress events for network fallback');
+
+promise_test(async t => {
+    // Set page_url to "?ignore" so the service worker falls back to network
+    // for the main resource request, and add a suffix to avoid colliding
+    // with other tests.
+    const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+    const frame = await with_iframe(page_url);
+    t.add_cleanup(() => { frame.remove(); });
+    // Add "?clone-and-ignore" so the service worker falls back to
+    // echo-content.py.
+    const echo_url = '/fetch/api/resources/echo-content.py?status=421';
+    const response = await frame.contentWindow.fetch(echo_url, {
+        method: 'POST',
+        body: 'text body'
+    });
+    assert_equals(response.status, 421);
+    const text = await response.text();
+    assert_equals(
+        text,
+        'text body. Request was sent 1 times.',
+        'the network fallback request should include the request body');
+  }, 'Fetch with POST with text on sw 421 response should not be retried.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html
new file mode 100644
index 0000000..a33309f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch for the frame loading.</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-rewrite-worker.js';
+var path = base_path() + 'resources/fetch-access-control.py';
+var host_info = get_host_info();
+
+function getLoadedObject(win, contentFunc, closeFunc) {
+  return new Promise(function(resolve) {
+      function done(contentString) {
+        var result = null;
+        // fetch-access-control.py returns a string like "report( <json> )".
+        // Eval the returned string with a report functionto get the json
+        // object.
+        try {
+          function report(obj) { result = obj };
+          eval(contentString);
+        } catch(e) {
+          // just resolve null if we get unexpected page content
+        }
+        closeFunc(win);
+        resolve(result);
+      }
+
+      // We can't catch the network error on window. So we use the timer.
+      var timeout = setTimeout(function() {
+          // Failure pages are considered cross-origin in some browsers.  This
+          // means you cannot even .resolve() the window because the check for
+          // the .then property will throw.  Instead, treat cross-origin
+          // failure pages as the empty string which will fail to parse as the
+          // expected json result.
+          var content = '';
+          try {
+            content = contentFunc(win);
+          } catch(e) {
+            // use default empty string for cross-domain window
+          }
+          done(content);
+        }, 10000);
+
+      win.onload = function() {
+          clearTimeout(timeout);
+          let content = '';
+          try {
+            content = contentFunc(win);
+          } catch(e) {
+            // use default empty string for cross-domain window (see above)
+          }
+          done(content);
+        };
+    });
+}
+
+function getLoadedFrameAsObject(frame) {
+  return getLoadedObject(frame, function(f) {
+      return f.contentDocument.body.textContent;
+    }, function(f) {
+      f.parentNode.removeChild(f);
+    });
+}
+
+function getLoadedWindowAsObject(win) {
+  return getLoadedObject(win, function(w) {
+      return w.document.body.textContent
+    }, function(w) {
+      w.close();
+    });
+}
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/frame-basic';
+    var frame;
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          frame = document.createElement('iframe');
+          frame.src =
+            scope + '?url=' +
+            encodeURIComponent(host_info['HTTPS_ORIGIN'] + path);
+          document.body.appendChild(frame);
+          return getLoadedFrameAsObject(frame);
+        })
+      .then(function(result) {
+          assert_equals(
+            result.jsonpResult,
+            'success',
+            'Basic type response could be loaded in the iframe.');
+          frame.remove();
+        });
+  }, 'Basic type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/frame-cors';
+    var frame;
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          frame = document.createElement('iframe');
+          frame.src =
+            scope + '?mode=cors&url=' +
+            encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+                               '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+                               '&ACACredentials=true');
+          document.body.appendChild(frame);
+          return getLoadedFrameAsObject(frame);
+        })
+      .then(function(result) {
+          assert_equals(
+            result.jsonpResult,
+            'success',
+            'CORS type response could be loaded in the iframe.');
+          frame.remove();
+        });
+  }, 'CORS type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/frame-opaque';
+    var frame;
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          frame = document.createElement('iframe');
+          frame.src =
+            scope + '?mode=no-cors&url=' +
+            encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path);
+          document.body.appendChild(frame);
+          return getLoadedFrameAsObject(frame);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            null,
+            'Opaque type response could not be loaded in the iframe.');
+          frame.remove();
+        });
+  }, 'Opaque type response could not be loaded in the iframe.');
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/window-basic';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          var win = window.open(
+            scope + '?url=' +
+            encodeURIComponent(host_info['HTTPS_ORIGIN'] + path));
+          return getLoadedWindowAsObject(win);
+        })
+      .then(function(result) {
+          assert_equals(
+            result.jsonpResult,
+            'success',
+            'Basic type response could be loaded in the new window.');
+        });
+  }, 'Basic type response could be loaded in the new window.');
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/window-cors';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          var win = window.open(
+            scope + '?mode=cors&url=' +
+            encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+                               '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+                               '&ACACredentials=true'));
+          return getLoadedWindowAsObject(win);
+        })
+      .then(function(result) {
+          assert_equals(
+            result.jsonpResult,
+            'success',
+            'CORS type response could be loaded in the new window.');
+        });
+  }, 'CORS type response could be loaded in the new window.');
+
+promise_test(function(t) {
+    var scope = 'resources/fetch-frame-resource/window-opaque';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          var win = window.open(
+            scope + '?mode=no-cors&url=' +
+            encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path));
+          return getLoadedWindowAsObject(win);
+        })
+      .then(function(result) {
+          assert_equals(
+            result,
+            null,
+            'Opaque type response could not be loaded in the new window.');
+        });
+  }, 'Opaque type response could not be loaded in the new window.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html
new file mode 100644
index 0000000..1f4813c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker: Visibility of headers during fetch.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+  var worker = 'resources/fetch-rewrite-worker.js';
+  var path = base_path() + 'resources/fetch-access-control.py';
+  var host_info = get_host_info();
+  var frame;
+
+  promise_test(function(t) {
+    var scope = 'resources/fetch-header-visibility-iframe.html';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+        t.add_cleanup(function() {
+            return service_worker_unregister(t, scope);
+          });
+
+        return wait_for_state(t, reg.installing, 'activated');
+      })
+      .then(function() {
+        frame = document.createElement('iframe');
+        frame.src = scope;
+        document.body.appendChild(frame);
+
+        // Resolve a promise when we recieve 2 success messages
+        return new Promise(function(resolve, reject) {
+          var remaining = 4;
+          function onMessage(e) {
+            if (e.data == 'PASS') {
+              remaining--;
+              if (remaining == 0) {
+                resolve();
+              } else {
+                return;
+              }
+            } else {
+              reject(e.data);
+            }
+
+            window.removeEventListener('message', onMessage);
+          }
+          window.addEventListener('message', onMessage);
+        });
+      })
+      .then(function(result) {
+        frame.remove();
+      });
+  }, 'Visibility of defaulted headers during interception');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
new file mode 100644
index 0000000..0e8fa93
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+    var host_info = get_host_info();
+    window.addEventListener('message', t.step_func(on_message), false);
+    with_iframe(
+      host_info['HTTPS_ORIGIN'] + base_path() +
+      'resources/fetch-mixed-content-iframe.html?target=inscope');
+    function on_message(e) {
+      assert_equals(e.data.results, 'finish');
+      t.done();
+    }
+  }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
new file mode 100644
index 0000000..391dc5d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+    var host_info = get_host_info();
+    window.addEventListener('message', t.step_func(on_message), false);
+    with_iframe(
+      host_info['HTTPS_ORIGIN'] + base_path() +
+      'resources/fetch-mixed-content-iframe.html?target=outscope');
+    function on_message(e) {
+      assert_equals(e.data.results, 'finish');
+      t.done();
+    }
+  }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html
new file mode 100644
index 0000000..467a66c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<title>Service Worker: CSS's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+const SCOPE = 'resources/fetch-request-css-base-url-iframe.html';
+const SCRIPT = 'resources/fetch-request-css-base-url-worker.js';
+let worker;
+
+var signalMessage;
+function getNextMessage() {
+  return new Promise(resolve => { signalMessage = resolve; });
+}
+
+promise_test(async (t) => {
+  const registration = await service_worker_unregister_and_register(
+      t, SCRIPT, SCOPE);
+  worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Creates a test concerning the base URL of a stylesheet. It loads a
+// stylesheet from a controlled page. The stylesheet makes a subresource
+// request for an image. The service worker messages back the details of the
+// image request in order to test the base URL.
+//
+// The request URL for the stylesheet is under "resources/request-url-path/".
+// The service worker may respond in a way such that the response URL is
+// different to the request URL.
+function base_url_test(params) {
+  promise_test(async (t) => {
+    let frame;
+    t.add_cleanup(() => {
+      if (frame)
+        frame.remove();
+    });
+
+    // Ask the service worker to message this page once it gets the request
+    // for the image.
+    let channel = new MessageChannel();
+    const sawPong = getNextMessage();
+    channel.port1.onmessage = (event) => {
+      signalMessage(event.data);
+    };
+    worker.postMessage({port:channel.port2},[channel.port2]);
+
+    // It sends a pong back immediately. This ping/pong protocol helps deflake
+    // the test for browsers where message/fetch ordering isn't guaranteed.
+    assert_equals('pong', await sawPong);
+
+    // Load the frame which will load the stylesheet that makes the image
+    // request.
+    const sawResult = getNextMessage();
+    frame = await with_iframe(params.framePath);
+    const result = await sawResult;
+
+    // Test the image request.
+    const base = new URL('.', document.location).href;
+    assert_equals(result.url,
+                  base + params.expectImageRequestPath,
+                  'request');
+    assert_equals(result.referrer,
+                  base + params.expectImageRequestReferrer,
+                  'referrer');
+  }, params.description);
+}
+
+const cssFile = 'fetch-request-css-base-url-style.css';
+
+base_url_test({
+  framePath: SCOPE + '?fetch',
+  expectImageRequestPath: 'resources/sample.png',
+  expectImageRequestReferrer: `resources/${cssFile}?fetch`,
+  description: 'base URL when service worker does respondWith(fetch(responseUrl)).'});
+
+base_url_test({
+  framePath: SCOPE + '?newResponse',
+  expectImageRequestPath: 'resources/request-url-path/sample.png',
+  expectImageRequestReferrer: `resources/request-url-path/${cssFile}?newResponse`,
+  description: 'base URL when service worker does respondWith(new Response()).'});
+
+// Cleanup step: this must be the last promise_test.
+promise_test(async (t) => {
+  return service_worker_unregister(t, SCOPE);
+}, 'cleanup global state');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
new file mode 100644
index 0000000..d9c1c7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Service Worker: Cross-origin CSS files fetched via SW.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function getElementColorInFrame(frame, id) {
+  var element = frame.contentDocument.getElementById(id);
+  var style = frame.contentWindow.getComputedStyle(element, '');
+  return style['color'];
+}
+
+promise_test(async t => {
+  var SCOPE =
+      'resources/fetch-request-css-cross-origin';
+  var SCRIPT =
+      'resources/fetch-request-css-cross-origin-worker.js';
+  let registration = await service_worker_unregister_and_register(
+    t, SCRIPT, SCOPE);
+  promise_test(async t => {
+    await registration.unregister();
+  }, 'cleanup global state');
+
+  await wait_for_state(t, registration.installing, 'activated');
+}, 'setup global state');
+
+promise_test(async t => {
+  const EXPECTED_COLOR = 'rgb(0, 0, 255)';
+  const PAGE =
+      'resources/fetch-request-css-cross-origin-mime-check-iframe.html';
+
+  const f = await with_iframe(PAGE);
+  t.add_cleanup(() => {f.remove(); });
+  assert_equals(
+      getElementColorInFrame(f, 'crossOriginCss'),
+      EXPECTED_COLOR,
+      'The color must be overridden by cross origin CSS.');
+  assert_equals(
+      getElementColorInFrame(f, 'crossOriginHtml'),
+      EXPECTED_COLOR,
+      'The color must not be overridden by cross origin non CSS file.');
+  assert_equals(
+      getElementColorInFrame(f, 'sameOriginCss'),
+      EXPECTED_COLOR,
+      'The color must be overridden by same origin CSS.');
+  assert_equals(
+      getElementColorInFrame(f, 'sameOriginHtml'),
+      EXPECTED_COLOR,
+      'The color must be overridden by same origin non CSS file.');
+  assert_equals(
+      getElementColorInFrame(f, 'synthetic'),
+      EXPECTED_COLOR,
+      'The color must be overridden by synthetic CSS.');
+}, 'MIME checking of CSS resources fetched via service worker when Content-Type is not set.');
+
+promise_test(async t => {
+  const PAGE =
+      'resources/fetch-request-css-cross-origin-read-contents.html';
+
+  const f = await with_iframe(PAGE);
+  t.add_cleanup(() => {f.remove(); });
+  assert_throws_dom('SecurityError', f.contentWindow.DOMException, () => {
+    f.contentDocument.styleSheets[0].cssRules[0].cssText;
+  });
+  assert_equals(
+    f.contentDocument.styleSheets[1].cssRules[0].cssText,
+    '#crossOriginCss { color: blue; }',
+    'cross-origin CORS approved response');
+  assert_equals(
+    f.contentDocument.styleSheets[2].cssRules[0].cssText,
+    '#sameOriginCss { color: blue; }',
+    'same-origin response');
+  assert_equals(
+    f.contentDocument.styleSheets[3].cssRules[0].cssText,
+    '#synthetic { color: blue; }',
+    'service worker generated response');
+  }, 'Same-origin policy for access to CSS resources fetched via service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html
new file mode 100644
index 0000000..586dea2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for css image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+var SCRIPT = 'resources/fetch-request-resources-worker.js';
+var host_info = get_host_info();
+var LOCAL_URL =
+  host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+var REMOTE_URL =
+  host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+function css_image_test(expected_results, frame, url, type,
+                        expected_mode, expected_credentials) {
+  expected_results[url] = {
+      url: url,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      message: 'CSSImage load (url:' + url + ' type:' + type + ')'
+    };
+  return frame.contentWindow.load_css_image(url, type);
+}
+
+function css_image_set_test(expected_results, frame, url, type,
+                            expected_mode, expected_credentials) {
+  expected_results[url] = {
+      url: url,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      message: 'CSSImageSet load (url:' + url + ' type:' + type + ')'
+    };
+  return frame.contentWindow.load_css_image_set(url, type);
+}
+
+function waitForWorker(worker) {
+  return new Promise(function(resolve) {
+    var channel = new MessageChannel();
+    channel.port1.addEventListener('message', function(msg) {
+      if (msg.data.ready) {
+        resolve(channel);
+      }
+    });
+    channel.port1.start();
+    worker.postMessage({port: channel.port2}, [channel.port2]);
+  });
+}
+
+function create_message_promise(channel, expected_results, worker, scope) {
+  return new Promise(function(resolve) {
+    channel.port1.addEventListener('message', function(msg) {
+      var result = msg.data;
+      if (!expected_results[result.url]) {
+        return;
+      }
+      resolve(result);
+    });
+  }).then(function(result) {
+      var expected = expected_results[result.url];
+      assert_equals(
+          result.mode, expected.mode,
+          'mode of ' + expected.message +  ' must be ' +
+          expected.mode + '.');
+      assert_equals(
+          result.credentials, expected.credentials,
+          'credentials of ' + expected.message +  ' must be ' +
+          expected.credentials + '.');
+      delete expected_results[result.url];
+    });
+}
+
+promise_test(function(t) {
+    var scope = SCOPE + "?img=backgroundImage";
+    var expected_results = {};
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          frame = f;
+          return waitForWorker(worker);
+        })
+      .then(function(channel) {
+          css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+                         'backgroundImage', 'no-cors', 'include');
+          css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+                        'backgroundImage', 'no-cors', 'include');
+
+          return Promise.all([
+              create_message_promise(channel, expected_results, worker, scope),
+              create_message_promise(channel, expected_results, worker, scope)
+            ]);
+        });
+  }, 'Verify FetchEvent for css image (backgroundImage).');
+
+promise_test(function(t) {
+    var scope = SCOPE + "?img=shapeOutside";
+    var expected_results = {};
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          frame = f;
+          return waitForWorker(worker);
+        })
+      .then(function(channel) {
+          css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+                         'shapeOutside', 'cors', 'same-origin');
+          css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+                         'shapeOutside', 'cors', 'same-origin');
+
+          return Promise.all([
+              create_message_promise(channel, expected_results, worker, scope),
+              create_message_promise(channel, expected_results, worker, scope)
+            ]);
+      });
+  }, 'Verify FetchEvent for css image (shapeOutside).');
+
+promise_test(function(t) {
+    var scope = SCOPE + "?img_set=backgroundImage";
+    var expected_results = {};
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();;
+            });
+          frame = f;
+          return waitForWorker(worker);
+        })
+      .then(function(channel) {
+          css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+                            'backgroundImage', 'no-cors', 'include');
+          css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+                            'backgroundImage', 'no-cors', 'include');
+
+          return Promise.all([
+              create_message_promise(channel, expected_results, worker, scope),
+              create_message_promise(channel, expected_results, worker, scope)
+            ]);
+      });
+  }, 'Verify FetchEvent for css image-set (backgroundImage).');
+
+promise_test(function(t) {
+    var scope = SCOPE + "?img_set=shapeOutside";
+    var expected_results = {};
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          frame = f;
+          return waitForWorker(worker);
+        })
+      .then(function(channel) {
+          css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+                             'shapeOutside', 'cors', 'same-origin');
+          css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+                            'shapeOutside', 'cors', 'same-origin');
+
+          return Promise.all([
+              create_message_promise(channel, expected_results, worker, scope),
+              create_message_promise(channel, expected_results, worker, scope)
+            ]);
+        });
+  }, 'Verify FetchEvent for css image-set (shapeOutside).');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html
new file mode 100644
index 0000000..a29f31d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<title>Service Worker: the fallback behavior of FetchEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function get_fetched_urls(worker) {
+  return new Promise(function(resolve) {
+      var channel = new MessageChannel();
+      channel.port1.onmessage = function(msg) { resolve(msg); };
+      worker.postMessage({port: channel.port2}, [channel.port2]);
+    });
+}
+
+function check_urls(worker, expected_requests) {
+  return get_fetched_urls(worker)
+    .then(function(msg) {
+        var requests = msg.data.requests;
+        assert_object_equals(requests, expected_requests);
+    });
+}
+
+var path = new URL(".", window.location).pathname;
+var SCOPE = 'resources/fetch-request-fallback-iframe.html';
+var SCRIPT = 'resources/fetch-request-fallback-worker.js';
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] +
+               path + 'resources/fetch-access-control.py?';
+var BASE_PNG_URL = BASE_URL + 'PNGIMAGE&';
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] +
+                     path + 'resources/fetch-access-control.py?';
+var OTHER_BASE_PNG_URL = OTHER_BASE_URL + 'PNGIMAGE&';
+var REDIRECT_URL = host_info['HTTPS_ORIGIN'] +
+                   path + 'resources/redirect.py?Redirect=';
+var register;
+
+promise_test(function(t) {
+  var registration;
+  var worker;
+
+  register = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+    .then(function(r) {
+        registration = r;
+        worker = registration.installing;
+        return wait_for_state(t, worker, 'activated');
+      })
+    .then(function() { return with_iframe(SCOPE); })
+    .then(function(frame) {
+        // This test should not be considered complete until after the service
+        // worker has been unregistered. Currently, `testharness.js` does not
+        // support asynchronous global "tear down" logic, so this must be
+        // expressed using a dedicated `promise_test`. Because the other
+        // sub-tests in this file are declared synchronously, this test will be
+        // the final test executed.
+        promise_test(function(t) {
+            t.add_cleanup(function() {
+                frame.remove();
+              });
+            return registration.unregister();
+          }, 'restore global state');
+
+        return {frame: frame, worker: worker};
+      });
+
+    return register;
+  }, 'initialize global state');
+
+function promise_frame_test(body, desc) {
+  promise_test(function(test) {
+      return register.then(function(result) {
+          return body(test, result.frame, result.worker);
+        });
+    }, desc);
+}
+
+promise_frame_test(function(t, frame, worker) {
+      return check_urls(
+          worker,
+          [{
+            url: host_info['HTTPS_ORIGIN'] + path + SCOPE,
+            mode: 'navigate'
+          }]);
+  }, 'The SW must intercept the request for a main resource.');
+
+promise_frame_test(function(t, frame, worker) {
+    return frame.contentWindow.xhr(BASE_URL)
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: BASE_URL, mode: 'cors' }]);
+        });
+  }, 'The SW must intercept the request of same origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+    return promise_rejects_js(
+        t,
+        frame.contentWindow.Error,
+        frame.contentWindow.xhr(OTHER_BASE_URL),
+        'SW fallbacked CORS-unsupported other origin XHR should fail.')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: OTHER_BASE_URL, mode: 'cors' }]);
+        });
+  }, 'The SW must intercept the request of CORS-unsupported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+    return frame.contentWindow.xhr(OTHER_BASE_URL + 'ACAOrigin=*')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: OTHER_BASE_URL + 'ACAOrigin=*', mode: 'cors' }]);
+        })
+  }, 'The SW must intercept the request of CORS-supported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+    return frame.contentWindow.xhr(
+                  REDIRECT_URL + encodeURIComponent(BASE_URL))
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL + encodeURIComponent(BASE_URL),
+                mode: 'cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request of redirected XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+    return promise_rejects_js(
+        t,
+        frame.contentWindow.Error,
+        frame.contentWindow.xhr(
+          REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL)),
+        'SW fallbacked XHR which is redirected to CORS-unsupported ' +
+          'other origin should fail.')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL),
+                mode: 'cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request for XHR which is' +
+     ' redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.xhr(
+                  REDIRECT_URL +
+                  encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'))
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL +
+                     encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+                mode: 'cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request for XHR which is ' +
+     'redirected to CORS-supported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.load_image(BASE_PNG_URL, '')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: BASE_PNG_URL, mode: 'no-cors' }]);
+        });
+  }, 'The SW must intercept the request for image.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.load_image(OTHER_BASE_PNG_URL, '')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: OTHER_BASE_PNG_URL, mode: 'no-cors' }]);
+        });
+  }, 'The SW must intercept the request for other origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+  return promise_rejects_js(
+        t,
+        frame.contentWindow.Error,
+        frame.contentWindow.load_image(OTHER_BASE_PNG_URL, 'anonymous'),
+        'SW fallbacked CORS-unsupported other origin image request ' +
+          'should fail.')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: OTHER_BASE_PNG_URL, mode: 'cors' }]);
+        })
+  }, 'The SW must intercept the request for CORS-unsupported other ' +
+     'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.load_image(
+                  OTHER_BASE_PNG_URL + 'ACAOrigin=*', 'anonymous')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{ url: OTHER_BASE_PNG_URL + 'ACAOrigin=*', mode: 'cors' }]);
+        });
+  }, 'The SW must intercept the request for CORS-supported other ' +
+     'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.load_image(
+                  REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), '')
+      .catch(function() {
+          assert_unreached(
+              'SW fallbacked redirected image request should succeed.');
+        })
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL + encodeURIComponent(BASE_PNG_URL),
+                mode: 'no-cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request for redirected ' +
+     'image resource.');
+
+promise_frame_test(function(t, frame, worker) {
+  return frame.contentWindow.load_image(
+                  REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), '')
+      .catch(function() {
+          assert_unreached(
+              'SW fallbacked image request which is redirected to ' +
+              'other origin should succeed.');
+        })
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+                mode: 'no-cors'
+              }]);
+        })
+  }, 'The SW must intercept only the first request for image ' +
+     'resource which is redirected to other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+  return promise_rejects_js(
+        t,
+        frame.contentWindow.Error,
+        frame.contentWindow.load_image(
+            REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+            'anonymous'),
+        'SW fallbacked image request which is redirected to ' +
+          'CORS-unsupported other origin should fail.')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+                mode: 'cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request for image ' +
+     'resource which is redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+    return frame.contentWindow.load_image(
+        REDIRECT_URL +
+          encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+        'anonymous')
+      .then(function() {
+          return check_urls(
+              worker,
+              [{
+                url: REDIRECT_URL +
+                     encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+                mode: 'cors'
+              }]);
+        });
+  }, 'The SW must intercept only the first request for image ' +
+     'resource which is redirected to CORS-supported other origin.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
new file mode 100644
index 0000000..03b7d35
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Service Worker: the headers of FetchEvent shouldn't contain freshness headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/fetch-request-no-freshness-headers-iframe.html';
+    var SCRIPT = 'resources/fetch-request-no-freshness-headers-worker.js';
+    var worker;
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          return new Promise(function(resolve) {
+              frame.onload = function() {
+                  resolve(frame);
+                };
+              frame.contentWindow.location.reload();
+            });
+        })
+      .then(function(frame) {
+          return new Promise(function(resolve) {
+              var channel = new MessageChannel();
+              channel.port1.onmessage = t.step_func(function(msg) {
+                  frame.remove();
+                  resolve(msg);
+                });
+              worker.postMessage(
+                {port: channel.port2}, [channel.port2]);
+            });
+        })
+      .then(function(msg) {
+          var freshness_headers = {
+            'if-none-match': true,
+            'if-modified-since': true
+          };
+          msg.data.requests.forEach(function(request) {
+              request.headers.forEach(function(header) {
+                  assert_false(
+                      !!freshness_headers[header[0]],
+                      header[0] + ' header must not be set in the ' +
+                      'FetchEvent\'s request. (url = ' + request.url + ')');
+                });
+            })
+        });
+  }, 'The headers of FetchEvent shouldn\'t contain freshness headers.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html
new file mode 100644
index 0000000..5ce015b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html
@@ -0,0 +1,385 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var test_scope = ""
+function assert_resolves(promise, description) {
+    return promise.then(
+        () => test(() => {}, description + " - " + test_scope),
+        (e) => test(() => { throw e; }, description + " - " + test_scope)
+    );
+}
+
+function assert_rejects(promise, description) {
+    return promise.then(
+        () => test(() => { assert_unreached(); }, description + " - " + test_scope),
+        () => test(() => {}, description + " - " + test_scope)
+    );
+}
+
+function iframe_test(url, timeout_enabled) {
+  return new Promise(function(resolve, reject) {
+      var frame = document.createElement('iframe');
+      frame.src = url;
+      if (timeout_enabled) {
+        // We can't catch the network error on iframe. So we use the timer for
+        // failure detection.
+        var timer = setTimeout(function() {
+            reject(new Error('iframe load timeout'));
+            frame.remove();
+          }, 10000);
+      }
+      frame.onload = function() {
+        if (timeout_enabled)
+          clearTimeout(timer);
+        try {
+          if (frame.contentDocument.body.textContent == 'Hello world\n')
+            resolve();
+          else
+            reject(new Error('content mismatch'));
+        } catch (e) {
+          // Chrome treats iframes that failed to load due to a network error as
+          // having a different origin, so accessing contentDocument throws an
+          // error. Other browsers might have different behavior.
+          reject(new Error(e));
+        }
+        frame.remove();
+      };
+      document.body.appendChild(frame);
+    });
+}
+
+promise_test(function(t) {
+    test_scope = "default";
+
+    var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+    var IMAGE_URL = base_path() + 'resources/square.png';
+    var AUDIO_URL = getAudioURI("/media/sound_5");
+    var XHR_URL = base_path() + 'resources/simple.txt';
+    var HTML_URL = base_path() + 'resources/sample.html';
+
+    var REDIRECT_TO_IMAGE_URL = REDIRECT_URL + encodeURIComponent(IMAGE_URL);
+    var REDIRECT_TO_AUDIO_URL = REDIRECT_URL + encodeURIComponent(AUDIO_URL);
+    var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+    var REDIRECT_TO_HTML_URL = REDIRECT_URL + encodeURIComponent(HTML_URL);
+
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(async function(f) {
+            frame = f;
+            // XMLHttpRequest tests.
+            await assert_resolves(frame.contentWindow.xhr(XHR_URL),
+                            'Normal XHR should succeed.');
+            await assert_resolves(frame.contentWindow.xhr(REDIRECT_TO_XHR_URL),
+                            'Redirected XHR should succeed.');
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&redirect-mode=follow'),
+                'Redirected XHR with Request.redirect=follow should succeed.');
+            await assert_rejects(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&redirect-mode=error'),
+                'Redirected XHR with Request.redirect=error should fail.');
+            await assert_rejects(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&redirect-mode=manual'),
+                'Redirected XHR with Request.redirect=manual should fail.');
+
+            // Image loading tests.
+            await assert_resolves(frame.contentWindow.load_image(IMAGE_URL),
+                            'Normal image resource should be loaded.');
+            await assert_resolves(
+                frame.contentWindow.load_image(REDIRECT_TO_IMAGE_URL),
+                'Redirected image resource should be loaded.');
+            await assert_resolves(
+                frame.contentWindow.load_image(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+                    '&redirect-mode=follow'),
+                'Loading redirected image with Request.redirect=follow should' +
+                ' succeed.');
+            await assert_rejects(
+                frame.contentWindow.load_image(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+                    '&redirect-mode=error'),
+                'Loading redirected image with Request.redirect=error should ' +
+                'fail.');
+            await assert_rejects(
+                frame.contentWindow.load_image(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+                    '&redirect-mode=manual'),
+                'Loading redirected image with Request.redirect=manual should' +
+                ' fail.');
+
+            // Audio loading tests.
+            await assert_resolves(frame.contentWindow.load_audio(AUDIO_URL),
+                            'Normal audio resource should be loaded.');
+            await assert_resolves(
+                frame.contentWindow.load_audio(REDIRECT_TO_AUDIO_URL),
+                'Redirected audio resource should be loaded.');
+            await assert_resolves(
+                frame.contentWindow.load_audio(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+                    '&redirect-mode=follow'),
+                'Loading redirected audio with Request.redirect=follow should' +
+                ' succeed.');
+            await assert_rejects(
+                frame.contentWindow.load_audio(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+                    '&redirect-mode=error'),
+                'Loading redirected audio with Request.redirect=error should ' +
+                'fail.');
+            await assert_rejects(
+                frame.contentWindow.load_audio(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+                    '&redirect-mode=manual'),
+                'Loading redirected audio with Request.redirect=manual should' +
+                ' fail.');
+
+            // Iframe tests.
+            await assert_resolves(iframe_test(HTML_URL),
+                            'Normal iframe loading should succeed.');
+            await assert_resolves(
+                iframe_test(REDIRECT_TO_HTML_URL),
+                'Normal redirected iframe loading should succeed.');
+            await assert_rejects(
+                iframe_test(SCOPE + '?url=' +
+                            encodeURIComponent(REDIRECT_TO_HTML_URL) +
+                            '&redirect-mode=follow',
+                            true /* timeout_enabled */),
+                'Redirected iframe loading with Request.redirect=follow should'+
+                ' fail.');
+            await assert_rejects(
+                iframe_test(SCOPE + '?url=' +
+                            encodeURIComponent(REDIRECT_TO_HTML_URL) +
+                            '&redirect-mode=error',
+                            true /* timeout_enabled */),
+                'Redirected iframe loading with Request.redirect=error should '+
+                'fail.');
+            await assert_resolves(
+                iframe_test(SCOPE + '?url=' +
+                            encodeURIComponent(REDIRECT_TO_HTML_URL) +
+                            '&redirect-mode=manual',
+                            true /* timeout_enabled */),
+                'Redirected iframe loading with Request.redirect=manual should'+
+                ' succeed.');
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Verify redirect mode of Fetch API and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected
+promise_test(function(t) {
+    test_scope = "redirected";
+
+    var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+    var XHR_URL = base_path() + 'resources/simple.txt';
+    var IMAGE_URL = base_path() + 'resources/square.png';
+
+    var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+    var host_info = get_host_info();
+
+    var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+    var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+      encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(async function(f) {
+          frame = f;
+            // XMLHttpRequest tests.
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(XHR_URL) +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true'),
+                'Normal XHR should be resolved and response should not be ' +
+                'redirected.');
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&expected_redirected=true' +
+                    '&expected_resolves=true'),
+                'Redirected XHR should be resolved and response should be ' +
+                'redirected.');
+
+            // tests for request's mode = cors
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(XHR_URL) +
+                    '&mode=cors' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true'),
+                'Normal XHR should be resolved and response should not be ' +
+                'redirected even with CORS mode.');
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&mode=cors' +
+                    '&redirect-mode=follow' +
+                    '&expected_redirected=true' +
+                    '&expected_resolves=true'),
+                'Redirected XHR should be resolved and response.redirected ' +
+                'should be redirected with CORS mode.');
+
+            // tests for request's mode = no-cors
+            // The response.redirect should be false since we will not add
+            // redirected url list when redirect-mode is not follow.
+            await assert_rejects(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&mode=no-cors' +
+                    '&redirect-mode=manual' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=false'),
+                'Redirected XHR should be reject and response should be ' +
+                'redirected with NO-CORS mode and redirect-mode=manual.');
+
+            // tests for redirecting to a cors
+            await assert_resolves(
+                frame.contentWindow.load_image(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+                    '&mode=no-cors' +
+                    '&redirect-mode=follow' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true'),
+                'Redirected CORS image should be reject and response should ' +
+                'not be redirected with NO-CORS mode.');
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Verify redirected of Response(Fetch API) and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected after cached
+promise_test(function(t) {
+    test_scope = "cache";
+
+    var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+    var XHR_URL = base_path() + 'resources/simple.txt';
+    var IMAGE_URL = base_path() + 'resources/square.png';
+
+    var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+    var host_info = get_host_info();
+
+    var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+    var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+      encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+    var worker;
+    var frame;
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+          worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(async function(f) {
+          frame = f;
+            // XMLHttpRequest tests.
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(XHR_URL) +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true' +
+                    '&cache'),
+                'Normal XHR should be resolved and response should not be ' +
+                'redirected.');
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&expected_redirected=true' +
+                    '&expected_resolves=true' +
+                    '&cache'),
+                'Redirected XHR should be resolved and response should be ' +
+                'redirected.');
+
+            // tests for request's mode = cors
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(XHR_URL) +
+                    '&mode=cors' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true' +
+                    '&cache'),
+                'Normal XHR should be resolved and response should not be ' +
+                'redirected even with CORS mode.');
+            await assert_resolves(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&mode=cors' +
+                    '&redirect-mode=follow' +
+                    '&expected_redirected=true' +
+                    '&expected_resolves=true' +
+                    '&cache'),
+                'Redirected XHR should be resolved and response.redirected ' +
+                'should be redirected with CORS mode.');
+
+            // tests for request's mode = no-cors
+            // The response.redirect should be false since we will not add
+            // redirected url list when redirect-mode is not follow.
+            await assert_rejects(
+                frame.contentWindow.xhr(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+                    '&mode=no-cors' +
+                    '&redirect-mode=manual' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=false' +
+                    '&cache'),
+                'Redirected XHR should be reject and response should be ' +
+                'redirected with NO-CORS mode and redirect-mode=manual.');
+
+            // tests for redirecting to a cors
+            await assert_resolves(
+                frame.contentWindow.load_image(
+                    './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+                    '&mode=no-cors' +
+                    '&redirect-mode=follow' +
+                    '&expected_redirected=false' +
+                    '&expected_resolves=true' +
+                    '&cache'),
+                'Redirected CORS image should be reject and response should ' +
+                'not be redirected with NO-CORS mode.');
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Verify redirected of Response(Fetch API), Cache API and ServiceWorker ' +
+     'FetchEvent.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html
new file mode 100644
index 0000000..b4680c3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let url_count = 0;
+const expected_results = {};
+
+function add_promise_to_test(url)
+{
+  const expected = expected_results[url];
+  return new Promise((resolve) => {
+    expected.resolve = resolve;
+  });
+}
+
+function image_test(frame, url, cross_origin, expected_mode,
+                    expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      cross_origin: cross_origin,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'image',
+      message: `Image load (url:${actual_url} cross_origin:${cross_origin})`
+    };
+  frame.contentWindow.load_image(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
+function script_test(frame, url, cross_origin, expected_mode,
+                     expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      cross_origin: cross_origin,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'script',
+      message: `Script load (url:${actual_url} cross_origin:${cross_origin})`
+    };
+  frame.contentWindow.load_script(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
+function css_test(frame, url, cross_origin, expected_mode,
+                  expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      cross_origin: cross_origin,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'style',
+      message: `CSS load (url:${actual_url} cross_origin:${cross_origin})`
+    };
+  frame.contentWindow.load_css(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
+function font_face_test(frame, url, expected_mode, expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      url: actual_url,
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'font',
+      message: `FontFace load (url: ${actual_url})`
+    };
+  frame.contentWindow.load_font(actual_url);
+  return add_promise_to_test(actual_url);
+}
+
+function script_integrity_test(frame, url, integrity, expected_integrity) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      url: actual_url,
+      mode: 'no-cors',
+      credentials: 'include',
+      redirect: 'follow',
+      integrity: expected_integrity,
+      destination: 'script',
+      message: `Script load (url:${actual_url})`
+    };
+  frame.contentWindow.load_script_with_integrity(actual_url, integrity);
+  return add_promise_to_test(actual_url);
+}
+
+function css_integrity_test(frame, url, integrity, expected_integrity) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      url: actual_url,
+      mode: 'no-cors',
+      credentials: 'include',
+      redirect: 'follow',
+      integrity: expected_integrity,
+      destination: 'style',
+      message: `CSS load (url:${actual_url})`
+    };
+  frame.contentWindow.load_css_with_integrity(actual_url, integrity);
+  return add_promise_to_test(actual_url);
+}
+
+function fetch_test(frame, url, mode, credentials,
+                    expected_mode, expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: '',
+      message: `fetch (url:${actual_url} mode:${mode} ` +
+               `credentials:${credentials})`
+    };
+  frame.contentWindow.fetch(
+      new Request(actual_url, {mode: mode, credentials: credentials}));
+  return add_promise_to_test(actual_url);
+}
+
+function audio_test(frame, url, cross_origin,
+                    expected_mode, expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'audio',
+      message: `Audio load (url:${actual_url} cross_origin:${cross_origin})`
+    };
+  frame.contentWindow.load_audio(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
+
+function video_test(frame, url, cross_origin,
+                    expected_mode, expected_credentials) {
+  const actual_url = url + (++url_count);
+  expected_results[actual_url] = {
+      mode: expected_mode,
+      credentials: expected_credentials,
+      redirect: 'follow',
+      integrity: '',
+      destination: 'video',
+      message: `Video load (url:${actual_url} cross_origin:${cross_origin})`
+    };
+  frame.contentWindow.load_video(actual_url, cross_origin);
+  return add_promise_to_test(actual_url);
+}
+
+promise_test(async t => {
+  const SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+  const SCRIPT = 'resources/fetch-request-resources-worker.js';
+  const host_info = get_host_info();
+  const LOCAL_URL =
+      host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+  const REMOTE_URL =
+      host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+  const registration =
+      await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  t.add_cleanup(() => registration.unregister());
+  const worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+
+  await new Promise((resolve, reject) => {
+    const channel = new MessageChannel();
+    channel.port1.onmessage = t.step_func(msg => {
+      if (msg.data.ready) {
+        resolve();
+        return;
+      }
+      const result = msg.data;
+      const expected = expected_results[result.url];
+      if (!expected) {
+        return;
+      }
+      test(() => {
+        assert_equals(
+            result.mode, expected.mode,
+            `mode of must be ${expected.mode}.`);
+        assert_equals(
+            result.credentials, expected.credentials,
+            `credentials of ${expected.message} must be ` +
+            `${expected.credentials}.`);
+        assert_equals(
+            result.redirect, expected.redirect,
+            `redirect mode of ${expected.message} must be ` +
+            `${expected.redirect}.`);
+        assert_equals(
+            result.integrity, expected.integrity,
+            `integrity of ${expected.message} must be ` +
+            `${expected.integrity}.`);
+        assert_equals(
+            result.destination, expected.destination,
+            `destination of ${expected.message} must be ` +
+            `${expected.destination}.`);
+      }, expected.message);
+      expected.resolve();
+      delete expected_results[result.url];
+    });
+    worker.postMessage({port: channel.port2}, [channel.port2]);
+  });
+
+  const f = await with_iframe(SCOPE);
+  t.add_cleanup(() => f.remove());
+
+  await image_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+
+  await image_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+  await image_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+  await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await image_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+  await image_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+  await script_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await script_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+  await script_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+  await script_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await script_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+  await script_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+  await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await css_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+  await css_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+  await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await css_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+  await css_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+  await font_face_test(f, LOCAL_URL, 'cors', 'same-origin');
+  await font_face_test(f, REMOTE_URL, 'cors', 'same-origin');
+
+  await script_integrity_test(f, LOCAL_URL, '     ', '     ');
+  await script_integrity_test(
+      f, LOCAL_URL,
+      'This is not a valid integrity because it has no dashes',
+      'This is not a valid integrity because it has no dashes');
+  await script_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+  await script_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+  await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+                              'sha256-foo sha384-abc ');
+  await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+                              'sha256-foo sha256-abc');
+
+  await css_integrity_test(f, LOCAL_URL, '     ', '     ');
+  await css_integrity_test(
+      f, LOCAL_URL,
+      'This is not a valid integrity because it has no dashes',
+      'This is not a valid integrity because it has no dashes');
+  await css_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+  await css_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+  await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+                           'sha256-foo sha384-abc ');
+  await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+                           'sha256-foo sha256-abc');
+
+  await fetch_test(f, LOCAL_URL, 'same-origin', 'omit', 'same-origin', 'omit');
+  await fetch_test(f, LOCAL_URL, 'same-origin', 'same-origin',
+                   'same-origin', 'same-origin');
+  await fetch_test(f, LOCAL_URL, 'same-origin', 'include',
+                   'same-origin', 'include');
+  await fetch_test(f, LOCAL_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+  await fetch_test(f, LOCAL_URL, 'no-cors', 'same-origin',
+                   'no-cors', 'same-origin');
+  await fetch_test(f, LOCAL_URL, 'no-cors', 'include', 'no-cors', 'include');
+  await fetch_test(f, LOCAL_URL, 'cors', 'omit', 'cors', 'omit');
+  await fetch_test(f, LOCAL_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+  await fetch_test(f, LOCAL_URL, 'cors', 'include', 'cors', 'include');
+  await fetch_test(f, REMOTE_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+  await fetch_test(f, REMOTE_URL, 'no-cors', 'same-origin', 'no-cors', 'same-origin');
+  await fetch_test(f, REMOTE_URL, 'no-cors', 'include', 'no-cors', 'include');
+  await fetch_test(f, REMOTE_URL, 'cors', 'omit', 'cors', 'omit');
+  await fetch_test(f, REMOTE_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+  await fetch_test(f, REMOTE_URL, 'cors', 'include', 'cors', 'include');
+
+  await audio_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await audio_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+  await audio_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+  await audio_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+  await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+  await video_test(f, LOCAL_URL, '', 'no-cors', 'include');
+  await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+  await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+  await video_test(f, REMOTE_URL, '', 'no-cors', 'include');
+  await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+  await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+}, 'Verify FetchEvent for resources.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
new file mode 100644
index 0000000..e6c0213
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
@@ -0,0 +1,19 @@
+// META: script=resources/test-helpers.sub.js
+
+"use strict";
+
+promise_test(async t => {
+  const url = "resources/fetch-request-xhr-sync-error-worker.js";
+  const scope = "resources/fetch-request-xhr-sync-iframe.html";
+
+  const registration = await service_worker_unregister_and_register(t, url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt"));
+  assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt"));
+  assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt"));
+}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors");
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
new file mode 100644
index 0000000..9f18096
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR on Worker is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test((t) => {
+    const url = 'resources/fetch-request-xhr-sync-on-worker-worker.js';
+    const scope = 'resources/fetch-request-xhr-sync-on-worker-scope/';
+    const non_existent_file = 'non-existent-file.txt';
+
+    // In Chromium, the service worker scope matching for workers is based on
+    // the URL of the parent HTML. So this test creates an iframe which is
+    // controlled by the service worker first, and creates a worker from the
+    // iframe.
+    return service_worker_unregister_and_register(t, url, scope)
+      .then((registration) => {
+          t.add_cleanup(() => registration.unregister());
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => { return with_iframe(scope + 'iframe_page'); })
+      .then((frame) => {
+          t.add_cleanup(() => frame.remove());
+          return frame.contentWindow.performSyncXHROnWorker(non_existent_file);
+        })
+      .then((result) => {
+          assert_equals(
+              result.status,
+              200,
+              'HTTP response status code for intercepted request'
+            );
+          assert_equals(
+            result.responseText,
+              'Response from service worker',
+              'HTTP response text for intercepted request'
+            );
+        });
+  }, 'Verify SyncXHR on Worker is intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
new file mode 100644
index 0000000..ec27fb8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+    var url = 'resources/fetch-request-xhr-sync-worker.js';
+    var scope = 'resources/fetch-request-xhr-sync-iframe.html';
+    var non_existent_file = 'non-existent-file.txt';
+
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return registration.unregister();
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(frame) {
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          return new Promise(function(resolve, reject) {
+              t.step_timeout(function() {
+                  var xhr;
+                  try {
+                    xhr = frame.contentWindow.performSyncXHR(non_existent_file);
+                    resolve(xhr);
+                  } catch (err) {
+                    reject(err);
+                  }
+                }, 0);
+            })
+        })
+      .then(function(xhr) {
+          assert_equals(
+              xhr.status,
+              200,
+              'HTTP response status code for intercepted request'
+            );
+          assert_equals(
+              xhr.responseText,
+              'Response from service worker',
+              'HTTP response text for intercepted request'
+            );
+        });
+  }, 'Verify SyncXHR is intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html
new file mode 100644
index 0000000..37a4573
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Service Worker: the body of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe-sub"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+    const kScope = 'resources/fetch-request-xhr-iframe.https.html';
+    const kScript = 'resources/fetch-request-xhr-worker.js';
+    return service_worker_unregister_and_register(t, kScript, kScope)
+      .then(registration => {
+          promise_test(() => {
+              return registration.unregister();
+            }, 'restore global state');
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => {
+          return with_iframe(kScope);
+        })
+      .then(f => {
+          frame = f;
+          add_completion_callback(() => { f.remove(); });
+        });
+  }, 'initialize global state');
+
+// Run the tests.
+promise_test(t => {
+    return frame.contentWindow.get_header_test();
+  }, 'event.request has the expected headers for same-origin GET.');
+
+promise_test(t => {
+    return frame.contentWindow.post_header_test();
+  }, 'event.request has the expected headers for same-origin POST.');
+
+promise_test(t => {
+    return frame.contentWindow.cross_origin_get_header_test();
+  }, 'event.request has the expected headers for cross-origin GET.');
+
+promise_test(t => {
+    return frame.contentWindow.cross_origin_post_header_test();
+  }, 'event.request has the expected headers for cross-origin POST.');
+
+promise_test(t => {
+    return frame.contentWindow.string_test();
+  }, 'FetchEvent#request.body contains XHR request data (string)');
+
+promise_test(t => {
+    return frame.contentWindow.blob_test();
+  }, 'FetchEvent#request.body contains XHR request data (blob)');
+
+promise_test(t => {
+    return frame.contentWindow.custom_method_test();
+  }, 'FetchEvent#request.method is set to XHR method');
+
+promise_test(t => {
+    return frame.contentWindow.options_method_test();
+  }, 'XHR using OPTIONS method');
+
+promise_test(t => {
+    return frame.contentWindow.form_data_test();
+  }, 'XHR with form data');
+
+promise_test(t => {
+    return frame.contentWindow.mode_credentials_test();
+  }, 'XHR with mode/credentials set');
+
+promise_test(t => {
+    return frame.contentWindow.data_url_test();
+  }, 'XHR to data URL');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html
new file mode 100644
index 0000000..8e190f4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<title>Service Worker: Tainting of responses fetched via SW.</title>
+<!-- This test makes a large number of requests sequentially. -->
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_ORIGIN = host_info.HTTPS_ORIGIN;
+var OTHER_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+var BASE_URL = BASE_ORIGIN + base_path() +
+               'resources/fetch-access-control.py?';
+var OTHER_BASE_URL = OTHER_ORIGIN + base_path() +
+                     'resources/fetch-access-control.py?';
+
+function frame_fetch(frame, url, mode, credentials) {
+  var foreignPromise = frame.contentWindow.fetch(
+      new Request(url, {mode: mode, credentials: credentials}))
+
+  // Event loops should be shared between contexts of similar origin, not all
+  // browsers adhere to this expectation at the time of this writing. Incorrect
+  // behavior in this regard can interfere with test execution when the
+  // provided iframe is removed from the document.
+  //
+  // WPT maintains a test dedicated the expected treatment of event loops, so
+  // the following workaround is acceptable in this context.
+  return Promise.resolve(foreignPromise);
+}
+
+var login_and_register;
+promise_test(function(t) {
+    var SCOPE = 'resources/fetch-response-taint-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var registration;
+
+    login_and_register = login_https(t, host_info.HTTPS_ORIGIN, host_info.HTTPS_REMOTE_ORIGIN)
+      .then(function() {
+          return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+        })
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(f) {
+          // This test should not be considered complete until after the
+          // service worker has been unregistered. Currently, `testharness.js`
+          // does not support asynchronous global "tear down" logic, so this
+          // must be expressed using a dedicated `promise_test`. Because the
+          // other sub-tests in this file are declared synchronously, this
+          // test will be the final test executed.
+          promise_test(function(t) {
+              f.remove();
+              return registration.unregister();
+            }, 'restore global state');
+
+          return f;
+        });
+    return login_and_register;
+  }, 'initialize global state');
+
+function ng_test(url, mode, credentials) {
+  promise_test(function(t) {
+      return login_and_register
+        .then(function(frame) {
+            var fetchRequest = frame_fetch(frame, url, mode, credentials);
+            return promise_rejects_js(t, frame.contentWindow.TypeError, fetchRequest);
+          });
+    }, 'url:\"' + url + '\" mode:\"' + mode +
+       '\" credentials:\"' + credentials + '\" should fail.');
+}
+
+function ok_test(url, mode, credentials, expected_type, expected_username) {
+  promise_test(function() {
+      return login_and_register.then(function(frame) {
+            return frame_fetch(frame, url, mode, credentials)
+          })
+        .then(function(res) {
+            assert_equals(res.type, expected_type, 'response type');
+            return res.text();
+          })
+        .then(function(text) {
+            if (expected_type == 'opaque') {
+              assert_equals(text, '');
+            } else {
+              return new Promise(function(resolve) {
+                    var report = resolve;
+                    // text must contain report() call.
+                    eval(text);
+                  })
+                .then(function(result) {
+                    assert_equals(result.username, expected_username);
+                  });
+            }
+          });
+    }, 'fetching url:\"' + url + '\" mode:\"' + mode +
+                        '\" credentials:\"' + credentials + '\" should ' +
+                        'succeed.');
+}
+
+function build_rewrite_url(origin, url, mode, credentials) {
+  return origin + '/?url=' + encodeURIComponent(url) + '&mode=' + mode +
+      '&credentials=' + credentials + '&';
+}
+
+function for_each_origin_mode_credentials(callback) {
+  [BASE_ORIGIN, OTHER_ORIGIN].forEach(function(origin) {
+      ['same-origin', 'no-cors', 'cors'].forEach(function(mode) {
+          ['omit', 'same-origin', 'include'].forEach(function(credentials) {
+              callback(origin, mode, credentials);
+            });
+        });
+    });
+}
+
+ok_test(BASE_URL, 'same-origin', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'same-origin', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'same-origin', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'no-cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'include', 'basic', 'username2s');
+ng_test(OTHER_BASE_URL, 'same-origin', 'omit');
+ng_test(OTHER_BASE_URL, 'same-origin', 'same-origin');
+ng_test(OTHER_BASE_URL, 'same-origin', 'include');
+ok_test(OTHER_BASE_URL, 'no-cors', 'omit', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'same-origin', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'include', 'opaque');
+ng_test(OTHER_BASE_URL, 'cors', 'omit');
+ng_test(OTHER_BASE_URL, 'cors', 'same-origin');
+ng_test(OTHER_BASE_URL, 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit', 'cors', 'undefined');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'same-origin', 'cors',
+        'undefined');
+ng_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + '&ACACredentials=true',
+        'cors', 'include', 'cors', 'username1s')
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin, BASE_URL, 'same-origin', 'omit');
+  // Fetch to the other origin with same-origin mode should fail.
+  if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+    ng_test(url, mode, credentials);
+  } else {
+    // The response type from the SW should be basic
+    ok_test(url, mode, credentials, 'basic', 'undefined');
+  }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin, BASE_URL, 'same-origin', 'same-origin');
+
+  // Fetch to the other origin with same-origin mode should fail.
+  if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+    ng_test(url, mode, credentials);
+  } else {
+    // The response type from the SW should be basic.
+    ok_test(url, mode, credentials, 'basic', 'username2s');
+  }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin, OTHER_BASE_URL, 'same-origin', 'omit');
+  // The response from the SW should be an error.
+  ng_test(url, mode, credentials);
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin, OTHER_BASE_URL, 'no-cors', 'omit');
+
+  // SW can respond only to no-cors requests.
+  if (mode != 'no-cors') {
+    ng_test(url, mode, credentials);
+  } else {
+    // The response type from the SW should be opaque.
+    ok_test(url, mode, credentials, 'opaque');
+  }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin, OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit');
+
+  // Fetch to the other origin with same-origin mode should fail.
+  if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+    ng_test(url, mode, credentials);
+  } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+    // Cors type response to a same-origin mode request should fail
+    ng_test(url, mode, credentials);
+  } else {
+    // The response from the SW should be cors.
+    ok_test(url, mode, credentials, 'cors', 'undefined');
+  }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+  var url = build_rewrite_url(
+      origin,
+      OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN +
+      '&ACACredentials=true',
+      'cors', 'include');
+  // Fetch to the other origin with same-origin mode should fail.
+  if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+    ng_test(url, mode, credentials);
+  } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+    // Cors type response to a same-origin mode request should fail
+    ng_test(url, mode, credentials);
+  } else {
+    // The response from the SW should be cors.
+    ok_test(url, mode, credentials, 'cors', 'username1s');
+  }
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html
new file mode 100644
index 0000000..891eb02
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: the response of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/fetch-response-xhr-iframe.https.html';
+    var SCRIPT = 'resources/fetch-response-xhr-worker.js';
+    var host_info = get_host_info();
+
+    window.addEventListener('message', t.step_func(on_message), false);
+    function on_message(e) {
+      assert_equals(e.data.results, 'foo, bar');
+      e.source.postMessage('ACK', host_info['HTTPS_ORIGIN']);
+    }
+
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          var channel;
+
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          channel = new MessageChannel();
+          var onPortMsg = new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+            });
+
+          frame.contentWindow.postMessage('START',
+                                          host_info['HTTPS_ORIGIN'],
+                                          [channel.port2]);
+
+          return onPortMsg;
+        })
+      .then(function(e) {
+          assert_equals(e.data.results, 'finish');
+        });
+  }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html
new file mode 100644
index 0000000..7c88845
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Waits for Activate Event</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const worker_url = 'resources/fetch-waits-for-activate-worker.js';
+const normalized_worker_url = normalizeURL(worker_url);
+const worker_scope = 'resources/fetch-waits-for-activate/';
+
+// Resolves with the Service Worker's registration once it's reached the
+// "activating" state. (The Service Worker should remain "activating" until
+// explicitly told advance to the "activated" state).
+async function registerAndWaitForActivating(t) {
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, worker_scope);
+  t.add_cleanup(() => service_worker_unregister(t, worker_scope));
+
+  await wait_for_state(t, registration.installing, 'activating');
+
+  return registration;
+}
+
+// Attempts to ensure that the "Handle Fetch" algorithm has reached the step
+//
+//   "If activeWorker’s state is "activating", wait for activeWorker’s state to
+//    become "activated"."
+//
+// by waiting for some time to pass.
+//
+// WARNING: whether the algorithm has reached that step isn't directly
+// observable, so this is best effort and can race. Note that this can only
+// result in false positives (where the algorithm hasn't reached that step yet
+// and any functional events haven't actually been handled by the Service
+// Worker).
+async function ensureFunctionalEventsAreWaiting(registration) {
+  await (new Promise(resolve => { setTimeout(resolve, 1000); }));
+
+  assert_equals(registration.active.scriptURL, normalized_worker_url,
+                'active worker should be present');
+  assert_equals(registration.active.state, 'activating',
+                'active worker should be in activating state');
+}
+
+promise_test(async t => {
+  const registration = await registerAndWaitForActivating(t);
+
+  let frame = null;
+  t.add_cleanup(() => {
+    if (frame) {
+      frame.remove();
+    }
+  });
+
+  // This should block until we message the worker to tell it to complete
+  // the activate event.
+  const frameLoadPromise = with_iframe(worker_scope).then(function(f) {
+    frame = f;
+  });
+
+  await ensureFunctionalEventsAreWaiting(registration);
+  assert_equals(frame, null, 'frame should not be loaded');
+
+  registration.active.postMessage('ACTIVATE');
+
+  await frameLoadPromise;
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+                normalized_worker_url,
+                'frame should now be loaded and controlled');
+  assert_equals(registration.active.state, 'activated',
+                'active worker should be in activated state');
+}, 'Navigation fetch events should wait for the activate event to complete.');
+
+promise_test(async t => {
+  const frame = await with_iframe(worker_scope);
+  t.add_cleanup(() => { frame.remove(); });
+
+  const registration = await registerAndWaitForActivating(t);
+
+  // Make the Service Worker control the frame so the frame can perform an
+  // intercepted fetch.
+  await (new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      assert_equals(
+        frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+        normalized_worker_url, 'frame should be controlled');
+      resolve();
+    };
+
+    registration.active.postMessage('CLAIM');
+  }));
+
+  const fetch_url = `${worker_scope}non/existent/path`;
+  const expected_fetch_result = 'Hello world';
+  let fetch_promise_settled = false;
+
+  // This should block until we message the worker to tell it to complete
+  // the activate event.
+  const fetchPromise = frame.contentWindow.fetch(fetch_url, {
+    method: 'POST',
+    body: expected_fetch_result,
+  }).then(response => {
+    fetch_promise_settled = true;
+    return response;
+  });
+
+  await ensureFunctionalEventsAreWaiting(registration);
+  assert_false(fetch_promise_settled,
+               "fetch()-ing a Service Worker-controlled scope shouldn't have " +
+               "settled yet");
+
+  registration.active.postMessage('ACTIVATE');
+
+  const response = await fetchPromise;
+  assert_equals(await response.text(), expected_fetch_result,
+                "Service Worker should have responded to request to" +
+                fetch_url)
+  assert_equals(registration.active.state, 'activated',
+                'active worker should be in activated state');
+}, 'Subresource fetch events should wait for the activate event to complete.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html b/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html
new file mode 100644
index 0000000..634c2ef
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+    var documentURL = 'no-such-worker';
+    navigator.serviceWorker.getRegistration(documentURL)
+      .then(function(value) {
+          assert_equals(value, undefined,
+                        'getRegistration should resolve with undefined');
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'getRegistration');
+
+promise_test(function(t) {
+    var scope = 'resources/scope/getregistration/normal';
+    var registration;
+    return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+                                                  scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(value) {
+          assert_equals(
+              value, registration,
+              'getRegistration should resolve to the same registration object');
+        });
+  }, 'Register then getRegistration');
+
+promise_test(function(t) {
+    var scope = 'resources/scope/getregistration/url-with-fragment';
+    var documentURL = scope + '#ref';
+    var registration;
+    return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+                                                  scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return navigator.serviceWorker.getRegistration(documentURL);
+        })
+      .then(function(value) {
+          assert_equals(
+              value, registration,
+              'getRegistration should resolve to the same registration object');
+        });
+  }, 'Register then getRegistration with a URL having a fragment');
+
+async_test(function(t) {
+    var documentURL = 'http://example.com/';
+    navigator.serviceWorker.getRegistration(documentURL)
+      .then(function() {
+          assert_unreached(
+              'getRegistration with an out of origin URL should fail');
+      }, function(reason) {
+          assert_equals(
+              reason.name, 'SecurityError',
+              'getRegistration with an out of origin URL should fail');
+          t.done();
+      })
+      .catch(unreached_rejection(t));
+  }, 'getRegistration with a cross origin URL');
+
+async_test(function(t) {
+    var scope = 'resources/scope/getregistration/register-unregister';
+    service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+                                           scope)
+      .then(function(registration) {
+          return registration.unregister();
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(value) {
+          assert_equals(value, undefined,
+                        'getRegistration should resolve with undefined');
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register then Unregister then getRegistration');
+
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/getregistration/register-unregister';
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/empty-worker.js', scope
+  );
+
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  const frameNav = frame.contentWindow.navigator;
+  await registration.unregister();
+  const value = await frameNav.serviceWorker.getRegistration(scope);
+
+  assert_equals(value, undefined, 'getRegistration should resolve with undefined');
+}, 'Register then Unregister then getRegistration in controlled iframe');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html b/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html
new file mode 100644
index 0000000..3a9b9a2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<title>Service Worker: getRegistrations()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Purge the existing registrations for the origin.
+// getRegistrations() is used in order to avoid adding additional complexity
+// e.g. adding an internal function.
+promise_test(async () => {
+  const registrations = await navigator.serviceWorker.getRegistrations();
+  await Promise.all(registrations.map(r => r.unregister()));
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(
+      value, [],
+      'getRegistrations should resolve with an empty array.');
+}, 'registrations are not returned following unregister');
+
+promise_test(async t => {
+  const scope = 'resources/scope/getregistrations/normal';
+  const script = 'resources/empty-worker.js';
+  const registrations = [
+      await service_worker_unregister_and_register(t, script, scope)];
+  t.add_cleanup(() => registrations[0].unregister());
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(value, registrations,
+      'getRegistrations should resolve with an array of registrations');
+}, 'Register then getRegistrations');
+
+promise_test(async t => {
+  const scope1 = 'resources/scope/getregistrations/scope1';
+  const scope2 = 'resources/scope/getregistrations/scope2';
+  const scope3 = 'resources/scope/getregistrations/scope12';
+
+  const script = 'resources/empty-worker.js';
+  t.add_cleanup(() => service_worker_unregister(t, scope1));
+  t.add_cleanup(() => service_worker_unregister(t, scope2));
+  t.add_cleanup(() => service_worker_unregister(t, scope3));
+
+  const registrations = [
+      await service_worker_unregister_and_register(t, script, scope1),
+      await service_worker_unregister_and_register(t, script, scope2),
+      await service_worker_unregister_and_register(t, script, scope3),
+  ];
+
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(value, registrations);
+}, 'Register multiple times then getRegistrations');
+
+promise_test(async t => {
+  const scope = 'resources/scope/getregistrations/register-unregister';
+  const script = 'resources/empty-worker.js';
+  const registration = await service_worker_unregister_and_register(t, script, scope);
+  await registration.unregister();
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(
+      value, [], 'getRegistrations should resolve with an empty array.');
+}, 'Register then Unregister then getRegistrations');
+
+promise_test(async t => {
+  const scope = 'resources/scope/getregistrations/register-unregister-controlled';
+  const script = 'resources/empty-worker.js';
+  const registration = await service_worker_unregister_and_register(t, script, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Create a frame controlled by the service worker and unregister the
+  // worker.
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+  await registration.unregister();
+
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(
+      value, [],
+      'getRegistrations should resolve with an empty array.');
+  assert_equals(registration.installing, null);
+  assert_equals(registration.waiting, null);
+  assert_equals(registration.active.state, 'activated');
+}, 'Register then Unregister with controlled frame then getRegistrations');
+
+promise_test(async t => {
+  const host_info = get_host_info();
+  // Rewrite the url to point to remote origin.
+  const frame_same_origin_url = new URL("resources/frame-for-getregistrations.html", window.location);
+  const frame_url = host_info['HTTPS_REMOTE_ORIGIN'] + frame_same_origin_url.pathname;
+  const scope = 'resources/scope-for-getregistrations';
+  const script = 'resources/empty-worker.js';
+
+  // Loads an iframe and waits for 'ready' message from it to resolve promise.
+  // Caller is responsible for removing frame.
+  function with_iframe_ready(url) {
+      return new Promise(resolve => {
+          const frame = document.createElement('iframe');
+          frame.src = url;
+          window.addEventListener('message', function onMessage(e) {
+            window.removeEventListener('message', onMessage);
+            if (e.data == 'ready') {
+              resolve(frame);
+            }
+          });
+          document.body.appendChild(frame);
+      });
+  }
+
+  // We need this special frame loading function because the frame is going
+  // to register it's own service worker and there is the possibility that that
+  // register() finishes after the register() for the same domain later in the
+  // test. So we have to wait until the cross origin register() is done, and not
+  // just until the frame loads.
+  const frame = await with_iframe_ready(frame_url);
+  t.add_cleanup(async () => {
+    // Wait until the cross-origin worker is unregistered.
+    let resolve;
+    const channel = new MessageChannel();
+    channel.port1.onmessage = e => {
+      if (e.data == 'unregistered')
+        resolve();
+    };
+    frame.contentWindow.postMessage('unregister', '*', [channel.port2]);
+    await new Promise(r => { resolve = r; });
+
+    frame.remove();
+  });
+
+  const registrations = [
+    await service_worker_unregister_and_register(t, script, scope)];
+  t.add_cleanup(() => registrations[0].unregister());
+  const value = await navigator.serviceWorker.getRegistrations();
+  assert_array_equals(
+      value, registrations,
+      'getRegistrations should only return same origin registrations.');
+}, 'getRegistrations promise resolves only with same origin registrations.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js
new file mode 100644
index 0000000..19d7784
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js
@@ -0,0 +1,53 @@
+// META: title=serviceWorker on service worker global
+// META: global=serviceworker
+
+test(() => {
+  assert_equals(registration.installing, null, 'registration.installing');
+  assert_equals(registration.waiting, null, 'registration.waiting');
+  assert_equals(registration.active, null, 'registration.active');
+  assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+  assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state');
+  assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`);
+}, 'First run');
+
+// Cache this for later tests.
+const initialServiceWorker = self.serviceWorker;
+
+async_test((t) => {
+  assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+  serviceWorker.postMessage({ messageTest: true });
+
+  // The rest of the test runs once this receives the above message.
+  addEventListener('message', t.step_func((event) => {
+    // Ignore unrelated messages.
+    if (!event.data.messageTest) return;
+    assert_equals(event.source, serviceWorker, 'event.source');
+    t.done();
+  }));
+}, 'Can post message to self during startup');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'install' event fires.
+async_test((t) => {
+  addEventListener('install', t.step_func_done(() => {
+    assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+    assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+    assert_equals(registration.installing, serviceWorker, 'registration.installing');
+    assert_equals(registration.waiting, null, 'registration.waiting');
+    assert_equals(registration.active, null, 'registration.active');
+    assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state');
+  }));
+}, 'During install');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'activate' event fires.
+async_test((t) => {
+  addEventListener('activate', t.step_func_done(() => {
+    assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+    assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+    assert_equals(registration.installing, null, 'registration.installing');
+    assert_equals(registration.waiting, null, 'registration.waiting');
+    assert_equals(registration.active, serviceWorker, 'registration.active');
+    assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state');
+  }));
+}, 'During activate');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js
new file mode 100644
index 0000000..20b3ddf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js
@@ -0,0 +1,5 @@
+// META: global=serviceworker
+
+test((t) => {
+  assert_false('targetClientId' in FetchEvent.prototype)
+}, 'targetClientId should not be on FetchEvent');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
new file mode 100644
index 0000000..5626237
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>register on a secure page after redirect from an non-secure url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+var host_info = get_host_info();
+
+// Loads a non-secure url in a new window, which redirects to |target_url|.
+// That page then registers a service worker, and messages back with the result.
+// Returns a promise that resolves with the result.
+function redirect_and_register(target_url) {
+  var redirect_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+    'resources/redirect.py?Redirect=';
+  var child = window.open(redirect_url + encodeURIComponent(target_url));
+  return new Promise(resolve => {
+        window.addEventListener('message', e => resolve(e.data));
+      })
+    .then(function(result) {
+        child.close();
+        return result;
+      });
+}
+
+promise_test(function(t) {
+  var target_url = window.location.origin + base_path() +
+      'resources/http-to-https-redirect-and-register-iframe.html';
+
+    return redirect_and_register(target_url)
+      .then(result => {
+          assert_equals(result, 'OK');
+        });
+  }, 'register on a secure page after redirect from an non-secure url');
+
+promise_test(function(t) {
+    var target_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+      'resources/http-to-https-redirect-and-register-iframe.html';
+
+    return redirect_and_register(target_url)
+      .then(result => {
+          assert_equals(result, 'FAIL: navigator.serviceWorker is undefined');
+        });
+  }, 'register on a non-secure page after redirect from an non-secure url');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
new file mode 100644
index 0000000..e63f6b3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+let expected = ['immutable', 'immutable', 'immutable', 'immutable', 'immutable'];
+
+promise_test(t =>
+  navigator.serviceWorker.register('resources/immutable-prototype-serviceworker.js', {scope: './resources/'})
+      .then(registration => {
+    let worker = registration.installing || registration.waiting || registration.active;
+    let channel = new MessageChannel()
+    worker.postMessage(channel.port2, [channel.port2]);
+    let resolve;
+    let promise = new Promise(r => resolve = r);
+    channel.port1.onmessage = resolve;
+    return promise.then(result => assert_array_equals(expected, result.data));
+  }),
+'worker prototype chain should be immutable');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html
new file mode 100644
index 0000000..773708a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: cross-origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+    const scope = 'resources/import-scripts-cross-origin';
+    await service_worker_unregister(t, scope);
+    let reg = await navigator.serviceWorker.register(
+      'resources/import-scripts-cross-origin-worker.sub.js', { scope: scope });
+    t.add_cleanup(_ => reg.unregister());
+    assert_not_equals(reg.installing, null, 'worker is installing');
+  }, 'importScripts() supports cross-origin requests');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html
new file mode 100644
index 0000000..1679831
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: MIME types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+/**
+ * Test that a Service Worker's importScript() only accepts valid MIME types.
+ */
+let serviceWorker = null;
+
+promise_test(async t => {
+  const scope = 'resources/import-scripts-mime-types';
+  const registration = await service_worker_unregister_and_register(t,
+    'resources/import-scripts-mime-types-worker.js', scope);
+
+  add_completion_callback(() => { registration.unregister(); });
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  serviceWorker = registration.active;
+}, 'Global setup');
+
+promise_test(async t => {
+  await fetch_tests_from_worker(serviceWorker);
+}, 'Fetch importScripts tests from service worker')
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html
new file mode 100644
index 0000000..07ea494
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: redirect</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+    const scope = 'resources/import-scripts-redirect';
+    await service_worker_unregister(t, scope);
+    let reg = await navigator.serviceWorker.register(
+      'resources/import-scripts-redirect-worker.js', { scope: scope });
+    assert_not_equals(reg.installing, null, 'worker is installing');
+    await reg.unregister();
+  }, 'importScripts() supports redirects');
+
+promise_test(async t => {
+    const scope = 'resources/import-scripts-redirect';
+    await service_worker_unregister(t, scope);
+    let reg = await navigator.serviceWorker.register(
+      'resources/import-scripts-redirect-worker.js', { scope: scope });
+    assert_not_equals(reg.installing, null, 'before update');
+    await wait_for_state(t, reg.installing, 'activated');
+    await Promise.all([
+      wait_for_update(t, reg),
+      reg.update()
+    ]);
+    assert_not_equals(reg.installing, null, 'after update');
+    await reg.unregister();
+  },
+  "an imported script redirects, and the body changes during the update check");
+
+promise_test(async t => {
+    const key = token();
+    const scope = 'resources/import-scripts-redirect';
+    await service_worker_unregister(t, scope);
+    let reg = await navigator.serviceWorker.register(
+      `resources/import-scripts-redirect-on-second-time-worker.js?Key=${key}`,
+      { scope });
+    t.add_cleanup(() => reg.unregister());
+
+    assert_not_equals(reg.installing, null, 'before update');
+    await wait_for_state(t, reg.installing, 'activated');
+    await Promise.all([
+      wait_for_update(t, reg),
+      reg.update()
+    ]);
+    assert_not_equals(reg.installing, null, 'after update');
+  },
+  "an imported script doesn't redirect initially, then redirects during " +
+  "the update check and the body changes");
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html
new file mode 100644
index 0000000..4742bd0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for importScripts: script resource map</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+  <script>
+    // This test registers a worker that imports a script multiple times. The
+    // script should be stored on the first import and thereafter that stored
+    // script should be loaded. The worker asserts that the stored script was
+    // loaded; if the assert fails then registration fails.
+
+    promise_test(async t => {
+      const SCOPE = "resources/import-scripts-resource-map";
+      const SCRIPT = "resources/import-scripts-resource-map-worker.js";
+      await service_worker_unregister(t, SCOPE);
+      const registration = await navigator.serviceWorker.register(SCRIPT, {
+        scope: SCOPE
+      });
+      await registration.unregister();
+    }, "import the same script URL multiple times");
+
+    promise_test(async t => {
+      const SCOPE = "resources/import-scripts-diff-resource-map";
+      const SCRIPT = "resources/import-scripts-diff-resource-map-worker.js";
+      await service_worker_unregister(t, SCOPE);
+      const registration = await navigator.serviceWorker.register(SCRIPT, {
+        scope: SCOPE
+      });
+      await registration.unregister();
+    }, "call importScripts() with multiple arguments");
+  </script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html
new file mode 100644
index 0000000..09b4496
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts updated flag</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test registers a worker that calls importScripts at various stages of
+// service worker lifetime. The sub-tests trigger subsequent `importScript`
+// invocations via the `message` event.
+
+var register;
+
+function post_and_wait_for_reply(worker, message) {
+  return new Promise(resolve => {
+      navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+      worker.postMessage(message);
+    });
+}
+
+promise_test(function(t) {
+    const scope = 'resources/import-scripts-updated-flag';
+    let registration;
+
+    register = service_worker_unregister_and_register(
+        t, 'resources/import-scripts-updated-flag-worker.js', scope)
+      .then(r => {
+          registration = r;
+          add_completion_callback(() => { registration.unregister(); });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => {
+          // This test should not be considered complete until after the
+          // service worker has been unregistered. Currently, `testharness.js`
+          // does not support asynchronous global "tear down" logic, so this
+          // must be expressed using a dedicated `promise_test`. Because the
+          // other sub-tests in this file are declared synchronously, this test
+          // will be the final test executed.
+          promise_test(function(t) {
+              return registration.unregister();
+            });
+
+          return registration.active;
+        });
+
+    return register;
+  }, 'initialize global state');
+
+promise_test(t => {
+    return register
+      .then(function(worker) {
+          return post_and_wait_for_reply(worker, 'root-and-message');
+        })
+      .then(result => {
+          assert_equals(result.error, null);
+          assert_equals(result.value, 'root-and-message');
+        });
+  }, 'import script previously imported at worker evaluation time');
+
+promise_test(t => {
+    return register
+      .then(function(worker) {
+          return post_and_wait_for_reply(worker, 'install-and-message');
+        })
+      .then(result => {
+          assert_equals(result.error, null);
+          assert_equals(result.value, 'install-and-message');
+        });
+  }, 'import script previously imported at worker install time');
+
+promise_test(t => {
+    return register
+      .then(function(worker) {
+          return post_and_wait_for_reply(worker, 'message');
+        })
+      .then(result => {
+          assert_equals(result.error, 'NetworkError');
+          assert_equals(result.value, null);
+        });
+  }, 'import script not previously imported');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html b/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html
new file mode 100644
index 0000000..be9be49
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<title>Service Worker: Indexed DB</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function readDB() {
+  return new Promise(function(resolve, reject) {
+      var openRequest = indexedDB.open('db');
+
+      openRequest.onerror = reject;
+      openRequest.onsuccess = function() {
+          var db = openRequest.result;
+          var tx = db.transaction('store');
+          var store = tx.objectStore('store');
+          var getRequest = store.get('key');
+
+          getRequest.onerror = function() {
+              db.close();
+              reject(getRequest.error);
+            };
+          getRequest.onsuccess = function() {
+              db.close();
+              resolve(getRequest.result);
+            };
+        };
+    });
+}
+
+function send(worker, action) {
+  return new Promise(function(resolve, reject) {
+      var messageChannel = new MessageChannel();
+      messageChannel.port1.onmessage = function(event) {
+          if (event.data.type === 'error') {
+            reject(event.data.reason);
+          }
+
+          resolve();
+        };
+
+      worker.postMessage(
+        {action: action, port: messageChannel.port2},
+        [messageChannel.port2]);
+    });
+}
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html';
+
+    return service_worker_unregister_and_register(
+        t, 'resources/indexeddb-worker.js', scope)
+      .then(function(registration) {
+          var worker = registration.installing;
+
+          promise_test(function() {
+              return registration.unregister();
+            }, 'clean up: registration');
+
+          return send(worker, 'create')
+            .then(function() {
+                promise_test(function() {
+                  return new Promise(function(resolve, reject) {
+                        var delete_request = indexedDB.deleteDatabase('db');
+
+                        delete_request.onsuccess = resolve;
+                        delete_request.onerror = reject;
+                      });
+                    }, 'clean up: database');
+              })
+            .then(readDB)
+            .then(function(value) {
+                assert_equals(
+                  value, 'value',
+                  'The get() result should match what the worker put().');
+              });
+        });
+  }, 'Verify Indexed DB operation in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html b/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html
new file mode 100644
index 0000000..7e74af8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+  return new Promise(function(resolve) {
+      worker.addEventListener('statechange', function(event) {
+          if (worker.state == 'installed')
+            resolve(true);
+          else if (worker.state == 'redundant')
+            resolve(false);
+        });
+    });
+}
+
+promise_test(function(t) {
+      var script = 'resources/install-event-type-worker.js';
+      var scope = 'resources/install-event-type';
+      return service_worker_unregister_and_register(t, script, scope)
+        .then(function(registration) {
+            return wait_for_install_event(registration.installing);
+          })
+        .then(function(did_install) {
+           assert_true(did_install, 'The worker was installed');
+          })
+    }, 'install event type');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/installing.https.html b/third_party/web_platform_tests/service-workers/service-worker/installing.https.html
new file mode 100644
index 0000000..0f257b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/installing.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.installing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "installing" is set
+promise_test(async t => {
+
+  t.add_cleanup(async() => {
+    if (frame)
+      frame.remove();
+    if (registration)
+      await registration.unregister();
+  });
+
+  await service_worker_unregister(t, SCOPE);
+  const frame = await with_iframe(SCOPE);
+  const registration =
+      await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+  const container = frame.contentWindow.navigator.serviceWorker;
+  assert_equals(container.controller, null, 'controller');
+  assert_equals(registration.active, null, 'registration.active');
+  assert_equals(registration.waiting, null, 'registration.waiting');
+  assert_equals(registration.installing.scriptURL, normalizeURL(SCRIPT),
+                'registration.installing.scriptURL');
+  // FIXME: Add a test for a frame created after installation.
+  // Should the existing frame ("frame") block activation?
+}, 'installing is set');
+
+// Tests that The ServiceWorker objects returned from installing attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+  const registration1 =
+      await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+  assert_equals(registration1.installing, registration2.installing,
+                'ServiceWorkerRegistration.installing should return the ' +
+                'same object');
+  await registration1.unregister();
+}, 'The ServiceWorker objects returned from installing attribute getter that ' +
+   'represent the same service worker are the same objects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html b/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html
new file mode 100644
index 0000000..eef868c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Service Worker Global Scope Interfaces</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+// interface-requirements-worker.sub.js checks additional interface
+// requirements, on top of the basic IDL that is validated in
+// service-workers/idlharness.any.js
+service_worker_test(
+  'resources/interface-requirements-worker.sub.js',
+  'Interfaces and attributes in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html b/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html
new file mode 100644
index 0000000..1c5920f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/invalid-blobtype-iframe.https.html';
+    var SCRIPT = 'resources/invalid-blobtype-worker.js';
+    var host_info = get_host_info();
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          var channel = new MessageChannel();
+          var onMsg = new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+            });
+
+          frame.contentWindow.postMessage({},
+                                          host_info['HTTPS_ORIGIN'],
+                                          [channel.port2]);
+          return onMsg;
+        })
+      .then(function(e) {
+          assert_equals(e.data.results, 'finish');
+        });
+  }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html
new file mode 100644
index 0000000..1bc9769
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/invalid-header-iframe.https.html';
+    var SCRIPT = 'resources/invalid-header-worker.js';
+    var host_info = get_host_info();
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          var channel = new MessageChannel();
+          var onMsg = new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+            });
+          frame.contentWindow.postMessage({},
+                                          host_info['HTTPS_ORIGIN'],
+                                          [channel.port2]);
+          return onMsg;
+        })
+      .then(function(e) {
+          assert_equals(e.data.results, 'finish');
+        });
+  }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html
new file mode 100644
index 0000000..c27a5f4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/iso-latin1-header-iframe.html';
+    var SCRIPT = 'resources/iso-latin1-header-worker.js';
+    var host_info = get_host_info();
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          var channel = new MessageChannel();
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          var onMsg = new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+            });
+
+          frame.contentWindow.postMessage({},
+                                          host_info['HTTPS_ORIGIN'],
+                                          [channel.port2]);
+          return onMsg;
+        })
+      .then(function(e) {
+          assert_equals(e.data.results, 'finish');
+        });
+  }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html
new file mode 100644
index 0000000..6702abc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<title>Service Worker: local URL windows and workers inherit controller</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/local-url-inherit-controller-worker.js';
+const SCOPE = 'resources/local-url-inherit-controller-frame.html';
+
+async function doAsyncTest(t, opts) {
+  let name = `${opts.scheme}-${opts.child}-${opts.check}`;
+  let scope = SCOPE + '?name=' + name;
+  let reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+  add_completion_callback(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  let frame = await with_iframe(scope);
+  add_completion_callback(_ => frame.remove());
+  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+                    'frame should be controlled');
+
+  let result = await frame.contentWindow.checkChildController(opts);
+  result = result.data;
+
+  let expect = 'unexpected';
+  if (opts.check === 'controller') {
+    expect = opts.expect === 'inherit'
+               ? frame.contentWindow.navigator.serviceWorker.controller.scriptURL
+               : null;
+  } else if (opts.check === 'fetch') {
+    // The service worker FetchEvent handler will provide an "intercepted"
+    // body.  If the local URL ends up with an opaque origin and is not
+    // intercepted then it will get an opaque Response.  In that case it
+    // should see an empty string body.
+    expect = opts.expect === 'intercept' ? 'intercepted' : '';
+  }
+
+  assert_equals(result, expect,
+                `${opts.scheme} URL ${opts.child} should ${opts.expect} ${opts.check}`);
+}
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'blob',
+    child: 'iframe',
+    check: 'controller',
+    expect: 'inherit',
+  });
+}, 'Same-origin blob URL iframe should inherit service worker controller.');
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'blob',
+    child: 'iframe',
+    check: 'fetch',
+    expect: 'intercept',
+  });
+}, 'Same-origin blob URL iframe should intercept fetch().');
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'blob',
+    child: 'worker',
+    check: 'controller',
+    expect: 'inherit',
+  });
+}, 'Same-origin blob URL worker should inherit service worker controller.');
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'blob',
+    child: 'worker',
+    check: 'fetch',
+    expect: 'intercept',
+  });
+}, 'Same-origin blob URL worker should intercept fetch().');
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'data',
+    child: 'iframe',
+    check: 'fetch',
+    expect: 'not intercept',
+  });
+}, 'Data URL iframe should not intercept fetch().');
+
+promise_test(function(t) {
+  // Data URLs should result in an opaque origin and should probably not
+  // have access to a cross-origin service worker.  See:
+  //
+  // https://github.com/w3c/ServiceWorker/issues/1262
+  //
+  return doAsyncTest(t, {
+    scheme: 'data',
+    child: 'worker',
+    check: 'controller',
+    expect: 'not inherit',
+  });
+}, 'Data URL worker should not inherit service worker controller.');
+
+promise_test(function(t) {
+  return doAsyncTest(t, {
+    scheme: 'data',
+    child: 'worker',
+    check: 'fetch',
+    expect: 'not intercept',
+  });
+}, 'Data URL worker should not intercept fetch().');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html b/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html
new file mode 100644
index 0000000..8175bcd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: MIME sniffing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    const SCOPE = 'resources/blank.html?mime-sniffing';
+    const SCRIPT = 'resources/mime-sniffing-worker.js';
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(registration => {
+          add_completion_callback(() => registration.unregister());
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(_ => with_iframe(SCOPE))
+      .then(frame => {
+          add_completion_callback(() => frame.remove());
+          assert_equals(frame.contentWindow.document.body.innerText, 'test');
+          const h1 = frame.contentWindow.document.getElementById('testid');
+          assert_equals(h1.innerText,'test');
+        });
+  }, 'The response from service worker should be correctly MIME siniffed.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html
new file mode 100644
index 0000000..82a48d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js
new file mode 100644
index 0000000..e673292
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js
@@ -0,0 +1 @@
+// Service worker for current/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
new file mode 100644
index 0000000..4585f15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.https.html" id="c"></iframe>
+<iframe src="../relevant/relevant.https.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+const current = document.querySelector('#c').contentWindow;
+const relevant = document.querySelector('#r').contentWindow;
+
+window.testRegister = options => {
+    return current.navigator.serviceWorker.register.call(relevant.navigator.serviceWorker, 'test-sw.js', options);
+};
+
+window.testGetRegistration = () => {
+    return current.navigator.serviceWorker.getRegistration.call(relevant.navigator.serviceWorker, 'test-sw.js');
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
new file mode 100644
index 0000000..e2a0e93
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
@@ -0,0 +1 @@
+// Service worker for incumbent/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
new file mode 100644
index 0000000..44f42ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
new file mode 100644
index 0000000..ff44cdf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
@@ -0,0 +1 @@
+// Service worker for relevant/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js
new file mode 100644
index 0000000..ce3c940
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js
@@ -0,0 +1 @@
+// Service worker for /
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html
new file mode 100644
index 0000000..b9dfe36
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>register()/getRegistration() URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-register-method">
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-getregistration-method">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="../resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.https.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+    window.addEventListener('load', () => resolve());
+});
+
+promise_test(t => {
+    let registration;
+
+    return loadPromise.then(() => {
+        return frames[0].testRegister();
+    }).then(r => {
+        registration = r;
+        return wait_for_state(t, registration.installing, 'activated');
+    }).then(_ => {
+        assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+        assert_equals(registration.scope, normalizeURL('relevant/'), 'the default scope URL should be parsed against the parsed script URL');
+
+        return registration.unregister();
+    });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the default scope URL');
+
+promise_test(t => {
+    let registration;
+
+    return loadPromise.then(() => {
+        return frames[0].testRegister({ scope: 'scope' });
+    }).then(r => {
+        registration = r;
+        return wait_for_state(t, registration.installing, 'activated');
+    }).then(_ => {
+        assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+        assert_equals(registration.scope, normalizeURL('relevant/scope'), 'the given scope URL should be parsed against the relevant global');
+
+        return registration.unregister();
+    });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the given scope URL');
+
+promise_test(t => {
+    let registration;
+
+    return loadPromise.then(() => {
+        return navigator.serviceWorker.register(normalizeURL('relevant/test-sw.js'));
+    }).then(r => {
+        registration = r;
+        return frames[0].testGetRegistration();
+    })
+    .then(gottenRegistration => {
+        assert_not_equals(registration, null, 'the registration should not be null');
+        assert_not_equals(gottenRegistration, null, 'the registration from the other frame should not be null');
+        assert_equals(gottenRegistration.scope, registration.scope,
+            'the retrieved registration\'s scope should be equal to the original\'s scope');
+
+        return registration.unregister();
+    });
+}, 'getRegistration should use the relevant global of the object it was called on to resolve the script URL');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html b/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html
new file mode 100644
index 0000000..00c20d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<title>Tests for cross-origin multipart image returned by service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+// This tests loading a multipart image via service worker. The service worker responds with
+// an opaque or a non-opaque response. The content of opaque response should not be readable.
+
+const script = 'resources/multipart-image-worker.js';
+const scope = 'resources/multipart-image-iframe.html';
+let frame;
+
+function check_image_data(data) {
+    assert_equals(data[0], 255);
+    assert_equals(data[1], 0);
+    assert_equals(data[2], 0);
+    assert_equals(data[3], 255);
+}
+
+promise_test(t => {
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          promise_test(() => {
+              if (frame) {
+                  frame.remove();
+              }
+              return registration.unregister();
+            }, 'restore global state');
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => with_iframe(scope))
+      .then(f => {
+          frame = f;
+      });
+  }, 'initialize global state');
+
+promise_test(t => {
+    return frame.contentWindow.load_multipart_image('same-origin-multipart-image')
+      .then(img => frame.contentWindow.get_image_data(img))
+      .then(img_data => {
+          check_image_data(img_data.data);
+      });
+  }, 'same-origin multipart image via SW should be readable');
+
+promise_test(t => {
+    return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-approved')
+      .then(img => frame.contentWindow.get_image_data(img))
+      .then(img_data => {
+          check_image_data(img_data.data);
+      });
+  }, 'cross-origin multipart image via SW with approved CORS should be readable');
+
+promise_test(t => {
+    return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-no-cors')
+      .then(img => {
+        assert_throws_dom('SecurityError', frame.contentWindow.DOMException,
+                          () => frame.contentWindow.get_image_data(img));
+      });
+  }, 'cross-origin multipart image with no-cors via SW should not be readable');
+
+promise_test(t => {
+    const promise = frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-rejected');
+    return promise_rejects_dom(t, 'NetworkError', frame.contentWindow.DOMException, promise);
+  }, 'cross-origin multipart image via SW with rejected CORS should fail to load');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html
new file mode 100644
index 0000000..752e132
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+async_test(function(t) {
+  var scope = 'resources/scope/subsequent-register-from-same-window';
+  var registration;
+
+  service_worker_unregister_and_register(t, worker_url, scope)
+    .then(function(r) {
+        registration = r;
+        return wait_for_state(t, r.installing, 'activated');
+      })
+    .then(function() {
+        return navigator.serviceWorker.register(worker_url, { scope: scope });
+      })
+    .then(function(new_registration) {
+        assert_equals(new_registration, registration,
+                      'register should resolve to the same registration');
+        assert_equals(new_registration.active, registration.active,
+                      'register should resolve to the same worker');
+        assert_equals(new_registration.active.state, 'activated',
+                      'the worker should be in state "activated"');
+        return registration.unregister();
+      })
+    .then(function() { t.done(); })
+    .catch(unreached_rejection(t));
+}, 'Subsequent registrations resolve to the same registration object');
+
+async_test(function(t) {
+  var scope = 'resources/scope/subsequent-register-from-different-iframe';
+  var frame;
+  var registration;
+
+  service_worker_unregister_and_register(t, worker_url, scope)
+    .then(function(r) {
+        registration = r;
+        return wait_for_state(t, r.installing, 'activated');
+      })
+    .then(function() { return with_iframe('resources/404.py'); })
+    .then(function(f) {
+        frame = f;
+        return frame.contentWindow.navigator.serviceWorker.register(
+            'empty-worker.js',
+            { scope: 'scope/subsequent-register-from-different-iframe' });
+      })
+    .then(function(new_registration) {
+        assert_not_equals(
+          registration, new_registration,
+          'register should resolve to a different registration');
+        assert_equals(
+          registration.scope, new_registration.scope,
+          'registrations should have the same scope');
+
+        assert_equals(
+          registration.installing, null,
+          'installing worker should be null');
+        assert_equals(
+          new_registration.installing, null,
+          'installing worker should be null');
+        assert_equals(
+          registration.waiting, null,
+          'waiting worker should be null')
+        assert_equals(
+          new_registration.waiting, null,
+          'waiting worker should be null')
+
+        assert_not_equals(
+          registration.active, new_registration.active,
+          'registration should have a different active worker');
+        assert_equals(
+          registration.active.scriptURL,
+          new_registration.active.scriptURL,
+          'active workers should have the same script URL');
+        assert_equals(
+          registration.active.state,
+          new_registration.active.state,
+          'active workers should be in the same state');
+
+        frame.remove();
+        return registration.unregister();
+      })
+    .then(function() { t.done(); })
+    .catch(unreached_rejection(t));
+}, 'Subsequent registrations from a different iframe resolve to the ' +
+       'different registration object but they refer to the same ' +
+       'registration and workers');
+
+async_test(function(t) {
+  var scope = 'resources/scope/concurrent-register';
+
+  service_worker_unregister(t, scope)
+    .then(function() {
+        var promises = [];
+        for (var i = 0; i < 10; ++i) {
+          promises.push(navigator.serviceWorker.register(worker_url,
+                                                         { scope: scope }));
+        }
+        return Promise.all(promises);
+      })
+    .then(function(registrations) {
+        registrations.forEach(function(registration) {
+            assert_equals(registration, registrations[0],
+                          'register should resolve to the same registration');
+          });
+        return registrations[0].unregister();
+      })
+    .then(function() { t.done(); })
+    .catch(unreached_rejection(t));
+}, 'Concurrent registrations resolve to the same registration object');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html b/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html
new file mode 100644
index 0000000..6a83f73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!-- In Bug 1217367, we will try to merge update events for same registration
+     if possible. This testcase is used to make sure the optimization algorithm
+     doesn't go wrong. -->
+<title>Service Worker: Trigger multiple updates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var script = 'resources/update-nocookie-worker.py';
+    var scope = 'resources/scope/update';
+    var expected_url = normalizeURL(script);
+    var registration;
+
+    return service_worker_unregister_and_register(t, expected_url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          // Test single update works before triggering multiple update events
+          return Promise.all([registration.update(),
+                              wait_for_update(t, registration)]);
+        })
+      .then(function() {
+          assert_equals(registration.installing.scriptURL, expected_url,
+                        'new installing should be set after update resolves.');
+          assert_equals(registration.waiting, null,
+                        'waiting should still be null after update resolves.');
+          assert_equals(registration.active.scriptURL, expected_url,
+                        'active should still exist after update found.');
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'installing should be null after installing.');
+          if (registration.waiting) {
+            assert_equals(registration.waiting.scriptURL, expected_url,
+                          'waiting should be set after installing.');
+            assert_equals(registration.active.scriptURL, expected_url,
+                          'active should still exist after installing.');
+            return wait_for_state(t, registration.waiting, 'activated');
+          }
+        })
+      .then(function() {
+          // Test triggering multiple update events at the same time.
+          var promiseList = [];
+          const burstUpdateCount = 10;
+          for (var i = 0; i < burstUpdateCount; i++) {
+            promiseList.push(registration.update());
+          }
+          promiseList.push(wait_for_update(t, registration));
+          return Promise.all(promiseList);
+        })
+      .then(function() {
+          assert_equals(registration.installing.scriptURL, expected_url,
+                        'new installing should be set after update resolves.');
+          assert_equals(registration.waiting, null,
+                        'waiting should still be null after update resolves.');
+          assert_equals(registration.active.scriptURL, expected_url,
+                        'active should still exist after update found.');
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'installing should be null after installing.');
+          if (registration.waiting) {
+            assert_equals(registration.waiting.scriptURL, expected_url,
+                          'waiting should be set after installing.');
+            assert_equals(registration.active.scriptURL, expected_url,
+                          'active should still exist after installing.');
+            return wait_for_state(t, registration.waiting, 'activated');
+          }
+        })
+      .then(function() {
+          // Test update still works after handling update event burst.
+          return Promise.all([registration.update(),
+                              wait_for_update(t, registration)]);
+        })
+      .then(function() {
+          assert_equals(registration.installing.scriptURL, expected_url,
+                        'new installing should be set after update resolves.');
+          assert_equals(registration.waiting, null,
+                        'waiting should be null after activated.');
+          assert_equals(registration.active.scriptURL, expected_url,
+                        'active should still exist after update found.');
+        });
+  }, 'Trigger multiple updates.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html
new file mode 100644
index 0000000..46d32a4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigate a Window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+
+function wait_for_message(msg) {
+  return new Promise(function(resolve, reject) {
+    window.addEventListener('message', function onMsg(evt) {
+      if (evt.data.type === msg) {
+        resolve();
+      }
+    });
+  });
+}
+
+function with_window(url) {
+  var win = window.open(url);
+  return wait_for_message('LOADED').then(_ => win);
+}
+
+function navigate_window(win, url) {
+  win.location = url;
+  return wait_for_message('LOADED').then(_ => win);
+}
+
+function reload_window(win) {
+  win.location.reload();
+  return wait_for_message('LOADED').then(_ => win);
+}
+
+function go_back(win) {
+  win.history.back();
+  return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function go_forward(win) {
+  win.history.forward();
+  return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function get_clients(win, sw, opts) {
+  return new Promise((resolve, reject) => {
+    win.navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+      win.navigator.serviceWorker.removeEventListener('message', onMsg);
+      if (evt.data.type === 'success') {
+        resolve(evt.data.detail);
+      } else {
+        reject(evt.data.detail);
+      }
+    });
+    sw.postMessage({ type: 'GET_CLIENTS', opts: (opts || {}) });
+  });
+}
+
+function compare_urls(a, b) {
+  return a.url < b.url ? -1 : b.url < a.url ? 1 : 0;
+}
+
+function validate_window(win, url, opts) {
+  return win.navigator.serviceWorker.getRegistration(url)
+    .then(reg => {
+        // In order to compare service worker instances we need to
+        // make sure the DOM object is owned by the same global; the
+        // opened window in this case.
+        assert_equals(win.navigator.serviceWorker.controller, reg.active,
+                      'window should be controlled by service worker');
+        return get_clients(win, reg.active, opts);
+      })
+    .then(resultList => {
+        // We should always see our controlled window.
+        var expected = [
+          { url: url, frameType: 'auxiliary' }
+        ];
+        // If we are including uncontrolled windows, then we might see the
+        // test window itself and the test harness.
+        if (opts.includeUncontrolled) {
+          expected.push({ url: BASE_URL + 'navigate-window.https.html',
+                          frameType: 'auxiliary' });
+          expected.push({
+            url: host_info['HTTPS_ORIGIN'] + '/testharness_runner.html',
+            frameType: 'top-level' });
+        }
+
+        assert_equals(resultList.length, expected.length,
+                      'expected number of clients');
+
+        expected.sort(compare_urls);
+        resultList.sort(compare_urls);
+
+        for (var i = 0; i < resultList.length; ++i) {
+          assert_equals(resultList[i].url, expected[i].url,
+                        'client should have expected url');
+          assert_equals(resultList[i].frameType, expected[i].frameType,
+                        'client should have expected frame type');
+        }
+        return win;
+      })
+}
+
+promise_test(function(t) {
+    var worker = BASE_URL + 'resources/navigate-window-worker.js';
+    var scope = BASE_URL + 'resources/loaded.html?navigate-window-controlled';
+    var url1 = scope + '&q=1';
+    var url2 = scope + '&q=2';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(reg => wait_for_state(t, reg.installing, 'activated') )
+      .then(___ => with_window(url1))
+      .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+      .then(win => navigate_window(win, url2))
+      .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+      .then(win => go_back(win))
+      .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+      .then(win => go_forward(win))
+      .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+      .then(win => reload_window(win))
+      .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+      .then(win => win.close())
+      .catch(unreached_rejection(t))
+      .then(___ => service_worker_unregister(t, scope))
+  }, 'Clients.matchAll() should not show an old window as controlled after ' +
+     'it navigates.');
+
+promise_test(function(t) {
+    var worker = BASE_URL + 'resources/navigate-window-worker.js';
+    var scope = BASE_URL + 'resources/loaded.html?navigate-window-uncontrolled';
+    var url1 = scope + '&q=1';
+    var url2 = scope + '&q=2';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(reg => wait_for_state(t, reg.installing, 'activated') )
+      .then(___ => with_window(url1))
+      .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+      .then(win => navigate_window(win, url2))
+      .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+      .then(win => go_back(win))
+      .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+      .then(win => go_forward(win))
+      .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+      .then(win => reload_window(win))
+      .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+      .then(win => win.close())
+      .catch(unreached_rejection(t))
+      .then(___ => service_worker_unregister(t, scope))
+  }, 'Clients.matchAll() should not show an old window after it navigates.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html
new file mode 100644
index 0000000..a4b5203
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html
@@ -0,0 +1,819 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation Post Request Origin Header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const script = new URL('./resources/fetch-rewrite-worker.js', self.location);
+const base = './resources/navigation-headers-server.py';
+const scope = base + '?with-sw';
+let registration;
+
+async function post_and_get_headers(t, form_host, method, swaction,
+                                    redirect_hosts=[]) {
+  if (swaction === 'navpreload') {
+    assert_true('navigationPreload' in registration,
+                'navigation preload must be supported');
+  }
+  let target_string;
+  if (swaction === 'no-sw') {
+    target_string = base + '?no-sw';
+  } else if (swaction === 'fallback') {
+    target_string = `${scope}&ignore`;
+  } else {
+    target_string = `${scope}&${swaction}`;
+  }
+  let target = new URL(target_string, self.location);
+
+  for (let i = redirect_hosts.length - 1; i >= 0; --i) {
+    const redirect_url = new URL('./resources/redirect.py', self.location);
+    redirect_url.hostname = redirect_hosts[i];
+    redirect_url.search = `?Status=307&Redirect=${encodeURIComponent(target)}`;
+    target = redirect_url;
+  }
+
+  let popup_url_path;
+  if (method === 'GET') {
+    popup_url_path = './resources/location-setter.html';
+  } else if (method === 'POST') {
+    popup_url_path = './resources/form-poster.html';
+  }
+
+  const popup_url = new URL(popup_url_path, self.location);
+  popup_url.hostname = form_host;
+  popup_url.search = `?target=${encodeURIComponent(target.href)}`;
+
+  const message_promise = new Promise(resolve => {
+    self.addEventListener('message', evt => {
+      resolve(evt.data);
+    });
+  });
+
+  const frame = await with_iframe(popup_url);
+  t.add_cleanup(() => frame.remove());
+
+  return await message_promise;
+}
+
+const SAME_ORIGIN = new URL(self.location.origin);
+const SAME_SITE = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+const CROSS_SITE = new URL(get_host_info().HTTPS_NOTSAMESITE_ORIGIN);
+
+promise_test(async t => {
+  registration = await service_worker_unregister_and_register(t, script, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+  if (registration.navigationPreload)
+    await registration.navigationPreload.enable();
+}, 'Setup service worker');
+
+//
+// Origin and referer headers
+//
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+   'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+   'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+   'origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+   'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result.origin, 'not set', 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+   'request sets correct origin and referer headers.');
+
+//
+// Origin and referer header tests using redirects
+//
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'no-sw', [SAME_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and no service worker ' +
+   'sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'passthrough', [SAME_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and passthrough service ' +
+   'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'fallback', [SAME_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and fallback service ' +
+   'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'change-request', [SAME_SITE.hostname]);
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and change-request service ' +
+   'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'no-sw', [CROSS_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and no service worker ' +
+   'sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'passthrough', [CROSS_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and passthrough service ' +
+   'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'fallback', [CROSS_SITE.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and fallback service ' +
+   'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'change-request', [CROSS_SITE.hostname]);
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and change-request service ' +
+   'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'no-sw', [CROSS_SITE.hostname,
+                                                      SAME_ORIGIN.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and no service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'passthrough', [CROSS_SITE.hostname,
+                                                            SAME_ORIGIN.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and passthrough service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'fallback', [CROSS_SITE.hostname,
+                                                         SAME_ORIGIN.hostname]);
+  assert_equals(result.origin, 'null', 'origin header');
+  assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and fallback service worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'change-request', [CROSS_SITE.hostname,
+                                                               SAME_ORIGIN.hostname]);
+  assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+  assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and change-request service worker sets correct origin and referer headers.');
+
+//
+// Sec-Fetch-* Headers (separated since not all browsers implement them)
+//
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+   'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+   'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'no-sw');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'passthrough');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'fallback');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+   'sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'navpreload');
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+   'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+                                            'change-request');
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+   'request sets correct sec-fetch headers.');
+
+//
+// Sec-Fetch-* header tests using redirects
+//
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'no-sw', [SAME_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and no service worker ' +
+   'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'passthrough', [SAME_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and passthrough service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'fallback', [SAME_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and fallback service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'navpreload', [SAME_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and navpreload service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'change-request', [SAME_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and change-request service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'no-sw', [CROSS_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and no service worker ' +
+   'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'passthrough', [CROSS_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and passthrough service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'fallback', [CROSS_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and fallback service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'navpreload', [CROSS_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and navpreload service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'change-request', [CROSS_SITE.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and change-request service ' +
+   'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'no-sw', [CROSS_SITE.hostname,
+                                                      SAME_ORIGIN.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and no service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'passthrough', [CROSS_SITE.hostname,
+                                                            SAME_ORIGIN.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and passthrough service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'fallback', [CROSS_SITE.hostname,
+                                                         SAME_ORIGIN.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and fallback service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'navpreload', [CROSS_SITE.hostname,
+                                                           SAME_ORIGIN.hostname]);
+  assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and navpreload service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+                                            'change-request', [CROSS_SITE.hostname,
+                                                               SAME_ORIGIN.hostname]);
+  assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+  assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+  assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+   'and change-request service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+  await registration.unregister();
+}, 'Cleanup service worker');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
new file mode 100644
index 0000000..ec74282
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/broken-chunked-encoding-worker.js';
+    var scope = 'resources/broken-chunked-encoding-scope.asis';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(_ => registration.unregister());
+          var worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(_ => with_iframe(scope))
+      .then(frame => {
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'PASS: preloadResponse resolved');
+        });
+  }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding.');
+
+promise_test(t => {
+    var script = 'resources/broken-chunked-encoding-worker.js';
+    var scope = 'resources/chunked-encoding-scope.py?use_broken_body';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(_ => registration.unregister());
+          var worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(_ => with_iframe(scope))
+      .then(frame => {
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            'PASS: preloadResponse resolved');
+        });
+  }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding with some delays');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
new file mode 100644
index 0000000..830ce32
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/chunked-encoding-worker.js';
+    var scope = 'resources/chunked-encoding-scope.py';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(_ => registration.unregister());
+          var worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(_ => with_iframe(scope))
+      .then(frame => {
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            '0123456789');
+        });
+  }, 'Navigation Preload must work with chunked encoding.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
new file mode 100644
index 0000000..7e8aacd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload empty response body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/empty-preload-response-body-worker.js';
+    var scope = 'resources/empty-preload-response-body-scope.html';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(_ => registration.unregister());
+          var worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(_ => with_iframe(scope))
+      .then(frame => {
+          assert_equals(
+            frame.contentDocument.body.textContent,
+            '[]');
+        });
+  }, 'Navigation Preload empty response body.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html
new file mode 100644
index 0000000..08e2f49
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>NavigationPreloadManager.getState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+function post_and_wait_for_reply(worker, message) {
+  return new Promise(resolve => {
+      navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+      worker.postMessage(message);
+    });
+}
+
+promise_test(t => {
+  const scope = '../resources/get-state';
+  const script = '../resources/empty-worker.js';
+  var np;
+
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(r => {
+        np = r.navigationPreload;
+        add_completion_callback(() => r.unregister());
+        return wait_for_state(t, r.installing, 'activated');
+      })
+    .then(() => np.getState())
+    .then(state => {
+        expect_navigation_preload_state(state, false, 'true', 'default state');
+        return np.enable();
+      })
+    .then(result => {
+        assert_equals(result, undefined,
+                      'enable() should resolve to undefined');
+        return np.getState();
+      })
+    .then(state => {
+        expect_navigation_preload_state(state, true, 'true',
+                                        'state after enable()');
+        return np.disable();
+      })
+    .then(result => {
+        assert_equals(result, undefined,
+                      'disable() should resolve to undefined');
+        return np.getState();
+      })
+    .then(state => {
+        expect_navigation_preload_state(state, false, 'true',
+                                        'state after disable()');
+        return np.setHeaderValue('dreams that cannot be');
+      })
+    .then(result => {
+        assert_equals(result, undefined,
+                      'setHeaderValue() should resolve to undefined');
+        return np.getState();
+      })
+    .then(state => {
+        expect_navigation_preload_state(state, false, 'dreams that cannot be',
+                                        'state after setHeaderValue()');
+        return np.setHeaderValue('').then(() => np.getState());
+      })
+    .then(state => {
+        expect_navigation_preload_state(state, false, '',
+                                        'after setHeaderValue to empty string');
+        return np.setHeaderValue(null).then(() => np.getState());
+      })
+    .then(state => {
+        expect_navigation_preload_state(state, false, 'null',
+                                        'after setHeaderValue to null');
+        return promise_rejects_js(t,
+            TypeError,
+            np.setHeaderValue('what\uDC00\uD800this'),
+            'setHeaderValue() should throw if passed surrogates');
+      })
+    .then(() => {
+        return promise_rejects_js(t,
+            TypeError,
+            np.setHeaderValue('zer\0o'),
+            'setHeaderValue() should throw if passed \\0');
+      })
+    .then(() => {
+        return promise_rejects_js(t,
+            TypeError,
+            np.setHeaderValue('\rcarriage'),
+            'setHeaderValue() should throw if passed \\r');
+      })
+    .then(() => {
+        return promise_rejects_js(t,
+            TypeError,
+            np.setHeaderValue('newline\n'),
+            'setHeaderValue() should throw if passed \\n');
+      })
+    .then(() => {
+        return promise_rejects_js(t,
+            TypeError,
+            np.setHeaderValue(),
+            'setHeaderValue() should throw if passed undefined');
+      })
+    .then(() => np.enable().then(() => np.getState()))
+    .then(state => {
+        expect_navigation_preload_state(state, true, 'null',
+                                        'enable() should not change header');
+      });
+  }, 'getState');
+
+// This test sends commands to a worker to call enable()/disable()/getState().
+// It checks the results from the worker and verifies that they match the
+// navigation preload state accessible from the page.
+promise_test(t => {
+  const scope = 'resources/get-state-worker';
+  const script = 'resources/get-state-worker.js';
+  var worker;
+  var registration;
+
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(r => {
+        registration = r;
+        add_completion_callback(() => registration.unregister());
+        worker = registration.installing;
+        return wait_for_state(t, worker, 'activated');
+      })
+    .then(() => {
+        // Call getState().
+        return post_and_wait_for_reply(worker, 'getState');
+      })
+    .then(data => {
+        return Promise.all([data, registration.navigationPreload.getState()]);
+      })
+    .then(states => {
+        expect_navigation_preload_state(states[0], false, 'true',
+                                        'default state (from worker)');
+        expect_navigation_preload_state(states[1], false, 'true',
+                                        'default state (from page)');
+        // Call enable() and then getState().
+        return post_and_wait_for_reply(worker, 'enable');
+      })
+    .then(data => {
+        assert_equals(data, undefined, 'enable() should resolve to undefined');
+        return Promise.all([
+            post_and_wait_for_reply(worker, 'getState'),
+            registration.navigationPreload.getState()
+          ]);
+      })
+    .then(states => {
+        expect_navigation_preload_state(states[0], true, 'true',
+                                        'state after enable() (from worker)');
+        expect_navigation_preload_state(states[1], true, 'true',
+                                        'state after enable() (from page)');
+        // Call disable() and then getState().
+        return post_and_wait_for_reply(worker, 'disable');
+      })
+    .then(data => {
+        assert_equals(data, undefined,
+                      '.disable() should resolve to undefined');
+        return Promise.all([
+            post_and_wait_for_reply(worker, 'getState'),
+            registration.navigationPreload.getState()
+          ]);
+      })
+    .then(states => {
+        expect_navigation_preload_state(states[0], false, 'true',
+                                        'state after disable() (from worker)');
+        expect_navigation_preload_state(states[1], false, 'true',
+                                        'state after disable() (from page)');
+        return post_and_wait_for_reply(worker, 'setHeaderValue');
+      })
+    .then(data => {
+        assert_equals(data, undefined,
+                      '.setHeaderValue() should resolve to undefined');
+        return Promise.all([
+            post_and_wait_for_reply(worker, 'getState'),
+            registration.navigationPreload.getState()]);
+      })
+    .then(states => {
+        expect_navigation_preload_state(
+            states[0], false, 'insightful',
+            'state after setHeaderValue() (from worker)');
+        expect_navigation_preload_state(
+            states[1], false, 'insightful',
+            'state after setHeaderValue() (from page)');
+      });
+  }, 'getState from a worker');
+
+// This tests navigation preload API when there is no active worker. It calls
+// the API from the main page and then from the worker itself.
+promise_test(t => {
+  const scope = 'resources/wait-for-activate-worker';
+  const script = 'resources/wait-for-activate-worker.js';
+  var registration;
+  var np;
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(r => {
+        registration = r;
+        np = registration.navigationPreload;
+        add_completion_callback(() => registration.unregister());
+        return Promise.all([
+            promise_rejects_dom(
+                t, 'InvalidStateError', np.enable(),
+                'enable should reject if there is no active worker'),
+            promise_rejects_dom(
+                t, 'InvalidStateError', np.disable(),
+                'disable should reject if there is no active worker'),
+            promise_rejects_dom(
+                t, 'InvalidStateError', np.setHeaderValue('umm'),
+                'setHeaderValue should reject if there is no active worker')]);
+      })
+    .then(() => np.getState())
+    .then(state => {
+        expect_navigation_preload_state(state, false, 'true',
+                                        'state before activation');
+        return post_and_wait_for_reply(registration.installing, 'ping');
+      })
+    .then(result => assert_equals(result, 'PASS'));
+  }, 'no active worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
new file mode 100644
index 0000000..392e5c1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ServiceWorker: navigator.serviceWorker.navigationPreload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+promise_test(async t => {
+  const SCRIPT = '../resources/empty-worker.js';
+  const SCOPE = '../resources/navigationpreload';
+  const registration =
+      await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  const navigationPreload = registration.navigationPreload;
+  assert_true(navigationPreload instanceof NavigationPreloadManager,
+              'ServiceWorkerRegistration.navigationPreload');
+  await registration.unregister();
+}, "The navigationPreload attribute must return service worker " +
+    "registration's NavigationPreloadManager object.");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html
new file mode 100644
index 0000000..5970f05
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload redirect response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_opaqueredirect(response_info, scope) {
+  assert_equals(response_info.type, 'opaqueredirect');
+  assert_equals(response_info.url, '' + new URL(scope, location));
+  assert_equals(response_info.status, 0);
+  assert_equals(response_info.ok, false);
+  assert_equals(response_info.statusText, '');
+  assert_equals(response_info.headers.length, 0);
+}
+
+function redirect_response_test(t, scope, expected_body, expected_urls) {
+  var script = 'resources/redirect-worker.js';
+  var registration;
+  var message_resolvers = [];
+  function wait_for_message(count) {
+    var promises = [];
+    message_resolvers = [];
+    for (var i = 0; i < count; ++i) {
+      promises.push(new Promise(resolve => message_resolvers.push(resolve)));
+    }
+    return promises;
+  }
+  function on_message(e) {
+    var resolve = message_resolvers.shift();
+    if (resolve)
+      resolve(e.data);
+  }
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(reg => {
+        registration = reg;
+        add_completion_callback(_ => registration.unregister());
+        var worker = registration.installing;
+        return wait_for_state(t, worker, 'activated');
+      })
+    .then(_ => with_iframe(scope + '&base'))
+    .then(frame => {
+        assert_equals(frame.contentDocument.body.textContent, 'OK');
+        frame.contentWindow.navigator.serviceWorker.onmessage = on_message;
+        return Promise.all(wait_for_message(expected_urls.length)
+                               .concat(with_iframe(scope)));
+      })
+    .then(results => {
+      var frame = results[expected_urls.length];
+      assert_equals(frame.contentDocument.body.textContent, expected_body);
+      for (var i = 0; i < expected_urls.length; ++i) {
+        check_opaqueredirect(results[i], expected_urls[i]);
+      }
+      frame.remove();
+      return registration.unregister();
+    });
+}
+
+promise_test(t => {
+    return redirect_response_test(
+        t,
+        'resources/redirect-scope.py?type=normal',
+        'redirected\n',
+        ['resources/redirect-scope.py?type=normal']);
+  }, 'Navigation Preload redirect response.');
+
+promise_test(t => {
+    return redirect_response_test(
+        t,
+        'resources/redirect-scope.py?type=no-location',
+        '',
+        ['resources/redirect-scope.py?type=no-location']);
+  }, 'Navigation Preload no-location redirect response.');
+
+promise_test(t => {
+    return redirect_response_test(
+        t,
+        'resources/redirect-scope.py?type=no-location-with-body',
+        'BODY',
+        ['resources/redirect-scope.py?type=no-location-with-body']);
+  }, 'Navigation Preload no-location redirect response with body.');
+
+promise_test(t => {
+    return redirect_response_test(
+        t,
+        'resources/redirect-scope.py?type=redirect-to-scope',
+        'redirected\n',
+        ['resources/redirect-scope.py?type=redirect-to-scope',
+         'resources/redirect-scope.py?type=redirect-to-scope2',
+         'resources/redirect-scope.py?type=redirect-to-scope3',]);
+  }, 'Navigation Preload redirect to the same scope.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html
new file mode 100644
index 0000000..0964201
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload request headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/request-headers-worker.js';
+    var scope = 'resources/request-headers-scope.py';
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(_ => registration.unregister());
+          var worker = registration.installing;
+          return wait_for_state(t, worker, 'activated');
+        })
+      .then(_ => with_iframe(scope))
+      .then(frame => {
+          var headers = JSON.parse(frame.contentDocument.body.textContent);
+          assert_true(
+            'SERVICE-WORKER-NAVIGATION-PRELOAD' in headers,
+            'The Navigation Preload request must specify a ' +
+            '"Service-Worker-Navigation-Preload" header.');
+          assert_array_equals(
+            headers['SERVICE-WORKER-NAVIGATION-PRELOAD'],
+            ['hello'],
+            'The Navigation Preload request must specify the correct value ' +
+            'for the "Service-Worker-Navigation-Preload" header.');
+          assert_true(
+            'UPGRADE-INSECURE-REQUESTS' in headers,
+            'The Navigation Preload request must specify an ' +
+            '"Upgrade-Insecure-Requests" header.');
+          assert_array_equals(
+            headers['UPGRADE-INSECURE-REQUESTS'],
+            ['1'],
+            'The Navigation Preload request must specify the correct value ' +
+            'for the "Upgrade-Insecure-Requests" header.');
+        });
+  }, 'Navigation Preload request headers.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
new file mode 100644
index 0000000..b4756d0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Resource Timing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_timing_entry(entry, url, decodedBodySize, encodedBodySize) {
+  assert_equals(entry.name, url, 'The entry name of '+ url);
+
+  assert_equals(
+    entry.entryType, 'resource',
+    'The entryType of preload response timing entry must be "resource' +
+    '" :' + url);
+  assert_equals(
+    entry.initiatorType, 'navigation',
+    'The initiatorType of preload response timing entry must be ' +
+    '"navigation":' + url);
+
+  // If the server returns the redirect response, |decodedBodySize| is null and
+  // |entry.decodedBodySize| shuld be 0. Otherwise |entry.decodedBodySize| must
+  // same as |decodedBodySize|
+  assert_equals(
+    entry.decodedBodySize, Number(decodedBodySize),
+    'decodedBodySize must same as the decoded size in the server:' + url);
+
+  // If the server returns the redirect response, |encodedBodySize| is null and
+  // |entry.encodedBodySize| shuld be 0. Otherwise |entry.encodedBodySize| must
+  // same as |encodedBodySize|
+  assert_equals(
+    entry.encodedBodySize, Number(encodedBodySize),
+    'encodedBodySize must same as the encoded size in the server:' + url);
+
+  assert_greater_than(
+    entry.transferSize, entry.decodedBodySize,
+    'transferSize must greater then encodedBodySize.');
+
+  assert_greater_than(entry.startTime, 0, 'startTime of ' + url);
+  assert_greater_than_equal(entry.fetchStart, entry.startTime,
+                            'fetchStart >= startTime of ' + url);
+  assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart,
+                            'domainLookupStart >= fetchStart of ' + url);
+  assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart,
+                            'domainLookupEnd >= domainLookupStart of ' + url);
+  assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd,
+                            'connectStart >= domainLookupEnd of ' + url);
+  assert_greater_than_equal(entry.connectEnd, entry.connectStart,
+                            'connectEnd >= connectStart of ' + url);
+  assert_greater_than_equal(entry.requestStart, entry.connectEnd,
+                            'requestStart >= connectEnd of ' + url);
+  assert_greater_than_equal(entry.responseStart, entry.requestStart,
+                            'domainLookupStart >= requestStart of ' + url);
+  assert_greater_than_equal(entry.responseEnd, entry.responseStart,
+                            'responseEnd >= responseStart of ' + url);
+  assert_greater_than(entry.duration, 0, 'duration of ' + url);
+}
+
+promise_test(t => {
+    var script = 'resources/resource-timing-worker.js';
+    var scope = 'resources/resource-timing-scope.py';
+    var registration;
+    var frames = [];
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(reg => {
+          registration = reg;
+          add_completion_callback(_ => registration.unregister());
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(_ => with_iframe(scope + '?type=normal'))
+      .then(frame => {
+          frames.push(frame);
+          return with_iframe(scope + '?type=redirect');
+        })
+      .then(frame => {
+          frames.push(frame);
+          frames.forEach(frame => {
+            var result = JSON.parse(frame.contentDocument.body.textContent);
+            assert_equals(
+              result.timingEntries.length, 1,
+              'performance.getEntriesByName() must returns one ' +
+              'PerformanceResourceTiming entry for the navigation preload.');
+            var entry = result.timingEntries[0];
+            check_timing_entry(entry, frame.src, result.decodedBodySize,
+                               result.encodedBodySize);
+            frame.remove();
+          });
+          return registration.unregister();
+        });
+  }, 'Navigation Preload Resource Timing.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
new file mode 100644
index 0000000..2a71953
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-type: text/html; charset=UTF-8
+Transfer-encoding: chunked
+
+hello
+world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
new file mode 100644
index 0000000..7a453e4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        self.registration.navigationPreload.enable());
+  });
+
+self.addEventListener('fetch', event => {
+    event.respondWith(event.preloadResponse
+      .then(
+        _ => new Response('PASS: preloadResponse resolved'),
+        _ => new Response('FAIL: preloadResponse rejected')));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
new file mode 100644
index 0000000..659c4d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
@@ -0,0 +1,19 @@
+import time
+
+def main(request, response):
+    use_broken_body = b'use_broken_body' in request.GET
+
+    response.add_required_headers = False
+    response.writer.write_status(200)
+    response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8")
+    response.writer.write_header(b"Transfer-encoding", b"chunked")
+    response.writer.end_headers()
+
+    for idx in range(10):
+        if use_broken_body:
+            response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx))
+        else:
+            response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx))
+        time.sleep(0.001)
+
+    response.writer.write(u"0\r\n\r\n")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        self.registration.navigationPreload.enable());
+  });
+
+self.addEventListener('fetch', event => {
+    event.respondWith(event.preloadResponse);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py
new file mode 100644
index 0000000..30a1dd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py
@@ -0,0 +1,20 @@
+def main(request, response):
+    """
+    Returns a response with a Set-Cookie header based on the query params.
+    The body will be "1" if the cookie is present in the request and `drop` parameter is "0",
+    otherwise the body will be "0".
+    """
+    same_site = request.GET.first(b"same-site")
+    cookie_name = request.GET.first(b"cookie-name")
+    drop = request.GET.first(b"drop")
+    cookie_in_request = b"0"
+    cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site)
+
+    if drop == b"1":
+        cookie += b"; Max-Age=0"
+
+    if request.cookies.get(cookie_name):
+        cookie_in_request = request.cookies[cookie_name].value
+
+    headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)]
+    return (200, headers, cookie_in_request)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
new file mode 100644
index 0000000..48c14b7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        self.registration.navigationPreload.enable());
+  });
+
+self.addEventListener('fetch', event => {
+    event.respondWith(
+      event.preloadResponse
+        .then(res => res.text())
+        .then(text => {
+            return new Response(
+                '<body>[' + text + ']</body>',
+                {headers: [['content-type', 'text/html']]});
+          }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
new file mode 100644
index 0000000..a14ffb4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
@@ -0,0 +1,21 @@
+// This worker listens for commands from the page and messages back
+// the result.
+
+function handle(message) {
+  const np = self.registration.navigationPreload;
+  switch (message) {
+    case 'getState':
+      return np.getState();
+    case 'enable':
+      return np.enable();
+    case 'disable':
+      return np.disable();
+    case 'setHeaderValue':
+      return np.setHeaderValue('insightful');
+  }
+  return Promise.reject('bad message');
+}
+
+self.addEventListener('message', e => {
+    e.waitUntil(handle(e.data).then(result => e.source.postMessage(result)));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js
new file mode 100644
index 0000000..86f0c09
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js
@@ -0,0 +1,5 @@
+function expect_navigation_preload_state(state, enabled, header, desc) {
+  assert_equals(Object.keys(state).length, 2, desc + ': # of keys');
+  assert_equals(state.enabled, enabled, desc + ': enabled');
+  assert_equals(state.headerValue, header, desc + ': header');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
new file mode 100644
index 0000000..6e1ab23
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', event => {
+  event.respondWith(event.preloadResponse);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
new file mode 100644
index 0000000..f9bfce5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>redirected</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
new file mode 100644
index 0000000..84a97e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
@@ -0,0 +1,38 @@
+def main(request, response):
+    if b"base" in request.GET:
+        return [(b"Content-Type", b"text/html")], b"OK"
+    type = request.GET.first(b"type")
+
+    if type == b"normal":
+        response.status = 302
+        response.headers.append(b"Location", b"redirect-redirected.html")
+        response.headers.append(b"Custom-Header", b"hello")
+        return b""
+
+    if type == b"no-location":
+        response.status = 302
+        response.headers.append(b"Content-Type", b"text/html")
+        response.headers.append(b"Custom-Header", b"hello")
+        return b""
+
+    if type == b"no-location-with-body":
+        response.status = 302
+        response.headers.append(b"Content-Type", b"text/html")
+        response.headers.append(b"Custom-Header", b"hello")
+        return b"<body>BODY</body>"
+
+    if type == b"redirect-to-scope":
+        response.status = 302
+        response.headers.append(b"Location",
+                                b"redirect-scope.py?type=redirect-to-scope2")
+        return b""
+    if type == b"redirect-to-scope2":
+        response.status = 302
+        response.headers.append(b"Location",
+                                b"redirect-scope.py?type=redirect-to-scope3")
+        return b""
+    if type == b"redirect-to-scope3":
+        response.status = 302
+        response.headers.append(b"Location", b"redirect-redirected.html")
+        response.headers.append(b"Custom-Header", b"hello")
+        return b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
new file mode 100644
index 0000000..1b55f2e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
@@ -0,0 +1,35 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        self.registration.navigationPreload.enable());
+  });
+
+function get_response_info(r) {
+  var info = {
+    type: r.type,
+    url: r.url,
+    status: r.status,
+    ok: r.ok,
+    statusText: r.statusText,
+    headers: []
+  };
+  r.headers.forEach((value, name) => { info.headers.push([value, name]); });
+  return info;
+}
+
+function post_to_page(data) {
+  return self.clients.matchAll()
+    .then(clients => clients.forEach(client => client.postMessage(data)));
+}
+
+self.addEventListener('fetch', event => {
+    event.respondWith(
+      event.preloadResponse
+        .then(
+          res => {
+            if (res.url.includes("base")) {
+              return res;
+            }
+            return post_to_page(get_response_info(res)).then(_ => res);
+          },
+          err => new Response(err.toString())));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
new file mode 100644
index 0000000..5bab5b0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
@@ -0,0 +1,14 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+    normalized = dict()
+
+    for key, values in dict(request.headers).items():
+        values = [isomorphic_decode(value) for value in values]
+        normalized[isomorphic_decode(key.upper())] = values
+
+    response.headers.append(b"Content-Type", b"text/html")
+
+    return json.dumps(normalized)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
new file mode 100644
index 0000000..1006cf2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        Promise.all[
+            self.registration.navigationPreload.enable(),
+            self.registration.navigationPreload.setHeaderValue('hello')]);
+  });
+
+self.addEventListener('fetch', event => {
+    event.respondWith(event.preloadResponse);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
new file mode 100644
index 0000000..856f9db
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
@@ -0,0 +1,19 @@
+import zlib
+
+def main(request, response):
+    type = request.GET.first(b"type")
+
+    if type == "normal":
+        content = b"This is Navigation Preload Resource Timing test."
+        output = zlib.compress(content, 9)
+        headers = [(b"Content-type", b"text/plain"),
+                   (b"Content-Encoding", b"deflate"),
+                   (b"X-Decoded-Body-Size", len(content)),
+                   (b"X-Encoded-Body-Size", len(output)),
+                   (b"Content-Length", len(output))]
+        return headers, output
+
+    if type == b"redirect":
+        response.status = 302
+        response.headers.append(b"Location", b"redirect-redirected.html")
+        return b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
new file mode 100644
index 0000000..fac0d8d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
@@ -0,0 +1,37 @@
+async function wait_for_performance_entries(url) {
+  let entries = performance.getEntriesByName(url);
+  if (entries.length > 0) {
+    return entries;
+  }
+  return new Promise((resolve) => {
+    new PerformanceObserver((list) => {
+      const entries = list.getEntriesByName(url);
+      if (entries.length > 0) {
+        resolve(entries);
+      }
+    }).observe({ entryTypes: ['resource'] });
+  });
+}
+
+self.addEventListener('activate', event => {
+    event.waitUntil(self.registration.navigationPreload.enable());
+  });
+
+self.addEventListener('fetch', event => {
+    let headers;
+    event.respondWith(
+      event.preloadResponse
+          .then(response => {
+            headers = response.headers;
+            return response.text()
+          })
+          .then(_ => wait_for_performance_entries(event.request.url))
+          .then(entries =>
+            new Response(
+              JSON.stringify({
+                decodedBodySize: headers.get('X-Decoded-Body-Size'),
+                encodedBodySize: headers.get('X-Encoded-Body-Size'),
+                timingEntries: entries
+              }),
+              {headers: {'Content-Type': 'text/html'}})));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
new file mode 100644
index 0000000..a28b612
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>samesite</body>
+<script>
+onmessage = (e) => {
+  if (e.data === "GetBody") {
+    parent.postMessage("samesite", '*');
+  }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
new file mode 100644
index 0000000..51fdc9e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Same Site SW registrator</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/test-helpers.sub.js"></script>
+<script>
+
+/**
+ *  This is a helper file to register/unregister service worker in a same-site
+ *  iframe.
+ **/
+
+async function messageToParent(msg) {
+  parent.postMessage(msg, '*');
+}
+
+onmessage = async (e) => {
+  // t is a , but the helper function needs a test object.
+  let t = {
+    step_func: (func) => func,
+  };
+  if (e.data === "Register") {
+    let reg = await service_worker_unregister_and_register(t, "samesite-worker.js", ".");
+    let worker = reg.installing;
+    await wait_for_state(t, worker, 'activated');
+    await messageToParent("SW Registered");
+  } else if (e.data == "Unregister") {
+    await service_worker_unregister(t, ".");
+    await messageToParent("SW Unregistered");
+  }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        self.registration.navigationPreload.enable());
+  });
+
+self.addEventListener('fetch', event => {
+    event.respondWith(event.preloadResponse);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
new file mode 100644
index 0000000..87791d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
@@ -0,0 +1,40 @@
+// This worker remains in the installing phase so that the
+// navigation preload API can be tested when there is no
+// active worker.
+importScripts('/resources/testharness.js');
+importScripts('helpers.js');
+
+function expect_rejection(promise) {
+  return promise.then(
+      () => { return Promise.reject('unexpected fulfillment'); },
+      err => { assert_equals('InvalidStateError', err.name); });
+}
+
+function test_before_activation() {
+  const np = self.registration.navigationPreload;
+  return expect_rejection(np.enable())
+      .then(() => expect_rejection(np.disable()))
+      .then(() => expect_rejection(np.setHeaderValue('hi')))
+      .then(() => np.getState())
+      .then(state => expect_navigation_preload_state(
+          state, false, 'true', 'state should be the default'))
+      .then(() => 'PASS')
+      .catch(err => 'FAIL: ' + err);
+}
+
+var resolve_done_promise;
+var done_promise = new Promise(resolve => { resolve_done_promise = resolve; });
+
+// Run the test once the page messages this worker.
+self.addEventListener('message', e => {
+    e.waitUntil(test_before_activation()
+        .then(result => {
+            e.source.postMessage(result);
+            resolve_done_promise();
+          }));
+  });
+
+// Don't become the active worker until the test is done.
+self.addEventListener('install', e => {
+    e.waitUntil(done_promise);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
new file mode 100644
index 0000000..a860d95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Navigation Preload: SameSite cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const scope = 'resources/cookie.py';
+const script = 'resources/navigation-preload-worker.js';
+
+async function drop_cookie(t, same_site, cookie) {
+    const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=1');
+    t.add_cleanup(() => frame.remove());
+}
+
+async function same_site_cookies_test(t, same_site, cookie) {
+  // Remove the cookie before the first visit.
+  await drop_cookie(t, same_site, cookie);
+
+  {
+    const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+    t.add_cleanup(() => frame.remove());
+    // The body will be 0 because this is the first visit.
+    assert_equals(frame.contentDocument.body.textContent, '0', 'first visit');
+  }
+
+  {
+    const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+    t.add_cleanup(() => frame.remove());
+    // The body will be 1 because this is the second visit.
+    assert_equals(frame.contentDocument.body.textContent, '1', 'second visit');
+  }
+
+  // Remove the cookie after the test.
+  t.add_cleanup(() => drop_cookie(t, same_site, cookie));
+}
+
+promise_test(async t => {
+  const registration =
+    await service_worker_unregister_and_register(t, script, scope);
+  promise_test(t => registration.unregister(), 'Unregister a service worker.');
+
+  await wait_for_state(t, registration.installing, 'activated');
+  await registration.navigationPreload.enable();
+}, 'Set up a service worker for navigation preload tests.');
+
+promise_test(async t => {
+  await same_site_cookies_test(t, 'None', 'cookie-key-none');
+}, 'Navigation Preload for same site cookies (None).');
+
+promise_test(async t => {
+  await same_site_cookies_test(t, 'Strict', 'cookie-key-strict');
+}, 'Navigation Preload for same site cookies (Strict).');
+
+promise_test(async t => {
+  await same_site_cookies_test(t, 'Lax', 'cookie-key-lax');
+}, 'Navigation Preload for same site cookies (Lax).');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
new file mode 100644
index 0000000..633da99
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload for same site iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+
+const SAME_SITE = get_host_info().HTTPS_REMOTE_ORIGIN;
+const RESOURCES_DIR = "/service-workers/service-worker/navigation-preload/resources/";
+
+/**
+ * This test is used for testing the NavigationPreload works in a same site iframe.
+ * The test scenario is
+ * 1. Create a same site iframe to register service worker and wait for it be activated
+ * 2. Create a same site iframe which be intercepted by the service worker.
+ * 3. Once the iframe is loaded, service worker should set the page through the preload response.
+ *    And checking if the iframe's body content is expected.
+ * 4. Unregister the service worker.
+ * 5. remove created iframes.
+ */
+
+promise_test(async (t) => {
+    let resolver;
+    let checkValue = false;
+    window.onmessage = (e) => {
+      if (checkValue) {
+        assert_equals(e.data, "samesite");
+        checkValue = false;
+      }
+      resolver();
+    };
+
+    let helperIframe = document.createElement("iframe");
+    helperIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-sw-helper.html";
+    document.body.appendChild(helperIframe);
+
+    await new Promise(resolve => {
+      resolver = resolve;
+      helperIframe.onload = async () => {
+        helperIframe.contentWindow.postMessage("Register", '*');
+     }
+    });
+
+    let sameSiteIframe = document.createElement("iframe");
+    sameSiteIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-iframe.html";
+    document.body.appendChild(sameSiteIframe);
+    await new Promise(resolve => {
+      resolver = resolve;
+      sameSiteIframe.onload = async() => {
+        checkValue = true;
+        sameSiteIframe.contentWindow.postMessage("GetBody", '*')
+      }
+    });
+
+    await new Promise(resolve => {
+      resolver = resolve;
+      helperIframe.contentWindow.postMessage("Unregister", '*')
+    });
+
+    helperIframe.remove();
+    sameSiteIframe.remove();
+  });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html
new file mode 100644
index 0000000..0441c61
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection must clear body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body>
+<form id="test-form" method="POST" style="display: none;">
+  <input type="submit" id="submit-button" />
+</form>
+<script>
+promise_test(function(t) {
+    var scope = 'resources/navigation-redirect-body.py';
+    var script = 'resources/navigation-redirect-body-worker.js';
+    var registration;
+    var frame = document.createElement('frame');
+    var form = document.getElementById('test-form');
+    var submit_button = document.getElementById('submit-button');
+
+    frame.src = 'about:blank';
+    frame.name = 'target_frame';
+    frame.id = 'frame';
+    document.body.appendChild(frame);
+    t.add_cleanup(function() { document.body.removeChild(frame); });
+
+    form.action = scope;
+    form.target = 'target_frame';
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          var frame_load_promise = new Promise(function(resolve) {
+              frame.addEventListener('load', function() {
+                  resolve(frame.contentWindow.document.body.innerText);
+                }, false);
+            });
+          submit_button.click();
+          return frame_load_promise;
+        })
+      .then(function(text) {
+          var request_uri = decodeURIComponent(text);
+          assert_equals(
+              request_uri,
+              '/service-workers/service-worker/resources/navigation-redirect-body.py?redirect');
+          return registration.unregister();
+        });
+  }, 'Navigation redirection must clear body');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html
new file mode 100644
index 0000000..59e1caf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation Redirect Resolution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+function make_absolute(url) {
+  return new URL(url, location).toString();
+}
+
+const script = 'resources/fetch-rewrite-worker.js';
+
+function redirect_result_test(scope, expected_url, description) {
+  promise_test(async t => {
+    const registration = await service_worker_unregister_and_register(
+          t, script, scope);
+      t.add_cleanup(() => {
+        return service_worker_unregister(t, scope);
+      })
+      await wait_for_state(t, registration.installing, 'activated');
+
+      // The navigation to |scope| will be resolved by a fetch to |redirect_url|
+      // which returns a relative Location header. If it is resolved relative to
+      // |scope|, the result will be navigate-redirect-resolution/blank.html. If
+      // relative to |redirect_url|, it will be resources/blank.html. The latter
+      // is correct.
+      const iframe = await with_iframe(scope);
+      t.add_cleanup(() => { iframe.remove(); });
+      assert_equals(iframe.contentWindow.location.href,
+                    make_absolute(expected_url));
+  }, description);
+}
+
+// |redirect_url| serves a relative redirect to resources/blank.html.
+const redirect_url = 'resources/redirect.py?Redirect=blank.html';
+
+// |scope_base| does not exist but will be replaced with a fetch of
+// |redirect_url| by fetch-rewrite-worker.js.
+const scope_base = 'resources/subdir/navigation-redirect-resolution?' +
+    'redirect-mode=manual&url=' +
+    encodeURIComponent(make_absolute(redirect_url));
+
+// When the Service Worker forwards the result of |redirect_url| as an
+// opaqueredirect response, the redirect uses the response's URL list as the
+// base URL, not the request.
+redirect_result_test(scope_base, 'resources/blank.html',
+                     'test relative opaqueredirect');
+
+// The response's base URL should be preserved across CacheStorage and clone.
+redirect_result_test(scope_base + '&cache=1', 'resources/blank.html',
+                     'test relative opaqueredirect with CacheStorage');
+redirect_result_test(scope_base + '&clone=1', 'resources/blank.html',
+                     'test relative opaqueredirect with clone');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html
new file mode 100644
index 0000000..d4d2788
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Worker: Service Worker can receive HTTP opaqueredirect response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body></body>
+<script>
+async_test(function(t) {
+    var frame_src = get_host_info()['HTTPS_ORIGIN'] + base_path() +
+      'resources/navigation-redirect-to-http-iframe.html';
+    function on_message(e) {
+      assert_equals(e.data.results, 'OK');
+      t.done();
+    }
+
+    window.addEventListener('message', t.step_func(on_message), false);
+
+    with_iframe(frame_src)
+      .then(function(frame) {
+           t.add_cleanup(function() { frame.remove(); });
+         });
+  }, 'Verify Service Worker can receive HTTP opaqueredirect response.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html
new file mode 100644
index 0000000..d7d3d52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html
@@ -0,0 +1,846 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection</title>
+<meta name="timeout" content="long">
+<!-- empty variant tests document.location and intercepted URLs -->
+<meta name="variant" content="">
+<!-- client variant tests the Clients API (resultingClientId and Client.url) -->
+<meta name="variant" content="?client">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const host_info = get_host_info();
+
+// This test registers three Service Workers at SCOPE1, SCOPE2 and
+// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests
+// which are intercepted by Service Worker while loading redirect page.
+const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?';
+const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+const SCRIPT = 'resources/redirect-worker.js';
+
+const OTHER_ORIGIN_IFRAME_URL =
+      OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html';
+const OTHER_ORIGIN_SCOPE =
+      OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const OTHER_ORIGIN_OUT_SCOPE =
+      OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+
+let registrations;
+let workers;
+let other_origin_frame;
+let message_resolvers = {};
+let next_message_id = 0;
+
+promise_test(async t  => {
+  // In this frame we register a service worker at OTHER_ORIGIN_SCOPE.
+  // And will use this frame to communicate with the worker.
+  other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL);
+
+  // Register same-origin service workers.
+  registrations = await Promise.all([
+      service_worker_unregister_and_register(t, SCRIPT, SCOPE1),
+      service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]);
+
+  // Wait for all workers to activate.
+  workers = registrations.map(get_effective_worker);
+  return Promise.all([
+      wait_for_state(t, workers[0], 'activated'),
+      wait_for_state(t, workers[1], 'activated'),
+      // This promise will resolve when |wait_for_worker_promise|
+      // in OTHER_ORIGIN_IFRAME_URL resolves.
+      send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]);
+}, 'initialize global state');
+
+function get_effective_worker(registration) {
+  if (registration.active)
+    return registration.active;
+  if (registration.waiting)
+    return registration.waiting;
+  if (registration.installing)
+    return registration.installing;
+}
+
+async function check_all_intercepted_urls(expected_urls) {
+  const urls = [];
+  urls.push(await get_intercepted_urls(workers[0]));
+  urls.push(await get_intercepted_urls(workers[1]));
+  // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's
+  // SW. This promise will resolve when get_request_infos() in
+  // OTHER_ORIGIN_IFRAME_URL resolves.
+  const request_infos = await send_to_iframe(other_origin_frame,
+                                             {command: 'get_request_infos'});
+  urls.push(request_infos.map(info => { return info.url; }));
+
+  assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.');
+}
+
+// Checks |clients| returned from a worker. Only the client matching
+// |expected_final_client_tag| should be found. Returns true if a client was
+// found. Note that the final client is not necessarily found by this worker,
+// if the client is cross-origin.
+//
+// |clients| is an object like:
+// {x: {found: true, id: id1, url: url1}, b: {found: false}}
+function check_clients(clients,
+                       expected_id,
+                       expected_url,
+                       expected_final_client_tag,
+                       worker_name) {
+  let found = false;
+  Object.keys(clients).forEach(key => {
+    const info = clients[key];
+    if (info.found) {
+      assert_true(!!expected_final_client_tag,
+                  `${worker_name} client tag exists`);
+      assert_equals(key, expected_final_client_tag,
+                    `${worker_name} client tag matches`);
+      assert_equals(info.id, expected_id, `${worker_name} client id`);
+      assert_equals(info.url, expected_url, `${worker_name} client url`);
+      found = true;
+    }
+  });
+  return found;
+}
+
+function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) {
+  assert_equals(infos.length, expected_infos.length,
+                `request length for ${worker}`);
+  for (var i = 0; i < infos.length; i++) {
+    const tag = expected_infos[i].resultingClientIdTag;
+    const url = expected_infos[i].url;
+    const actual_id = infos[i].resultingClientId;
+    const expected_id = actual_ids[tag];
+    assert_equals(typeof(actual_id), 'string',
+                  `resultingClientId for ${url} request to ${worker}`);
+    if (expected_id) {
+      assert_equals(actual_id, expected_id,
+                    `resultingClientId for ${url} request to ${worker}`);
+    } else {
+      actual_ids[tag] = actual_id;
+    }
+  }
+}
+
+// Creates an iframe and navigates to |url|, which is expected to start a chain
+// of redirects.
+// - |expected_last_url| is the expected window.location after the
+//   navigation.
+//
+// - |expected_request_infos| is the expected requests that the service workers
+//   were dispatched fetch events for. The format is:
+//   [
+//     [
+//       // Requests received by workers[0].
+//       {url: url1, resultingClientIdTag: 'a'},
+//       {url: url2, resultingClientIdTag: 'a'}
+//     ],
+//     [
+//       // Requests received by workers[1].
+//       {url: url3, resultingClientIdTag: 'a'}
+//     ],
+//     [
+//       // Requests received by the cross-origin worker.
+//       {url: url4, resultingClientIdTag: 'x'}
+//       {url: url5, resultingClientIdTag: 'x'}
+//     ]
+//   ]
+//   Here, |url| is |event.request.url| and |resultingClientIdTag| represents
+//   |event.resultingClientId|. Since the actual client ids are not known
+//   beforehand, the expectation isn't the literal expected value, but all equal
+//   tags must map to the same actual id.
+//
+// - |expected_final_client_tag| is the resultingClientIdTag that is
+//   expected to map to the created client's id. This is null if there
+//   is no such tag, which can happen when the final request was a cross-origin
+//   redirect to out-scope, so no worker received a fetch event whose
+//   resultingClientId is the id of the resulting client.
+//
+// In the example above:
+// - workers[0] receives two requests with the same resultingClientId.
+// - workers[1] receives one request also with that resultingClientId.
+// - The cross-origin worker receives two requests with the same
+//   resultingClientId which differs from the previous one.
+// - Assuming |expected_final_client_tag| is 'x', then the created
+//   client has the id seen by the cross-origin worker above.
+function redirect_test(url,
+                       expected_last_url,
+                       expected_request_infos,
+                       expected_final_client_tag,
+                       test_name) {
+  promise_test(async t => {
+    const frame = await with_iframe(url);
+    t.add_cleanup(() => { frame.remove(); });
+
+    // Switch on variant.
+    if (document.location.search == '?client') {
+      return client_variant_test(url, expected_last_url, expected_request_infos,
+                                 expected_final_client_tag, test_name);
+    }
+
+    return default_variant_test(url, expected_last_url, expected_request_infos,
+                                frame, test_name);
+  }, test_name);
+}
+
+// The default variant tests the request interception chain and
+// resulting document.location.
+async function default_variant_test(url,
+                                    expected_last_url,
+                                    expected_request_infos,
+                                    frame,
+                                    test_name) {
+  const expected_intercepted_urls = expected_request_infos.map(
+      requests_for_worker => {
+    return requests_for_worker.map(info => {
+      return info.url;
+    });
+  });
+  await check_all_intercepted_urls(expected_intercepted_urls);
+  const last_url = await send_to_iframe(frame, 'getLocation');
+  assert_equals(last_url, expected_last_url, 'Last URL should match.');
+}
+
+// The "client" variant tests the Clients API using resultingClientId.
+async function client_variant_test(url,
+                                   expected_last_url,
+                                   expected_request_infos,
+                                   expected_final_client_tag,
+                                   test_name) {
+  // Request infos is an array like:
+  // [
+  //   [{url: url1, resultingClientIdTag: tag1}],
+  //   [{url: url2, resultingClientIdTag: tag2}],
+  //   [{url: url3: resultingClientIdTag: tag3}]
+  // ]
+  const requestInfos = await get_all_request_infos();
+
+  // We check the actual infos against the expected ones, and learn the
+  // actual ids as we go.
+  const actual_ids = {};
+  check_resulting_client_ids(requestInfos[0],
+                             expected_request_infos[0],
+                             actual_ids,
+                             'worker0');
+  check_resulting_client_ids(requestInfos[1],
+                             expected_request_infos[1],
+                             actual_ids,
+                             'worker1');
+  check_resulting_client_ids(requestInfos[2],
+                             expected_request_infos[2],
+                             actual_ids,
+                             'crossOriginWorker');
+
+  // Now |actual_ids| maps tag to actual id:
+  // {x: id1, b: id2, c: id3}
+  // Ask each worker to try to resolve the actual ids to clients.
+  // Only |expected_final_client_tag| should resolve to a client.
+  const client_infos = await get_all_clients(actual_ids);
+
+  // Client infos is an object like:
+  // {
+  //   worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}},
+  //   worker1: {x: {found: true, id: id1, url: url1}},
+  //   crossOriginWorker: {x: {found: false}}, {b: {found: false}}
+  // }
+  //
+  // Now check each client info. check_clients() verifies each info: only
+  // |expected_final_client_tag| should ever be found and the found client
+  // should have the expected url and id. A wrinkle is that not all workers
+  // will find the client, if they are cross-origin to the client. This
+  // means check_clients() trivially passes if no clients are found. So
+  // additionally check that at least one worker found the client (|found|),
+  // if that was expected (|expect_found|).
+  let found = false;
+  const expect_found = !!expected_final_client_tag;
+  const expected_id = actual_ids[expected_final_client_tag];
+  found = check_clients(client_infos.worker0,
+                        expected_id,
+                        expected_last_url,
+                        expected_final_client_tag,
+                        'worker0');
+  found = check_clients(client_infos.worker1,
+                        expected_id,
+                        expected_last_url,
+                        expected_final_client_tag,
+                        'worker1') || found;
+  found = check_clients(client_infos.crossOriginWorker,
+                        expected_id,
+                        expected_last_url,
+                        expected_final_client_tag,
+                        'crossOriginWorker') || found;
+  assert_equals(found, expect_found, 'client found');
+
+  if (!expect_found) {
+    // TODO(falken): Ask the other origin frame if it has a client of the
+    // expected URL.
+  }
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+  if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] &&
+      e.origin != host_info['HTTPS_ORIGIN'] ) {
+    console.error('invalid origin: ' + e.origin);
+    return;
+  }
+  var resolve = message_resolvers[e.data.id];
+  delete message_resolvers[e.data.id];
+  resolve(e.data.result);
+}
+
+function send_to_iframe(frame, message) {
+  var message_id = next_message_id++;
+  return new Promise(resolve => {
+    message_resolvers[message_id] = resolve;
+    frame.contentWindow.postMessage(
+        {id: message_id, message},
+        '*');
+  });
+}
+
+async function get_all_clients(actual_ids) {
+  const client_infos = {};
+  client_infos['worker0'] = await get_clients(workers[0], actual_ids);
+  client_infos['worker1'] = await get_clients(workers[1], actual_ids);
+  client_infos['crossOriginWorker'] =
+      await send_to_iframe(other_origin_frame,
+                           {command: 'get_clients', actual_ids});
+  return client_infos;
+}
+
+function get_clients(worker, actual_ids) {
+  return new Promise(resolve => {
+    var channel = new MessageChannel();
+    channel.port1.onmessage = (msg) => {
+      resolve(msg.data.clients);
+    };
+    worker.postMessage({command: 'getClients', actual_ids, port: channel.port2},
+                       [channel.port2]);
+  });
+}
+
+// Returns an array of the URLs that |worker| received fetch events for:
+//   [url1, url2]
+async function get_intercepted_urls(worker) {
+  const infos = await get_request_infos(worker);
+  return infos.map(info => { return info.url; });
+}
+
+// Returns the requests that |worker| received fetch events for. The return
+// value is an array of format:
+// [
+//   {url: url1, resultingClientId: id},
+//   {url: url2, resultingClientId: id}
+// ]
+function get_request_infos(worker) {
+  return new Promise(resolve => {
+    var channel = new MessageChannel();
+    channel.port1.onmessage = (msg) => {
+      resolve(msg.data.requestInfos);
+    };
+    worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+                       [channel.port2]);
+  });
+}
+
+// Returns an array of the requests the workers received fetch events for:
+// [
+//   // Requests from workers[0].
+//   [
+//     {url: url1, resultingClientIdTag: tag1},
+//     {url: url2, resultingClientIdTag: tag1}
+//   ],
+//
+//   // Requests from workers[1].
+//   [{url: url3, resultingClientIdTag: tag2}],
+//
+//   // Requests from the cross-origin worker.
+//   []
+// ]
+async function get_all_request_infos()  {
+  const request_infos = [];
+  request_infos.push(await get_request_infos(workers[0]));
+  request_infos.push(await get_request_infos(workers[1]));
+  request_infos.push(await send_to_iframe(other_origin_frame,
+                                          {command: 'get_request_infos'}));
+  return request_infos;
+}
+
+let url;
+let url1;
+let url2;
+
+// Normal redirect (from out-scope to in-scope).
+url = SCOPE1;
+redirect_test(
+    OUT_SCOPE + 'url=' + encodeURIComponent(url),
+    url,
+    [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'Normal redirect to same-origin scope.');
+
+
+url = SCOPE1 + '#ref';
+redirect_test(
+    OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref',
+    url,
+    [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'Normal redirect to same-origin scope with a hash fragment.');
+
+url = SCOPE1 + '#ref2';
+redirect_test(
+    OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref',
+    url,
+    [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'Normal redirect to same-origin scope with different hash fragments.');
+
+url = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    OUT_SCOPE + 'url=' + encodeURIComponent(url),
+    url,
+    [[], [], [{url, resultingClientIdTag: 'x'}]],
+    'x',
+    'Normal redirect to other-origin scope.');
+
+// SW fallbacked redirect. SW doesn't handle the fetch request.
+url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE);
+redirect_test(
+    url,
+    OUT_SCOPE,
+    [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'SW-fallbacked redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-fallbacked redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref';
+url2 = SCOPE1 + '#ref';
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-fallbacked redirect to same-origin same-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref';
+url2 = SCOPE1 + '#ref2';
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-fallbacked redirect to same-origin same-scope with different hash ' +
+    'fragments.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'x'}],
+      [{url: url2, resultingClientIdTag: 'x'}],
+      []
+    ],
+    'x',
+    'SW-fallbacked redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+    null,
+    'SW-fallbacked redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'a'}],
+      [],
+      [{url: url2, resultingClientIdTag: 'x'}]
+    ],
+    'x',
+    'SW-fallbacked redirect to other-origin in-scope.');
+
+
+url3 = SCOPE1;
+url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3);
+url1 = SCOPE1 + 'url=' + encodeURIComponent(url2);
+redirect_test(
+    url1,
+    url3,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'a'},
+        {url: url3, resultingClientIdTag: 'x'}
+      ],
+      [],
+      [{url: url2, resultingClientIdTag: 'b'}]
+    ],
+    'x',
+    'SW-fallbacked redirect to other-origin and back to same-origin.');
+
+// SW generated redirect.
+// SW: event.respondWith(Response.redirect(params['url']));
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'SW-generated redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref';
+url2 = OUT_SCOPE + '#ref';
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'SW-generated redirect to same-origin out-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref';
+url2 = OUT_SCOPE + '#ref2';
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'SW-generated redirect to same-origin out-scope with different hash ' +
+    'fragments.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-generated redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'x'}],
+      [{url: url2, resultingClientIdTag: 'x'}],
+      []
+    ],
+    'x',
+    'SW-generated redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+    null,
+    'SW-generated redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'a'}],
+      [],
+      [{url: url2, resultingClientIdTag: 'x'}]
+    ],
+    'x',
+    'SW-generated redirect to other-origin in-scope.');
+
+
+// SW fetched redirect.
+// SW: event.respondWith(fetch(event.request));
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE)
+url2 = OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'SW-fetched redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-fetched redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'x'}],
+      [{url: url2, resultingClientIdTag: 'x'}],
+      []
+    ],
+    'x',
+    'SW-fetched redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+    null,
+    'SW-fetched redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'a'}],
+      [],
+      [{url: url2, resultingClientIdTag: 'x'}]
+    ],
+    'x',
+    'SW-fetched redirect to other-origin in-scope.');
+
+
+// SW responds with a fetch from a different url.
+// SW: event.respondWith(fetch(params['url']));
+url2 = SCOPE1;
+url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2);
+redirect_test(
+    url1,
+    url1,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'SW-fetched response from different URL, same-origin same-scope.');
+
+
+// Opaque redirect.
+// SW: event.respondWith(fetch(
+//         new Request(event.request.url, {redirect: 'manual'})));
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'Redirect to same-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'Redirect to same-origin same-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'x'}],
+      [{url: url2, resultingClientIdTag: 'x'}],
+      []
+    ],
+    'x',
+    'Redirect to same-origin other-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+    null,
+    'Redirect to other-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'a'}],
+      [],
+      [{url: url2, resultingClientIdTag: 'x'}]
+    ],
+    'x',
+    'Redirect to other-origin in-scope with opaque redirect response.');
+
+url= SCOPE1 + 'sw=manual&noLocationRedirect';
+redirect_test(
+    url, url, [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'No location redirect response.');
+
+
+// Opaque redirect passed through Cache.
+// SW responds with an opaque redirectresponse from the Cache API.
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'Redirect to same-origin out-scope with opaque redirect response which ' +
+    'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+    url1,
+    url2,
+    [
+      [
+        {url: url1, resultingClientIdTag: 'x'},
+        {url: url2, resultingClientIdTag: 'x'}
+      ],
+      [],
+      []
+    ],
+    'x',
+    'Redirect to same-origin same-scope with opaque redirect response which ' +
+    'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'x'}],
+      [{url: url2, resultingClientIdTag: 'x'}],
+      []
+    ],
+    'x',
+    'Redirect to same-origin other-scope with opaque redirect response which ' +
+    'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+       encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+    null,
+    'Redirect to other-origin out-scope with opaque redirect response which ' +
+    'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+       encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+    url1,
+    url2,
+    [
+      [{url: url1, resultingClientIdTag: 'a'}],
+      [],
+      [{url: url2, resultingClientIdTag: 'x'}],
+    ],
+    'x',
+    'Redirect to other-origin in-scope with opaque redirect response which ' +
+    'is passed through Cache.');
+
+url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect';
+redirect_test(
+    url,
+    url,
+    [[{url, resultingClientIdTag: 'x'}], [], []],
+    'x',
+    'No location redirect response via Cache.');
+
+// Clean up the test environment. This promise_test() needs to be the last one.
+promise_test(async t => {
+  registrations.forEach(async registration => {
+    if (registration)
+      await registration.unregister();
+  });
+  await send_to_iframe(other_origin_frame, {command: 'unregister'});
+  other_origin_frame.remove();
+}, 'clean up global state');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html
new file mode 100644
index 0000000..7f6c756
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation setting cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const scopepath = '/cookies/resources/setSameSite.py?with-sw';
+
+async function unregister_service_worker(origin) {
+  let target_url = origin +
+      '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+      '?scopepath=' + encodeURIComponent(scopepath);
+  const w = window.open(target_url);
+  try {
+    await wait_for_message('SW-UNREGISTERED');
+  } finally {
+    w.close();
+  }
+}
+
+async function register_service_worker(origin) {
+  let target_url = origin +
+      '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+      '?scopepath=' + encodeURIComponent(scopepath);
+  const w = window.open(target_url);
+  try {
+    await wait_for_message('SW-REGISTERED');
+  } finally {
+    w.close();
+  }
+}
+
+async function clear_cookies(origin) {
+  let target_url = origin + '/cookies/samesite/resources/puppet.html';
+  const w = window.open(target_url);
+  try {
+    await wait_for_message('READY');
+    w.postMessage({ type: 'drop' }, '*');
+    await wait_for_message('drop-complete');
+  } finally {
+    w.close();
+  }
+}
+
+// The following tests are adapted from /cookies/samesite/setcookie-navigation.https.html
+
+// Asserts that cookies are present or not present (according to `expectation`)
+// in the cookie string `cookies` with the correct names and value.
+function assert_cookies_present(cookies, value, expected_cookie_names, expectation) {
+  for (name of expected_cookie_names) {
+    let re = new RegExp("(?:^|; )" + name + "=" + value + "(?:$|;)");
+    let assertion = expectation ? assert_true : assert_false;
+    assertion(re.test(cookies), "`" + name + "=" + value + "` in cookies");
+  }
+}
+
+// Navigate from ORIGIN to |origin_to|, expecting the navigation to set SameSite
+// cookies on |origin_to|.
+function navigate_test(method, origin_to, query, title) {
+  promise_test(async function(t) {
+    // The cookies don't need to be cleared on each run because |value| is
+    // a new random value on each run, so on each run we are overwriting and
+    // checking for a cookie with a different random value.
+    let value = query + "&" + Math.random();
+    let url_from = SECURE_ORIGIN + "/cookies/samesite/resources/navigate.html"
+    let url_to = origin_to + "/cookies/resources/setSameSite.py?" + value;
+    var w = window.open(url_from);
+    await wait_for_message('READY', SECURE_ORIGIN);
+    assert_equals(SECURE_ORIGIN, window.origin);
+    assert_equals(SECURE_ORIGIN, w.origin);
+    let command = (method === "POST") ? "post-form" : "navigate";
+    w.postMessage({ type: command, url: url_to }, "*");
+    let message = await wait_for_message('COOKIES_SET', origin_to);
+    let samesite_cookie_names = ['samesite_strict', 'samesite_lax', 'samesite_none', 'samesite_unspecified'];
+    assert_cookies_present(message.data.cookies, value, samesite_cookie_names, true);
+    w.close();
+  }, title);
+}
+
+promise_test(async t => {
+  await register_service_worker(SECURE_ORIGIN);
+  await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+}, 'Setup service workers');
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&ignore",
+              "Same-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+              "Cross-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&ignore",
+              "Same-site top-level POST with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+              "Cross-site top-level with fallback service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw",
+              "Same-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+              "Cross-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw",
+              "Same-site top-level POST with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+              "Cross-site top-level with passthrough service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&navpreload",
+              "Same-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&navpreload",
+              "Cross-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+// navpreload not supported with POST method
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&change-request",
+              "Same-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+              "Cross-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&change-request",
+              "Same-site top-level POST with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+              "Cross-site top-level with change-request service worker POST should be able to set SameSite=* cookies.");
+
+promise_test(async t => {
+  await unregister_service_worker(SECURE_ORIGIN);
+  await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+  await clear_cookies(SECURE_ORIGIN);
+  await clear_cookies(SECURE_CROSS_SITE_ORIGIN);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html
new file mode 100644
index 0000000..acb02c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+    'startTime',
+    'workerStart',
+    'fetchStart',
+    'requestStart',
+    'responseStart',
+    'responseEnd',
+];
+
+function navigate_in_frame(frame, url) {
+    frame.contentWindow.location = url;
+    return new Promise((resolve) => {
+        frame.addEventListener('load', () => {
+            const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+            const {timeOrigin} = frame.contentWindow.performance;
+            resolve({
+                workerStart: timing.workerStart + timeOrigin,
+                fetchStart: timing.fetchStart + timeOrigin
+            })
+        });
+    });
+}
+
+const worker_url = 'resources/navigation-timing-worker-extended.js';
+
+promise_test(async (t) => {
+    const scope = 'resources/timings/dummy.html';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activating');
+    const frame = await with_iframe('resources/empty.html');
+    t.add_cleanup(() => frame.remove());
+
+    const [timingFromEntry, timingFromWorker] = await Promise.all([
+        navigate_in_frame(frame, scope),
+        new Promise(resolve => {
+            window.addEventListener('message', m => {
+                resolve(m.data)
+            })
+        })])
+
+    assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart,
+        'workerStart marking should not wait for worker activation to finish');
+    assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd,
+        'fetchStart should be marked once the worker is activated');
+    assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart,
+        'fetchStart should be marked before the Fetch event handler is called');
+}, 'Service worker controlled navigation timing');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html
new file mode 100644
index 0000000..6b51a5c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+    'startTime',
+    'workerStart',
+    'fetchStart',
+    'requestStart',
+    'responseStart',
+    'responseEnd',
+];
+
+function verify(timing) {
+    for (let i = 0; i < timingEventOrder.length - 1; i++) {
+        assert_true(timing[timingEventOrder[i]] <= timing[timingEventOrder[i + 1]],
+                `Expected ${timingEventOrder[i]} <= ${timingEventOrder[i + 1]}`);
+    }
+}
+
+function navigate_in_frame(frame, url) {
+    frame.contentWindow.location = url;
+    return new Promise((resolve) => {
+        frame.addEventListener('load', () => {
+            const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+            resolve(timing);
+        });
+    });
+}
+
+const worker_url = 'resources/navigation-timing-worker.js';
+
+promise_test(async (t) => {
+    const scope = 'resources/empty.html';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activated');
+    const frame = await with_iframe(scope);
+    t.add_cleanup(() => frame.remove());
+
+    const timing = await navigate_in_frame(frame, scope);
+    assert_greater_than(timing.workerStart, 0);
+    verify(timing);
+}, 'Service worker controlled navigation timing');
+
+promise_test(async (t) => {
+    const scope = 'resources/empty.html?network-fallback';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activated');
+    const frame = await with_iframe(scope);
+    t.add_cleanup(() => frame.remove());
+
+    const timing = await navigate_in_frame(frame, scope);
+    verify(timing);
+}, 'Service worker controlled navigation timing network fallback');
+
+promise_test(async (t) => {
+    const scope = 'resources/redirect.py?Redirect=empty.html';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activated');
+    const frame = await with_iframe(scope);
+    t.add_cleanup(() => frame.remove());
+
+    const timing = await navigate_in_frame(frame, scope);
+    verify(timing);
+    // Additional checks for redirected navigation.
+    assert_true(timing.redirectStart <= timing.redirectEnd,
+        'Expected redirectStart <= redirectEnd');
+    assert_true(timing.redirectEnd <= timing.fetchStart,
+        'Expected redirectEnd <= fetchStart');
+}, 'Service worker controlled navigation timing redirect');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html b/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html
new file mode 100644
index 0000000..7269cbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: nested blob URL worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/simple-intercept-worker.js';
+const SCOPE = 'resources/';
+const RESOURCE = 'resources/simple.txt';
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'Nested blob URL workers should be intercepted by a service worker.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'Nested worker created from a blob URL worker should be intercepted by a service worker.');
+
+promise_test((t) => {
+  return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'Nested blob URL worker created from a worker should be intercepted by a service worker.');
+
+async function runTest(t, iframe_url) {
+  const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  t.add_cleanup(_ => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  const frame = await with_iframe(iframe_url);
+  t.add_cleanup(_ => frame.remove());
+  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                    null, 'frame should be controlled');
+
+  const response_text = await frame.contentWindow.fetch_in_worker(RESOURCE);
+  assert_equals(response_text, 'intercepted by service worker',
+                'fetch() should be intercepted.');
+}
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html b/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html
new file mode 100644
index 0000000..7a90743
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Verify nextHopProtocol is set correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function getNextHopProtocol(frame, url) {
+  let final_url = new URL(url, self.location).href;
+  await frame.contentWindow.fetch(final_url).then(r => r.text());
+  let entryList = frame.contentWindow.performance.getEntriesByName(final_url);
+  let entry = entryList[entryList.length - 1];
+  return entry.nextHopProtocol;
+}
+
+async function runTest(t, base_url, expected_protocol) {
+  const scope = 'resources/empty.html?next-hop-protocol';
+  const script = 'resources/fetch-rewrite-worker.js';
+  let frame;
+
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(async _ => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  frame = await with_iframe(scope);
+  t.add_cleanup(_ => frame.remove());
+
+  assert_equals(await getNextHopProtocol(frame, `${base_url}?generate-png`),
+                '', 'nextHopProtocol is not set on synthetic response');
+  assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
+                expected_protocol, 'nextHopProtocol is set on fallback');
+  assert_equals(await getNextHopProtocol(frame, `${base_url}`),
+                expected_protocol, 'nextHopProtocol is set on pass-through');
+  assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
+                expected_protocol, 'nextHopProtocol is set on cached response');
+}
+
+promise_test(async (t) => {
+  return runTest(t, 'resources/empty.js', 'http/1.1');
+}, 'nextHopProtocol reports H1 correctly when routed via a service worker.');
+
+// This may be expected to fail if the WPT infrastructure does not fully
+// support H2 protocol testing yet.
+promise_test(async (t) => {
+  return runTest(t, 'resources/empty.h2.js', 'h2');
+}, 'nextHopProtocol reports H2 correctly when routed via a service worker.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
new file mode 100644
index 0000000..f7c2ef3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
@@ -0,0 +1,7 @@
+// META: global=serviceworker-module
+
+// This is imported to ensure import('./basic-module-2.js') fails even if
+// it has been previously statically imported.
+import './resources/basic-module-2.js';
+
+import './resources/no-dynamic-import.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js
new file mode 100644
index 0000000..25b370b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js
@@ -0,0 +1,3 @@
+// META: global=serviceworker
+
+importScripts('resources/no-dynamic-import.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html
new file mode 100644
index 0000000..f5e80bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install(worker) {
+  return new Promise(function(resolve, reject) {
+      worker.addEventListener('statechange', function(event) {
+          if (worker.state == 'installed')
+            resolve();
+          else if (worker.state == 'redundant')
+            reject();
+        });
+    });
+}
+
+function wait_for_activate(worker) {
+  return new Promise(function(resolve, reject) {
+      worker.addEventListener('statechange', function(event) {
+          if (worker.state == 'activated')
+            resolve();
+          else if (worker.state == 'redundant')
+            reject();
+        });
+    });
+}
+
+function make_test(name, script) {
+  promise_test(function(t) {
+      var scope = script;
+      var registration;
+      return service_worker_unregister_and_register(t, script, scope)
+        .then(function(r) {
+            registration = r;
+
+            t.add_cleanup(function() {
+                return r.unregister();
+              });
+
+            return wait_for_install(registration.installing);
+          })
+        .then(function() {
+            // Activate should succeed regardless of script errors.
+            return wait_for_activate(registration.waiting);
+          });
+    }, name);
+}
+
+[
+  {
+    name: 'activate handler throws an error',
+    script: 'resources/onactivate-throw-error-worker.js',
+  },
+  {
+    name: 'activate handler throws an error, error handler does not cancel',
+    script: 'resources/onactivate-throw-error-with-empty-onerror-worker.js',
+  },
+  {
+    name: 'activate handler dispatches an event that throws an error',
+    script: 'resources/onactivate-throw-error-from-nested-event-worker.js',
+  },
+  {
+    name: 'activate handler throws an error that is cancelled',
+    script: 'resources/onactivate-throw-error-then-cancel-worker.js',
+  },
+  {
+    name: 'activate handler throws an error and prevents default',
+    script: 'resources/onactivate-throw-error-then-prevent-default-worker.js',
+  }
+].forEach(function(test_case) {
+    make_test(test_case.name, test_case.script);
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html
new file mode 100644
index 0000000..fe7f6e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+  return new Promise(function(resolve) {
+      worker.addEventListener('statechange', function(event) {
+          if (worker.state == 'installed')
+            resolve(true);
+          else if (worker.state == 'redundant')
+            resolve(false);
+        });
+    });
+}
+
+function make_test(name, script, expect_install) {
+  promise_test(function(t) {
+      var scope = script;
+      return service_worker_unregister_and_register(t, script, scope)
+        .then(function(registration) {
+            return wait_for_install_event(registration.installing);
+          })
+        .then(function(did_install) {
+            assert_equals(did_install, expect_install,
+                          'The worker was installed');
+          })
+    }, name);
+}
+
+[
+  {
+    name: 'install handler throws an error',
+    script: 'resources/oninstall-throw-error-worker.js',
+    expect_install: true
+  },
+  {
+    name: 'install handler throws an error, error handler does not cancel',
+    script: 'resources/oninstall-throw-error-with-empty-onerror-worker.js',
+    expect_install: true
+  },
+  {
+    name: 'install handler dispatches an event that throws an error',
+    script: 'resources/oninstall-throw-error-from-nested-event-worker.js',
+    expect_install: true
+  },
+  {
+    name: 'install handler throws an error in the waitUntil',
+    script: 'resources/oninstall-waituntil-throw-error-worker.js',
+    expect_install: false
+  },
+
+  // The following two cases test what happens when the ServiceWorkerGlobalScope
+  // 'error' event handler cancels the resulting error event.  Since the
+  // original 'install' event handler through, the installation should still
+  // be stopped in this case.  See:
+  // https://github.com/slightlyoff/ServiceWorker/issues/778
+  {
+    name: 'install handler throws an error that is cancelled',
+    script: 'resources/oninstall-throw-error-then-cancel-worker.js',
+    expect_install: true
+  },
+  {
+    name: 'install handler throws an error and prevents default',
+    script: 'resources/oninstall-throw-error-then-prevent-default-worker.js',
+    expect_install: true
+  }
+].forEach(function(test_case) {
+    make_test(test_case.name, test_case.script, test_case.expect_install);
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html b/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html
new file mode 100644
index 0000000..417aa4e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opaque responses should not be reused for XHRs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const WORKER =
+  'resources/opaque-response-preloaded-worker.js';
+
+var done;
+
+// These test that the browser does not inappropriately use a cached opaque
+// response for a request that is not no-cors. The test opens a controlled
+// iframe that uses link rel=preload to issue a same-origin no-cors request.
+// The service worker responds to the request with an opaque response. Then the
+// iframe does an XHR (not no-cors) to that URL again. The request should fail.
+promise_test(t => {
+    const SCOPE =
+      'resources/opaque-response-being-preloaded-xhr.html';
+    const promise = new Promise(resolve => done = resolve);
+
+    return service_worker_unregister_and_register(t, WORKER, SCOPE)
+      .then(reg => {
+           add_completion_callback(() => reg.unregister());
+           return wait_for_state(t, reg.installing, 'activated');
+         })
+      .then(() => with_iframe(SCOPE))
+      .then(frame => t.add_cleanup(() => frame.remove() ))
+      .then(() => promise)
+      .then(result => assert_equals(result, 'PASS'));
+  }, 'Opaque responses should not be reused for XHRs, loading case');
+
+promise_test(t => {
+    const SCOPE =
+      'resources/opaque-response-preloaded-xhr.html';
+    const promise = new Promise(resolve => done = resolve);
+
+    return service_worker_unregister_and_register(t, WORKER, SCOPE)
+      .then(reg => {
+           add_completion_callback(() => reg.unregister());
+           return wait_for_state(t, reg.installing, 'activated');
+         })
+      .then(() => with_iframe(SCOPE))
+      .then(frame => t.add_cleanup(() => frame.remove() ))
+      .then(() => promise)
+      .then(result => assert_equals(result, 'PASS'));
+  }, 'Opaque responses should not be reused for XHRs, done case');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html
new file mode 100644
index 0000000..7d21218
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<title>Cache Storage: verify scripts loaded from cache_storage are marked opaque</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+'use strict';
+
+const SW_URL = 'resources/opaque-script-sw.js';
+const BASE_SCOPE = './resources/opaque-script-frame.html';
+const SAME_ORIGIN_BASE = new URL('./resources/', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./resources/',
+    get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+function wait_for_error() {
+  return new Promise(resolve => {
+    self.addEventListener('message', function messageHandler(evt) {
+      if (evt.data.type !== 'ErrorEvent')
+        return;
+      self.removeEventListener('message', messageHandler);
+      resolve(evt.data.msg);
+    });
+  });
+}
+
+// Load an iframe that dynamically adds a script tag that is
+// same/cross origin and large/small.  It then calls a function
+// defined in that loaded script that throws an unhandled error.
+// The resulting message exposed in the global onerror handler
+// is reported back from this function.  Opaque cross origin
+// scripts should not expose the details of the uncaught exception.
+async function get_error_message(t, mode, size) {
+  const script_base = mode === 'same-origin' ? SAME_ORIGIN_BASE
+                                             : CROSS_ORIGIN_BASE;
+  const script = script_base + `opaque-script-${size}.js`;
+  const scope = BASE_SCOPE + `?script=${script}`;
+  const reg = await service_worker_unregister_and_register(t, SW_URL, scope);
+  t.add_cleanup(_ => reg.unregister());
+  assert_true(!!reg.installing);
+  await wait_for_state(t, reg.installing, 'activated');
+  const error_promise = wait_for_error();
+  const f = await with_iframe(scope);
+  t.add_cleanup(_ => f.remove());
+  const error = await error_promise;
+  return error;
+}
+
+promise_test(async t => {
+  const error = await get_error_message(t, 'same-origin', 'small');
+  assert_true(error.includes('Intentional error'));
+}, 'Verify small same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+  const error = await get_error_message(t, 'same-origin', 'large');
+  assert_true(error.includes('Intentional error'));
+}, 'Verify large same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+  const error = await get_error_message(t, 'cross-origin', 'small');
+  assert_false(error.includes('Intentional error'));
+}, 'Verify small cross-origin cache_storage scripts are opaque.');
+
+promise_test(async t => {
+  const error = await get_error_message(t, 'cross-origin', 'large');
+  assert_false(error.includes('Intentional error'));
+}, 'Verify large cross-origin cache_storage scripts are opaque.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html
new file mode 100644
index 0000000..1f42c52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test creates a iframe in a first-party context and then registers a
+service worker (such that the iframe client is unclaimed).
+A third-party iframe is then created which has its SW call clients.claim()
+and then the test checks that the 1p iframe was not claimed int he process.
+Finally the test has its SW call clients.claim() and confirms the 1p iframe is
+claimed.
+
+<script>
+promise_test(async t => {
+  const script = './resources/partitioned-storage-sw.js';
+  const scope = './resources/partitioned-';
+
+  // Add a 1p iframe.
+  const wait_frame_url = new URL(
+    './resources/partitioned-service-worker-iframe-claim.html?1p-mode',
+    self.location);
+
+  const frame = await with_iframe(wait_frame_url, false);
+  t.add_cleanup(async () => {
+    frame.remove();
+  });
+
+  // Add service worker to this 1P context.
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Register the message listener.
+  self.addEventListener('message', messageEventHandler);
+
+  // Now we need to create a third-party iframe whose SW will claim it and then
+  // the iframe will postMessage that its serviceWorker.controller state has
+  // changed.
+  const third_party_iframe_url = new URL(
+    './resources/partitioned-service-worker-iframe-claim.html?3p-mode',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+  // Create the 3p window (which will in turn create the iframe with the SW)
+  // and await on its data.
+  const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url,
+    'window');
+  assert_equals(frame_3p_data.status, "success",
+     "3p iframe was successfully claimed");
+
+  // Confirm that the 1p iframe wasn't claimed at the same time.
+  const controller_1p_iframe = makeMessagePromise();
+  frame.contentWindow.postMessage({type: "get-controller"});
+  const controller_1p_iframe_data = await controller_1p_iframe;
+  assert_equals(controller_1p_iframe_data.controller, null,
+     "Test iframe client isn't claimed yet.");
+
+
+  // Tell the SW to claim.
+  const claimed_1p_iframe = makeMessagePromise();
+  reg.active.postMessage({type: "claim"});
+  const claimed_1p_iframe_data = await claimed_1p_iframe;
+
+  assert_equals(claimed_1p_iframe_data.status, "success",
+     "iframe client was successfully claimed.");
+
+}, "ServiceWorker's clients.claim() is partitioned");
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
new file mode 100644
index 0000000..7c4d4f1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets the SW's (randomly)
+generated ID. It does the same thing for the SW but in a third-party context
+and then confirms that the IDs are different.
+
+<script>
+promise_test(async t => {
+  const script = './resources/partitioned-storage-sw.js'
+  const scope = './resources/partitioned-'
+  const absoluteScope = new URL(scope, window.location).href;
+
+  // Add service worker to this 1P context.
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Register the message listener.
+  self.addEventListener('message', messageEventHandler);
+
+  // Open an iframe that will create a promise within the SW.
+  // The query param is there to track which request the service worker is
+  // handling.
+  //
+  // This promise is necessary to prevent the service worker from being
+  // shutdown during the test which would cause a new ID to be generated
+  // and thus invalidate the test.
+  const wait_frame_url = new URL(
+    './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+    self.location);
+
+  // We don't really need the data the SW sent us from this request
+  // but we can use the ID to confirm the SW wasn't shut down during the
+  // test.
+  const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+                                                       'iframe');
+
+  // Now we need to create a third-party iframe that will send us its SW's
+  // ID.
+  const third_party_iframe_url = new URL(
+    './resources/partitioned-service-worker-third-party-iframe-getRegistrations.html',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+  // Create the 3p window (which will in turn create the iframe with the SW)
+  // and await on its data.
+  const frame_3p_ID = await loadAndReturnSwData(t, third_party_iframe_url,
+    'window');
+
+  // Now get this frame's SW's ID.
+  const frame_1p_ID_promise = makeMessagePromise();
+
+  const retrieved_registrations =
+        await navigator.serviceWorker.getRegistrations();
+  // It's possible that other tests have left behind other service workers.
+  // This steps filters those other SWs out.
+  const filtered_registrations =
+    retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+  // Register a listener on the service worker container and then forward to
+  // the self event listener so we can reuse the existing message promise
+  // function.
+  navigator.serviceWorker.addEventListener('message', evt => {
+    self.postMessage(evt.data, '*');
+  });
+
+  filtered_registrations[0].active.postMessage({type: "get-id"});
+
+  const  frame_1p_ID = await frame_1p_ID_promise;
+
+  // First check that the SW didn't shutdown during the run of the test.
+  // (Note: We're not using assert_equals because random values make it
+  // difficult to use a test expectations file.)
+  assert_true(wait_frame_1p_data.ID === frame_1p_ID.ID,
+    "1p SW didn't shutdown");
+  // Now check that the 1p and 3p IDs differ.
+  assert_false(frame_1p_ID.ID === frame_3p_ID.ID,
+    "1p SW ID matches 3p SW ID");
+
+  // Finally, for clean up, resolve the SW's promise so it stops waiting.
+  const resolve_frame_url = new URL(
+    './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+  // We don't care about the data.
+  await loadAndReturnSwData(t, resolve_frame_url, 'iframe');
+
+}, "ServiceWorker's getRegistrations() is partitioned");
+
+
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
new file mode 100644
index 0000000..46beec8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets has the SW send
+its list of clients from client.matchAll(). It does the same thing for the
+SW in a third-party context as well and confirms that each SW see's the correct
+clients and that they don't see eachother's clients.
+
+<script>
+promise_test(async t => {
+
+  const script = './resources/partitioned-storage-sw.js'
+  const scope = './resources/partitioned-'
+
+  // Add service worker to this 1P context.
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Register the message listener.
+  self.addEventListener('message', messageEventHandler);
+
+  // Create a third-party iframe that will send us its SW's clients.
+  const third_party_iframe_url = new URL(
+    './resources/partitioned-service-worker-third-party-iframe-matchAll.html',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+  const {urls_list: frame_3p_urls_list} = await loadAndReturnSwData(t,
+    third_party_iframe_url, 'window');
+
+  // Register a listener on the service worker container and then forward to
+  // the self event listener so we can reuse the existing message promise
+  // function.
+  navigator.serviceWorker.addEventListener('message', evt => {
+    self.postMessage(evt.data, '*');
+  });
+
+  const frame_1p_data_promise = makeMessagePromise();
+
+  reg.active.postMessage({type: "get-match-all"});
+
+  const {urls_list: frame_1p_urls_list} = await frame_1p_data_promise;
+
+  // If partitioning is working, the 1p and 3p SWs should only see a single
+  // client.
+  assert_equals(frame_3p_urls_list.length, 1);
+  assert_equals(frame_1p_urls_list.length, 1);
+  // Confirm that the expected URL was seen by each.
+  assert_equals(frame_3p_urls_list[0], third_party_iframe_url.toString(),
+    "3p SW has the correct client url.");
+  assert_equals(frame_1p_urls_list[0], window.location.href,
+    "1P SW has the correct client url.");
+}, "ServiceWorker's matchAll() is partitioned");
+
+
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html
new file mode 100644
index 0000000..17a375f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+  <!-- Debugging text for both test cases -->
+  The 3p iframe's postMessage:
+  <p id="iframe_response">No message received</p>
+
+  The nested iframe's postMessage:
+  <p id="nested_iframe_response">No message received</p>
+
+<script>
+promise_test(async t => {
+  const script = './resources/partitioned-storage-sw.js'
+  const scope = './resources/partitioned-'
+
+  // Add service worker to this 1P context. wait_for_state() and
+  // service_worker_unregister_and_register() are helper functions
+  // for creating test ServiceWorkers defined in:
+  // service-workers/service-worker/resources/test-helpers.sub.js
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Registers the message listener with messageEventHandler(), defined in:
+  // service-workers/service-worker/resources/partitioned-utils.js
+  self.addEventListener('message', messageEventHandler);
+
+  // Open an iframe that will create a promise within the SW.
+  // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+  // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+  // `?From1pFrame`: query param that tracks which request the service worker is
+  // handling.
+  const wait_frame_url = new URL(
+    './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+    self.location);
+
+  // Loads a child iframe with wait_frame_url as the content and returns
+  // a promise for the data messaged from the loaded iframe.
+  // loadAndReturnSwData() defined in:
+  // service-workers/service-worker/resources/partitioned-utils.js:
+  const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+                                                       'iframe');
+  assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+
+  // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+  const third_party_iframe_url = new URL(
+    './resources/partitioned-service-worker-third-party-iframe.html',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+  // loadAndReturnSwData() creates a HTTPS_NOTSAMESITE_ORIGIN or 3p `window`
+  // element which embeds an iframe with the ServiceWorker and returns
+  // a promise of the data messaged from that frame.
+  const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, 'window');
+  assert_equals(frame_3p_data.source, 'From3pFrame',
+    'The data for the 3p frame came from the wrong source');
+
+  // Print some debug info to the main frame.
+  document.getElementById("iframe_response").innerHTML =
+      "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+      frame_3p_data.source + ". ";
+
+  // Now do the same for the 1p iframe.
+  // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+  // `resolve.fakehtml`: URL scope that resolves the promise.
+  const resolve_frame_url = new URL(
+    './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+  const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+                                                  'iframe');
+  assert_equals(frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+  // Both the 1p frames should have been serviced by the same service worker ID.
+  // If this isn't the case then that means the SW could have been deactivated
+  // which invalidates the test.
+  assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+    'The 1p frames were serviced by different service workers.');
+
+  document.getElementById("iframe_response").innerHTML +=
+    "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+    frame_1p_data.source;
+
+  // If partitioning is working correctly then only the 1p iframe should see
+  // (and resolve) its SW's promise. Additionally the two frames should see
+  // different IDs.
+  assert_true(frame_1p_data.has_pending,
+      'The 1p iframe saw a pending promise in the service worker.');
+  assert_false(frame_3p_data.has_pending,
+    'The 3p iframe saw a pending promise in the service worker.');
+  assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+    'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+// Optional Test: Checking for partitioned ServiceWorkers in an A->B->A
+// (nested-iframes with cross-site ancestor) scenario.
+promise_test(async t => {
+  const script = './resources/partitioned-storage-sw.js'
+  const scope = './resources/partitioned-'
+
+  // Add service worker to this 1P context. wait_for_state() and
+  // service_worker_unregister_and_register() are helper functions
+  // for creating test ServiceWorkers defined in:
+  // service-workers/service-worker/resources/test-helpers.sub.js
+  const reg = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  // Registers the message listener with messageEventHandler(), defined in:
+  // service-workers/service-worker/resources/partitioned-utils.js
+  self.addEventListener('message', messageEventHandler);
+
+  // Open an iframe that will create a promise within the SW.
+  // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+  // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+  // `?From1pFrame`: query param that tracks which request the service worker is
+  // handling.
+  const wait_frame_url = new URL(
+    './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+    self.location);
+
+  // Load a child iframe with wait_frame_url as the content.
+  // loadAndReturnSwData() defined in:
+  // service-workers/service-worker/resources/partitioned-utils.js:
+  const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+                                                       'iframe');
+  assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+
+  // Now create a set of nested iframes in the configuration A1->B->A2
+  // where B is cross-site and A2 is same-site to this top-level
+  // site (A1). The innermost iframe of the nested iframes (A2) will
+  // create an additional iframe to finally resolve the ServiceWorker.
+  const nested_iframe_url = new URL(
+    './resources/partitioned-service-worker-nested-iframe-parent.html',
+    get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+  // Create the nested iframes (which will in turn create the iframe
+  // with the ServiceWorker) and await on receiving its data.
+  const nested_iframe_data = await loadAndReturnSwData(t, nested_iframe_url, 'iframe');
+  assert_equals(nested_iframe_data.source, 'FromNestedFrame',
+    'The data for the nested iframe frame came from the wrong source');
+
+  // Print some debug info to the main frame.
+  document.getElementById("nested_iframe_response").innerHTML =
+      "Nested iframe's has_pending: " + nested_iframe_data.has_pending + " source: " +
+      nested_iframe_data.source + ". ";
+
+  // Now do the same for the 1p iframe.
+  // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+  // `resolve.fakehtml`: URL scope that resolves the promise.
+  const resolve_frame_url = new URL(
+    './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+  const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+                                                  'iframe');
+  assert_equals(frame_1p_data.source, 'From1pFrame',
+    'The data for the 1p frame came from the wrong source');
+  // Both the 1p frames should have been serviced by the same service worker ID.
+  // If this isn't the case then that means the SW could have been deactivated
+  // which invalidates the test.
+  assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+    'The 1p frames were serviced by different service workers.');
+
+  document.getElementById("nested_iframe_response").innerHTML +=
+    "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+    frame_1p_data.source;
+
+  // If partitioning is working correctly then only the 1p iframe should see
+  // (and resolve) its SW's promise. Additionally, the innermost iframe of
+  // the nested iframes (A2 in the configuration A1->B->A2) should have a
+  // different service worker ID than the 1p (A1) frame.
+  assert_true(frame_1p_data.has_pending,
+      'The 1p iframe saw a pending promise in the service worker.');
+  assert_false(nested_iframe_data.has_pending,
+    'The 3p iframe saw a pending promise in the service worker.');
+  assert_not_equals(frame_1p_data.ID, nested_iframe_data.ID,
+    'The frames were serviced by the same service worker thread.');
+}, 'Services workers with cross-site ancestors are partitioned.');
+
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html b/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html
new file mode 100644
index 0000000..e56e6fe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+    'resources/performance-timeline-worker.js',
+    'Test Performance Timeline API in Service Worker');
+
+// The purpose of this test is to verify that service worker overhead
+// is included in the Performance API's timing information.
+promise_test(t => {
+  let script = 'resources/empty-but-slow-worker.js';
+  let scope = 'resources/sample.txt?slow-sw-timing';
+  let url = new URL(scope, window.location).href;
+  let slowURL = url + '&slow';
+  let frame;
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(reg => {
+        t.add_cleanup(() => service_worker_unregister(t, scope));
+
+        return wait_for_state(t, reg.installing, 'activated');
+      })
+    .then(_ => with_iframe(scope))
+    .then(f => {
+      frame = f;
+      return frame.contentWindow.fetch(url).then(r => r && r.text());
+    })
+    .then(_ => {
+      return frame.contentWindow.fetch(slowURL).then(r => r && r.text());
+    })
+    .then(_ => {
+      function elapsed(u) {
+        let entry = frame.contentWindow.performance.getEntriesByName(u);
+        return entry[0] ? entry[0].duration : undefined;
+      }
+      let urlTime = elapsed(url);
+      let slowURLTime = elapsed(slowURL);
+      // Verify the request slowed by the service worker is indeed measured
+      // to be slower.  Note, we compare to smaller delay instead of the exact
+      // delay amount to avoid making the test racy under automation.
+      assert_greater_than(slowURLTime, urlTime + 1000,
+                  'Slow service worker request should measure increased delay.');
+      frame.remove();
+    })
+}, 'empty service worker fetch event included in performance timings');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html
new file mode 100644
index 0000000..16fddd5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage Blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    let script = 'resources/postmessage-blob-url.js';
+    let scope = 'resources/blank.html';
+    let registration;
+    let blobText = 'Blob text';
+    let blob;
+    let blobUrl;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(r => {
+          add_completion_callback(() => r.unregister());
+          registration = r;
+          let worker = registration.installing;
+          blob = new Blob([blobText]);
+          blobUrl = URL.createObjectURL(blob);
+          return new Promise(resolve => {
+            navigator.serviceWorker.onmessage = e => { resolve(e.data); }
+            worker.postMessage(blobUrl);
+          });
+        })
+      .then(response => {
+          assert_equals(response, 'Worker reply:' + blobText);
+          URL.revokeObjectURL(blobUrl);
+          return registration.unregister();
+        });
+  }, 'postMessage Blob URL to a ServiceWorker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
new file mode 100644
index 0000000..117def9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage from waiting serviceworker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function echo(worker, data) {
+  return new Promise(resolve => {
+    navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+      navigator.serviceWorker.removeEventListener('message', onMsg);
+      resolve(evt);
+    });
+    worker.postMessage(data);
+  });
+}
+
+promise_test(t => {
+  let script = 'resources/echo-message-to-source-worker.js';
+  let scope = 'resources/client-postmessage-from-wait-serviceworker';
+  let registration;
+  let frame;
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(swr => {
+      t.add_cleanup(() => service_worker_unregister(t, scope));
+
+      registration = swr;
+      return wait_for_state(t, registration.installing, 'activated');
+    }).then(_ => {
+      return with_iframe(scope);
+    }).then(f => {
+      frame = f;
+      return navigator.serviceWorker.register(script + '?update', { scope: scope })
+    }).then(swr => {
+      assert_equals(swr, registration, 'should be same registration');
+      return wait_for_state(t, registration.installing, 'installed');
+    }).then(_ => {
+      return echo(registration.waiting, 'waiting');
+    }).then(evt => {
+      assert_equals(evt.source, registration.waiting,
+                    'message event source should be correct');
+      return echo(registration.active, 'active');
+    }).then(evt => {
+      assert_equals(evt.source, registration.active,
+                    'message event source should be correct');
+      frame.remove();
+    });
+}, 'Client.postMessage() from waiting serviceworker.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
new file mode 100644
index 0000000..29c0560
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage via MessagePort to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/postmessage-msgport-to-client-worker.js';
+    var scope = 'resources/blank.html';
+    var port;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(registration => {
+          add_completion_callback(() => registration.unregister());
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => with_iframe(scope))
+      .then(frame => {
+          t.add_cleanup(() => frame.remove());
+          return new Promise(resolve => {
+              var w = frame.contentWindow;
+              w.navigator.serviceWorker.onmessage = resolve;
+              w.navigator.serviceWorker.controller.postMessage('ping');
+            });
+        })
+      .then(e => {
+          port = e.ports[0];
+          port.postMessage({value: 1});
+          port.postMessage({value: 2});
+          port.postMessage({done: true});
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          assert_equals(e.data.ack, 'Acking value: 1');
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          assert_equals(e.data.ack, 'Acking value: 2');
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => { assert_true(e.data.done, 'done'); });
+  }, 'postMessage MessagePorts from ServiceWorker to Client');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
new file mode 100644
index 0000000..83e5f45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client (message queue)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This function creates a message listener that captures all messages
+// sent to this window and matches them with corresponding requests.
+// This frees test code from having to use clunky constructs just to
+// avoid race conditions, since the relative order of message and
+// request arrival doesn't matter.
+function create_message_listener(t) {
+    const listener = {
+        messages: new Set(),
+        requests: new Set(),
+        waitFor: function(predicate) {
+            for (const event of this.messages) {
+                // If a message satisfying the predicate has already
+                // arrived, it gets matched to this request.
+                if (predicate(event)) {
+                    this.messages.delete(event);
+                    return Promise.resolve(event);
+                }
+            }
+
+            // If no match was found, the request is stored and a
+            // promise is returned.
+            const request = { predicate };
+            const promise = new Promise(resolve => request.resolve = resolve);
+            this.requests.add(request);
+            return promise;
+        }
+    };
+    window.onmessage = t.step_func(event => {
+        for (const request of listener.requests) {
+            // If the new message matches a stored request's
+            // predicate, the request's promise is resolved with this
+            // message.
+            if (request.predicate(event)) {
+                listener.requests.delete(request);
+                request.resolve(event);
+                return;
+            }
+        };
+
+        // No outstanding request for this message, store it in case
+        // it's requested later.
+        listener.messages.add(event);
+    });
+    return listener;
+}
+
+async function service_worker_register_and_activate(t, script, scope) {
+    const registration = await service_worker_unregister_and_register(t, script, scope);
+    t.add_cleanup(() => registration.unregister());
+    const worker = registration.installing;
+    await wait_for_state(t, worker, 'activated');
+    return worker;
+}
+
+// Add an iframe (parent) whose document contains a nested iframe
+// (child), then set the child's src attribute to child_url and return
+// its Window (without waiting for it to finish loading).
+async function with_nested_iframes(t, child_url) {
+    const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
+    t.add_cleanup(() => parent.remove());
+    const child = parent.contentWindow.document.getElementById('child');
+    child.setAttribute('src', child_url);
+    return child.contentWindow;
+}
+
+// Returns a predicate matching a fetch message with the specified
+// key.
+function fetch_message(key) {
+    return event => event.data.type === 'fetch' && event.data.key === key;
+}
+
+// Returns a predicate matching a ping message with the specified
+// payload.
+function ping_message(data) {
+    return event => event.data.type === 'ping' && event.data.data === data;
+}
+
+// A client message queue test is a testharness.js test with some
+// additional setup:
+// 1. A listener (see create_message_listener)
+// 2. An active service worker
+// 3. Two nested iframes
+// 4. A state transition function that controls the order of events
+//    during the test
+function client_message_queue_test(url, test_function, description) {
+    promise_test(async t => {
+        t.listener = create_message_listener(t);
+
+        const script = 'resources/stalling-service-worker.js';
+        const scope = 'resources/';
+        t.service_worker = await service_worker_register_and_activate(t, script, scope);
+
+        // We create two nested iframes such that both are controlled by
+        // the newly installed service worker.
+        const child_url = url + '?role=child';
+        t.frame = await with_nested_iframes(t, child_url);
+
+        t.state_transition = async function(from, to, scripts) {
+            // A state transition begins with the child's parser
+            // fetching a script due to a <script> tag. The request
+            // arrives at the service worker, which notifies the
+            // parent, which in turn notifies the test. Note that the
+            // event loop keeps spinning while the parser is waiting.
+            const request = await this.listener.waitFor(fetch_message(to));
+
+            // The test instructs the service worker to send two ping
+            // messages through the Client interface: first to the
+            // child, then to the parent.
+            this.service_worker.postMessage(from);
+
+            // When the parent receives the ping message, it forwards
+            // it to the test. Assuming that messages to both child
+            // and parent are mapped to the same task queue (this is
+            // not [yet] required by the spec), receiving this message
+            // guarantees that the child has already dispatched its
+            // message if it was allowed to do so.
+            await this.listener.waitFor(ping_message(from));
+
+            // Finally, reply to the service worker's fetch
+            // notification with the script it should use as the fetch
+            // request's response. This is a defensive mechanism that
+            // ensures the child's parser really is blocked until the
+            // test is ready to continue.
+            request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
+        };
+
+        await test_function(t);
+    }, description);
+}
+
+function client_message_queue_enable_test(
+    install_script,
+    start_script,
+    earliest_dispatch,
+    description)
+{
+    function assert_state_less_than_equal(state1, state2, explanation) {
+        const states = ['init', 'install', 'start', 'finish', 'loaded'];
+        const index1 = states.indexOf(state1);
+        const index2 = states.indexOf(state2);
+        if (index1 > index2)
+          assert_unreached(explanation);
+    }
+
+    client_message_queue_test('enable-client-message-queue.html', async t => {
+        // While parsing the child's document, the child transitions
+        // from the 'init' state all the way to the 'finish' state.
+        // Once parsing is finished it would enter the final 'loaded'
+        // state. All but the last transition require assitance from
+        // the test.
+        await t.state_transition('init', 'install', [install_script]);
+        await t.state_transition('install', 'start', [start_script]);
+        await t.state_transition('start', 'finish', []);
+
+        // Wait for all messages to get dispatched on the child's
+        // ServiceWorkerContainer and then verify that each message
+        // was dispatched after |earliest_dispatch|.
+        const report = await t.frame.report;
+        ['init', 'install', 'start'].forEach(state => {
+            const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`;
+            assert_state_less_than_equal(earliest_dispatch,
+                                         report[state],
+                                         explanation);
+        });
+    }, description);
+}
+
+const empty_script = ``;
+
+const add_event_listener =
+    `navigator.serviceWorker.addEventListener('message', handle_message);`;
+
+const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
+
+const start_messages = `navigator.serviceWorker.startMessages();`;
+
+client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
+    'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
+
+client_message_queue_enable_test(add_event_listener, start_messages, 'start',
+    'Messages from ServiceWorker to Client only received after calling startMessages().');
+
+client_message_queue_enable_test(set_onmessage, empty_script, 'install',
+    'Messages from ServiceWorker to Client only received after setting onmessage.');
+
+const resolve_manual_promise = `resolve_manual_promise();`
+
+async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
+    await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
+    let result = await t.frame.result;
+    assert_equals(result[0], 'microtask', 'The microtask was executed first.');
+    assert_equals(result[1], 'message', 'The message was dispatched.');
+}
+
+client_message_queue_test('message-vs-microtask.html', t => {
+    return test_microtasks_when_client_message_queue_enabled(t, [
+        add_event_listener,
+        start_messages,
+    ]);
+}, 'Microtasks run before dispatching messages after calling startMessages().');
+
+client_message_queue_test('message-vs-microtask.html', t => {
+    return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
+}, 'Microtasks run before dispatching messages after setting onmessage.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html
new file mode 100644
index 0000000..f834a4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+  const script = 'resources/postmessage-to-client-worker.js';
+  const scope = 'resources/blank.html';
+
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+  const w = frame.contentWindow;
+
+  w.navigator.serviceWorker.controller.postMessage('ping');
+  let e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+
+  assert_equals(e.constructor, w.MessageEvent,
+                'message events should use MessageEvent interface.');
+  assert_equals(e.type, 'message', 'type should be "message".');
+  assert_false(e.bubbles, 'message events should not bubble.');
+  assert_false(e.cancelable, 'message events should not be cancelable.');
+  assert_equals(e.origin, location.origin,
+                'origin of message should be origin of Service Worker.');
+  assert_equals(e.lastEventId, '',
+                'lastEventId should be an empty string.');
+  assert_equals(e.source.constructor, w.ServiceWorker,
+                'source should use ServiceWorker interface.');
+  assert_equals(e.source, w.navigator.serviceWorker.controller,
+                'source should be the service worker that sent the message.');
+  assert_equals(e.ports.length, 0, 'ports should be an empty array.');
+  assert_equals(e.data, 'Sending message via clients');
+
+  e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+  assert_equals(e.data, 'quit');
+}, 'postMessage from ServiceWorker to Client.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html
new file mode 100644
index 0000000..7abb302
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    var script = 'resources/postmessage-worker.js';
+    var scope = 'resources/blank.html';
+    var registration;
+    var worker;
+    var port;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(r => {
+          t.add_cleanup(() => r.unregister());
+          registration = r;
+          worker = registration.installing;
+
+          var messageChannel = new MessageChannel();
+          port = messageChannel.port1;
+          return new Promise(resolve => {
+              port.onmessage = resolve;
+              worker.postMessage({port: messageChannel.port2},
+                                 [messageChannel.port2]);
+              worker.postMessage({value: 1});
+              worker.postMessage({value: 2});
+              worker.postMessage({done: true});
+            });
+        })
+      .then(e => {
+          assert_equals(e.data, 'Acking value: 1');
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          assert_equals(e.data, 'Acking value: 2');
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          assert_equals(e.data, 'quit');
+          return registration.unregister(scope);
+        });
+  }, 'postMessage to a ServiceWorker (and back via MessagePort)');
+
+promise_test(t => {
+    var script = 'resources/postmessage-transferables-worker.js';
+    var scope = 'resources/blank.html';
+    var sw = navigator.serviceWorker;
+
+    var message = 'Hello, world!';
+    var text_encoder = new TextEncoder;
+    var text_decoder = new TextDecoder;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(r => {
+          t.add_cleanup(() => r.unregister());
+
+          var ab = text_encoder.encode(message);
+          assert_equals(ab.byteLength, message.length);
+          r.installing.postMessage(ab, [ab.buffer]);
+          assert_equals(text_decoder.decode(ab), '');
+          assert_equals(ab.byteLength, 0);
+
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the transferred array buffer.
+          assert_equals(e.data.content, message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the array buffer sent back from
+          // ServiceWorker via Client.postMessage.
+          assert_equals(text_decoder.decode(e.data), message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify that the array buffer on ServiceWorker is neutered.
+          assert_equals(e.data.content, '');
+          assert_equals(e.data.byteLength, 0);
+        });
+  }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client');
+
+promise_test(t => {
+    var script = 'resources/postmessage-transferables-worker.js';
+    var scope = 'resources/blank.html';
+    var message = 'Hello, world!';
+    var text_encoder = new TextEncoder;
+    var text_decoder = new TextDecoder;
+    var port;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(r => {
+          t.add_cleanup(() => r.unregister());
+
+          var channel = new MessageChannel;
+          port = channel.port1;
+          r.installing.postMessage(undefined, [channel.port2]);
+
+          var ab = text_encoder.encode(message);
+          assert_equals(ab.byteLength, message.length);
+          port.postMessage(ab, [ab.buffer]);
+          assert_equals(text_decoder.decode(ab), '');
+          assert_equals(ab.byteLength, 0);
+
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the transferred array buffer.
+          assert_equals(e.data.content, message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the array buffer sent back from
+          // ServiceWorker via Client.postMessage.
+          assert_equals(text_decoder.decode(e.data), message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { port.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify that the array buffer on ServiceWorker is neutered.
+          assert_equals(e.data.content, '');
+          assert_equals(e.data.byteLength, 0);
+        });
+  }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client' +
+     ' over MessagePort');
+
+  promise_test(t => {
+    var script = 'resources/postmessage-dictionary-transferables-worker.js';
+    var scope = 'resources/blank.html';
+    var sw = navigator.serviceWorker;
+
+    var message = 'Hello, world!';
+    var text_encoder = new TextEncoder;
+    var text_decoder = new TextDecoder;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(r => {
+          t.add_cleanup(() => r.unregister());
+
+          var ab = text_encoder.encode(message);
+          assert_equals(ab.byteLength, message.length);
+          r.installing.postMessage(ab, {transfer: [ab.buffer]});
+          assert_equals(text_decoder.decode(ab), '');
+          assert_equals(ab.byteLength, 0);
+
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the transferred array buffer.
+          assert_equals(e.data.content, message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify the integrity of the array buffer sent back from
+          // ServiceWorker via Client.postMessage.
+          assert_equals(text_decoder.decode(e.data), message);
+          assert_equals(e.data.byteLength, message.length);
+          return new Promise(resolve => { sw.onmessage = resolve; });
+        })
+      .then(e => {
+          // Verify that the array buffer on ServiceWorker is neutered.
+          assert_equals(e.data.content, '');
+          assert_equals(e.data.byteLength, 0);
+        });
+  }, 'postMessage with dictionary a transferable ArrayBuffer between ServiceWorker and Client');
+
+  promise_test(async t => {
+    const firstScript = 'resources/postmessage-echo-worker.js?one';
+    const secondScript = 'resources/postmessage-echo-worker.js?two';
+    const scope = 'resources/';
+
+    const registration = await service_worker_unregister_and_register(t, firstScript, scope);
+    t.add_cleanup(() => registration.unregister());
+    const firstWorker = registration.installing;
+
+    const messagePromise = new Promise(resolve => {
+      navigator.serviceWorker.addEventListener('message', (event) => {
+        resolve(event.data);
+      }, {once: true});
+    });
+
+    await wait_for_state(t, firstWorker, 'activated');
+    await navigator.serviceWorker.register(secondScript, {scope});
+    const secondWorker = registration.installing;
+    await wait_for_state(t, firstWorker, 'redundant');
+
+    // postMessage() to a redundant worker should be dropped silently.
+    // Historically, this threw an exception.
+    firstWorker.postMessage('firstWorker');
+
+    // To test somewhat that it was not received, send a message to another
+    // worker and check that we get a reply for that one.
+    secondWorker.postMessage('secondWorker');
+    const data = await messagePromise;
+    assert_equals(data, 'secondWorker');
+  }, 'postMessage to a redundant worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js
new file mode 100644
index 0000000..6c4e270
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js
@@ -0,0 +1,223 @@
+// META: title=Service Worker: navigator.serviceWorker.ready
+// META: script=resources/test-helpers.sub.js
+
+test(() => {
+  assert_equals(
+    navigator.serviceWorker.ready,
+    navigator.serviceWorker.ready,
+    'repeated access to ready without intervening registrations should return the same Promise object'
+  );
+}, 'ready returns the same Promise object');
+
+promise_test(async t => {
+  const frame = await with_iframe('resources/blank.html?uncontrolled');
+  t.add_cleanup(() => frame.remove());
+
+  const promise = frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(
+    Object.getPrototypeOf(promise),
+    frame.contentWindow.Promise.prototype,
+    'the Promise should be in the context of the related document'
+  );
+}, 'ready returns a Promise object in the context of the related document');
+
+promise_test(async t => {
+  const url = 'resources/empty-worker.js';
+  const scope = 'resources/blank.html?ready-controlled';
+  const expectedURL = normalizeURL(url);
+  const registration = await service_worker_unregister_and_register(t, url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(readyReg.installing, null, 'installing should be null');
+  assert_equals(readyReg.waiting, null, 'waiting should be null');
+  assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null');
+  assert_equals(
+    frame.contentWindow.navigator.serviceWorker.controller,
+    readyReg.active,
+    'the controller should be the active worker'
+  );
+  assert_in_array(
+    readyReg.active.state,
+    ['activating', 'activated'],
+    '.ready should be resolved when the registration has an active worker'
+  );
+}, 'ready on a controlled document');
+
+promise_test(async t => {
+  const url = 'resources/empty-worker.js';
+  const scope = 'resources/blank.html?ready-potential-controlled';
+  const expected_url = normalizeURL(url);
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  const registration = await navigator.serviceWorker.register(url, { scope });
+  t.add_cleanup(() => registration.unregister());
+
+  const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(readyReg.installing, null, 'installing should be null');
+  assert_equals(readyReg.waiting, null, 'waiting should be null.')
+  assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null');
+  assert_in_array(
+    readyReg.active.state,
+    ['activating', 'activated'],
+    '.ready should be resolved when the registration has an active worker'
+  );
+  assert_equals(
+    frame.contentWindow.navigator.serviceWorker.controller,
+    null,
+    'uncontrolled document should not have a controller'
+  );
+}, 'ready on a potential controlled document');
+
+promise_test(async t => {
+  const url = 'resources/empty-worker.js';
+  const scope = 'resources/blank.html?ready-installing';
+
+  await service_worker_unregister(t, scope);
+
+  const frame = await with_iframe(scope);
+  const promise = frame.contentWindow.navigator.serviceWorker.ready;
+  navigator.serviceWorker.register(url, { scope });
+  const registration = await promise;
+
+  t.add_cleanup(async () => {
+    await registration.unregister();
+    frame.remove();
+  });
+
+  assert_equals(registration.installing, null, 'installing should be null');
+  assert_equals(registration.waiting, null, 'waiting should be null');
+  assert_not_equals(registration.active, null, 'active after ready should not be null');
+  assert_in_array(
+    registration.active.state,
+    ['activating', 'activated'],
+    '.ready should be resolved when the registration has an active worker'
+  );
+}, 'ready on an iframe whose parent registers a new service worker');
+
+promise_test(async t => {
+  const scope = 'resources/register-iframe.html';
+  const frame = await with_iframe(scope);
+
+  const registration = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  t.add_cleanup(async () => {
+    await registration.unregister();
+    frame.remove();
+  });
+
+  assert_equals(registration.installing, null, 'installing should be null');
+  assert_equals(registration.waiting, null, 'waiting should be null');
+  assert_not_equals(registration.active, null, 'active after ready should not be null');
+  assert_in_array(
+    registration.active.state,
+    ['activating', 'activated'],
+    '.ready should be resolved with "active worker"'
+  );
+ }, 'ready on an iframe that installs a new service worker');
+
+promise_test(async t => {
+  const url = 'resources/empty-worker.js';
+  const matchedScope = 'resources/blank.html?ready-after-match';
+  const longerMatchedScope = 'resources/blank.html?ready-after-match-longer';
+
+  await service_worker_unregister(t, matchedScope);
+  await service_worker_unregister(t, longerMatchedScope);
+
+  const frame = await with_iframe(longerMatchedScope);
+  const registration = await navigator.serviceWorker.register(url, { scope: matchedScope });
+
+  t.add_cleanup(async () => {
+    await registration.unregister();
+    frame.remove();
+  });
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+
+  t.add_cleanup(() => longerRegistration.unregister());
+
+  const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(
+    readyReg.scope,
+    normalizeURL(longerMatchedScope),
+    'longer matched registration should be returned'
+  );
+  assert_equals(
+    frame.contentWindow.navigator.serviceWorker.controller,
+    null,
+    'controller should be null'
+  );
+}, 'ready after a longer matched registration registered');
+
+promise_test(async t => {
+  const url = 'resources/empty-worker.js';
+  const matchedScope = 'resources/blank.html?ready-after-resolve';
+  const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer';
+  const registration = await service_worker_unregister_and_register(t, url, matchedScope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const frame = await with_iframe(longerMatchedScope);
+  t.add_cleanup(() => frame.remove());
+
+  const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(
+    readyReg1.scope,
+    normalizeURL(matchedScope),
+    'matched registration should be returned'
+  );
+
+  const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+  t.add_cleanup(() => longerReg.unregister());
+
+  const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+  assert_equals(
+    readyReg2.scope,
+    normalizeURL(matchedScope),
+    'ready should only be resolved once'
+  );
+}, 'access ready after it has been resolved');
+
+promise_test(async t => {
+  const url1 = 'resources/empty-worker.js';
+  const url2 = url1 + '?2';
+  const matchedScope = 'resources/blank.html?ready-after-unregister';
+  const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope);
+  t.add_cleanup(() => reg1.unregister());
+
+  await wait_for_state(t, reg1.installing, 'activating');
+
+  const frame = await with_iframe(matchedScope);
+  t.add_cleanup(() => frame.remove());
+
+  await reg1.unregister();
+
+  // Ready promise should be pending, waiting for a new registration to arrive
+  const readyPromise = frame.contentWindow.navigator.serviceWorker.ready;
+
+  const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope });
+  t.add_cleanup(() => reg2.unregister());
+
+  const readyReg = await readyPromise;
+
+  // Wait for registration update, since it comes from another global, the states are racy.
+  await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated');
+
+  assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration');
+  assert_not_equals(reg1, reg2, 'Registrations should be different');
+}, 'resolve ready after unregistering');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html
new file mode 100644
index 0000000..71b35d0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<title>Service Worker: Redirected response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests
+// the resulting response against the expected values. It also adds the
+// response to |cache| and checks the cached response matches the expected
+// values.
+//
+// |options|: a dictionary of parameters for the test
+// |options.url|: the URL to fetch
+// |options.fetch_option|: the options passed to |fetch_method|
+// |options.fetch_method|: the method used to fetch. Useful for testing an
+//                         iframe's fetch() vs. this page's fetch().
+// |options.expected_type|: The value of response.type
+// |options.expected_redirected|: The value of response.redirected
+// |options.expected_intercepted_urls|: The list of intercepted request URLs.
+function redirected_test(options) {
+  return options.fetch_method.call(null, options.url, options.fetch_option).then(response => {
+        let cloned_response = response.clone();
+        assert_equals(
+            response.type, options.expected_type,
+            'The response type of response must match. URL: ' + options.url);
+        assert_equals(
+            cloned_response.type, options.expected_type,
+            'The response type of cloned response must match. URL: ' + options.url);
+        assert_equals(
+            response.redirected, options.expected_redirected,
+            'The redirected flag of response must match. URL: ' + options.url);
+        assert_equals(
+            cloned_response.redirected, options.expected_redirected,
+            'The redirected flag of cloned response must match. URL: ' + options.url);
+        if (options.expected_response_url) {
+            assert_equals(
+                cloned_response.url, options.expected_response_url,
+                'The URL does not meet expectation. URL: ' + options.url);
+        }
+        return cache.put(options.url, response);
+      })
+    .then(_ => cache.match(options.url))
+    .then(response => {
+        assert_equals(
+            response.type, options.expected_type,
+            'The response type of response in CacheStorage must match. ' +
+            'URL: ' + options.url);
+        assert_equals(
+            response.redirected, options.expected_redirected,
+            'The redirected flag of response in CacheStorage must match. ' +
+            'URL: ' + options.url);
+        return check_intercepted_urls(options.expected_intercepted_urls);
+      });
+}
+
+async function take_intercepted_urls() {
+  const message = new Promise((resolve) => {
+    let channel = new MessageChannel();
+    channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); };
+    worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+                       [channel.port2]);
+  });
+  const request_infos = await message;
+  return request_infos.map(info => { return info.url; });
+}
+
+function check_intercepted_urls(expected_urls) {
+  return take_intercepted_urls().then((urls) => {
+      assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.');
+    });
+}
+
+function setup_and_clean() {
+  // To prevent interference from previous tests, take the intercepted URLs from
+  // the service worker.
+  return setup.then(() => take_intercepted_urls());
+}
+
+
+let host_info = get_host_info();
+const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+                     'resources/redirect.py?Redirect=';
+const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+                   'resources/simple.txt?';
+const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL);
+let frame;
+let cache;
+let setup;
+let worker;
+
+promise_test(t => {
+    const SCOPE = 'resources/blank.html?redirected-response';
+    const SCRIPT = 'resources/redirect-worker.js';
+    const CACHE_NAME = 'service-workers/service-worker/redirected-response';
+    setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(registration => {
+          promise_test(
+              () => registration.unregister(),
+              'restore global state (service worker registration)');
+          worker = registration.installing;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(_ => self.caches.open(CACHE_NAME))
+      .then(c => {
+          cache = c;
+          promise_test(
+            () => self.caches.delete(CACHE_NAME),
+            'restore global state (caches)');
+          return with_iframe(SCOPE);
+        })
+      .then(f => {
+          frame = f;
+          add_completion_callback(() => f.remove());
+          return check_intercepted_urls(
+              [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]);
+        });
+      return setup;
+  }, 'initialize global state (service worker registration and caches)');
+
+// ===============================================================
+// Tests for requests that are out-of-scope of the service worker.
+// ===============================================================
+promise_test(t => setup_and_clean()
+  .then(() => redirected_test({url: TARGET_URL,
+                               fetch_option: {},
+                               fetch_method: self.fetch,
+                               expected_type: 'basic',
+                               expected_redirected: false,
+                               expected_intercepted_urls: []})),
+  'mode: "follow", non-intercepted request, no server redirect');
+
+promise_test(t => setup_and_clean()
+  .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL,
+                               fetch_option: {},
+                               fetch_method: self.fetch,
+                               expected_type: 'basic',
+                               expected_redirected: true,
+                               expected_intercepted_urls: []})),
+  'mode: "follow", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+  .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual',
+                               fetch_option: {redirect: 'manual'},
+                               fetch_method: self.fetch,
+                               expected_type: 'opaqueredirect',
+                               expected_redirected: false,
+                               expected_intercepted_urls: []})),
+  'mode: "manual", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+    .then(() => promise_rejects_js(
+                   t, TypeError,
+                   self.fetch(REDIRECT_TO_TARGET_URL + '&error',
+                              {redirect:'error'}),
+                   'The redirect response from the server should be treated as' +
+                   ' an error when the redirect flag of request was \'error\'.'))
+    .then(() => check_intercepted_urls([])),
+  'mode: "error", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = TARGET_URL + '&sw=fetch';
+      return redirected_test({url: url,
+                              fetch_option: {},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url]})
+    }),
+  'mode: "follow", no mode change, no server redirect');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a redirected response.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=follow&sw=fetch';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'follow'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: true,
+                              expected_intercepted_urls: [url]})
+    }),
+  'mode: "follow", no mode change');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=error&sw=follow';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'error'}),
+          'The redirected response from the service worker should be ' +
+          'treated as an error when the redirect flag of request was ' +
+          '\'error\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "error", mode change: "follow"');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=manual&sw=follow';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'manual'}),
+          'The redirected response from the service worker should be ' +
+          'treated as an error when the redirect flag of request was ' +
+          '\'manual\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "manual", mode change: "follow"');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns an opaqueredirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=follow&sw=manual';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'follow'}),
+          'The opaqueredirect response from the service worker should ' +
+          'be treated as an error when the redirect flag of request was' +
+          ' \'follow\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "follow", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=error&sw=manual';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'error'}),
+          'The opaqueredirect response from the service worker should ' +
+          'be treated as an error when the redirect flag of request was' +
+          ' \'error\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "error", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = REDIRECT_TO_TARGET_URL +
+                  '&original-redirect-mode=manual&sw=manual';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'manual'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'opaqueredirect',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url]});
+    }),
+  'mode: "manual", no mode change');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=follow&sw=gen';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'follow'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: true,
+                              expected_intercepted_urls: [url, TARGET_URL]})
+    }),
+  'mode: "follow", generated redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=error&sw=gen';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'error'}),
+          'The generated redirect response from the service worker should ' +
+          'be treated as an error when the redirect flag of request was' +
+          ' \'error\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "error", generated redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=manual&sw=gen';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'manual'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'opaqueredirect',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url]})
+    }),
+  'mode: "manual", generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response manually with the Response
+// constructor.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=follow&sw=gen-manual';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'follow'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: true,
+                              expected_intercepted_urls: [url, TARGET_URL]})
+    }),
+  'mode: "follow", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=error&sw=gen-manual';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'error'}),
+          'The generated redirect response from the service worker should ' +
+          'be treated as an error when the redirect flag of request was' +
+          ' \'error\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "error", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=' + encodeURIComponent(TARGET_URL) +
+                  '&original-redirect-mode=manual&sw=gen-manual';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'manual'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'opaqueredirect',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url]})
+    }),
+  'mode: "manual", manually-generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response with a relative location header.
+// Generated responses do not have URLs, so this should fail to resolve.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=blank.html' +
+                  '&original-redirect-mode=follow&sw=gen-manual';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'follow'}),
+          'Following the generated redirect response from the service worker '+
+          'should result fail.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "follow", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=blank.html' +
+                  '&original-redirect-mode=error&sw=gen-manual';
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(url, {redirect: 'error'}),
+          'The generated redirect response from the service worker should ' +
+          'be treated as an error when the redirect flag of request was' +
+          ' \'error\'.')
+        .then(() => check_intercepted_urls([url]));
+    }),
+  'mode: "error", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() +
+                  'sample?url=blank.html' +
+                  '&original-redirect-mode=manual&sw=gen-manual';
+      return redirected_test({url: url,
+                              fetch_option: {redirect: 'manual'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'opaqueredirect',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url]})
+    }),
+  'mode: "manual", generated relative redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response. And the fetch follows the
+// redirection multiple times.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      // The Fetch spec says: "If request’s redirect count is twenty, return a
+      // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+      // So fetch can follow the redirect response 20 times.
+      let urls = [TARGET_URL];
+      for (let i = 0; i < 20; ++i) {
+        urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+                    encodeURIComponent(urls[0]));
+
+      }
+      return redirected_test({url: urls[0],
+                              fetch_option: {redirect: 'follow'},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: true,
+                              expected_intercepted_urls: urls})
+    }),
+  'Fetch should follow the redirect response 20 times');
+
+promise_test(t => setup_and_clean()
+    .then(() => {
+      let urls = [TARGET_URL];
+      // The Fetch spec says: "If request’s redirect count is twenty, return a
+      // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+      // So fetch can't follow the redirect response 21 times.
+      for (let i = 0; i < 21; ++i) {
+        urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+                    encodeURIComponent(urls[0]));
+
+      }
+      return promise_rejects_js(
+          t, frame.contentWindow.TypeError,
+          frame.contentWindow.fetch(urls[0], {redirect: 'follow'}),
+          'Fetch should not follow the redirect response 21 times.')
+        .then(() => {
+          urls.pop();
+          return check_intercepted_urls(urls)
+        });
+    }),
+  'Fetch should not follow the redirect response 21 times.');
+
+// =======================================================
+// A test for verifying the url of a service-worker-redirected request is
+// propagated to the outer response.
+// =======================================================
+promise_test(t => setup_and_clean()
+    .then(() => {
+      const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' +
+                  encodeURIComponent(TARGET_URL) +'&sw=fetch-url';
+      return redirected_test({url: url,
+                              fetch_option: {},
+                              fetch_method: frame.contentWindow.fetch,
+                              expected_type: 'basic',
+                              expected_redirected: false,
+                              expected_intercepted_urls: [url],
+                              expected_response_url: TARGET_URL});
+    }),
+  'The URL for the service worker redirected request should be propagated to ' +
+  'response.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referer.https.html b/third_party/web_platform_tests/service-workers/service-worker/referer.https.html
new file mode 100644
index 0000000..0957e4c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referer.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+    var SCOPE = 'resources/referer-iframe.html';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var host_info = get_host_info();
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, SCOPE);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          var channel = new MessageChannel();
+          t.add_cleanup(function() {
+              frame.remove();
+            });
+
+          var onMsg = new Promise(function(resolve) {
+              channel.port1.onmessage = resolve;
+            });
+
+          frame.contentWindow.postMessage({},
+                                          host_info['HTTPS_ORIGIN'],
+                                          [channel.port2]);
+          return onMsg;
+        })
+      .then(function(e) {
+          assert_equals(e.data.results, 'finish');
+        });
+  }, 'Verify the referer');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html
new file mode 100644
index 0000000..784343e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch() with Referrer Policy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCOPE = 'resources/referrer-policy-iframe.html';
+const SCRIPT = 'resources/fetch-rewrite-worker-referrer-policy.js';
+
+promise_test(async t => {
+    const registration =
+        await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+    await wait_for_state(t, registration.installing, 'activated');
+    t.add_cleanup(() => registration.unregister(),
+                 'Remove registration as a cleanup');
+
+    const full_scope_url = new URL(SCOPE, location.href);
+    const redirect_to = `${full_scope_url.href}?ignore=true`;
+    const frame = await with_iframe(
+        `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+        'header(Referrer-Policy,origin)');
+    assert_equals(frame.contentDocument.referrer,
+                  full_scope_url.origin + '/');
+    t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with referrer-policy (origin) ' +
+   'should only have origin.');
+
+promise_test(async t => {
+    const registration =
+        await service_worker_unregister_and_register(t, SCRIPT, SCOPE, `{type: 'module'}`);
+    await wait_for_state(t, registration.installing, 'activated');
+    t.add_cleanup(() => registration.unregister(),
+                 'Remove registration as a cleanup');
+
+    const full_scope_url = new URL(SCOPE, location.href);
+    const redirect_to = `${full_scope_url.href}?ignore=true`;
+    const frame = await with_iframe(
+        `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+        'header(Referrer-Policy,origin)');
+    assert_equals(frame.contentDocument.referrer,
+                  full_scope_url.origin + '/');
+    t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with a module script with referrer-policy (origin) ' +
+   'should only have origin.');
+
+promise_test(async t => {
+    const registration =
+        await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+    await wait_for_state(t, registration.installing, 'activated');
+    t.add_cleanup(() => registration.unregister(),
+                 'Remove registration as a cleanup');
+
+    const host_info = get_host_info();
+    const frame = await with_iframe(SCOPE);
+    const channel = new MessageChannel();
+    t.add_cleanup(() => frame.remove());
+    const e = await new Promise(resolve => {
+        channel.port1.onmessage = resolve;
+        frame.contentWindow.postMessage(
+            {}, host_info['HTTPS_ORIGIN'], [channel.port2]);
+    });
+    assert_equals(e.data.results, 'finish');
+}, 'Referrer for fetch requests initiated from a service worker with ' +
+   'referrer-policy (origin) should only have origin.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
new file mode 100644
index 0000000..65c60a1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: check referrer of top-level script fetch</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+async function get_toplevel_script_headers(worker) {
+  worker.postMessage("getHeaders");
+  return new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+  });
+}
+
+promise_test(async (t) => {
+  const script = "resources/test-request-headers-worker.py";
+  const scope = "resources/blank.html";
+  const host_info = get_host_info();
+
+  const registration = await service_worker_unregister_and_register(
+    t, script, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+  await wait_for_state(t, registration.installing, "activated");
+
+  const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+  // Check referrer for register().
+  const register_headers = await get_toplevel_script_headers(registration.active);
+  assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+  // Check referrer for update().
+  await registration.update();
+  await wait_for_state(t, registration.installing, "installed");
+  const update_headers = await get_toplevel_script_headers(registration.waiting);
+  assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the top-level script fetch should be the document URL");
+
+promise_test(async (t) => {
+  const script = "resources/test-request-headers-worker.py";
+  const scope = "resources/blank.html";
+  const host_info = get_host_info();
+
+  const registration = await service_worker_unregister_and_register(
+    t, script, scope, {type: 'module'});
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+  await wait_for_state(t, registration.installing, "activated");
+
+  const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+  // Check referrer for register().
+  const register_headers = await get_toplevel_script_headers(registration.active);
+  assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+  // Check referrer for update().
+  await registration.update();
+  await wait_for_state(t, registration.installing, "installed");
+  const update_headers = await get_toplevel_script_headers(registration.waiting);
+  assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the module script fetch should be the document URL");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html
new file mode 100644
index 0000000..9c1b639
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Service Worker: Register() on Closed Window</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+var host_info = get_host_info();
+var frameURL = host_info['HTTPS_ORIGIN'] + base_path() +
+               'resources/register-closed-window-iframe.html';
+
+async_test(function(t) {
+  var frame;
+  with_iframe(frameURL).then(function(f) {
+    frame = f;
+    return new Promise(function(resolve) {
+      window.addEventListener('message', function messageHandler(evt) {
+        window.removeEventListener('message', messageHandler);
+        resolve(evt.data);
+      });
+      frame.contentWindow.postMessage('START', '*');
+    });
+  }).then(function(result) {
+    assert_equals(result, 'OK', 'frame should complete without crashing');
+    frame.remove();
+    t.done();
+  }).catch(unreached_rejection(t));
+}, 'Call register() on ServiceWorkerContainer owned by closed window.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html
new file mode 100644
index 0000000..1d86548
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>register() and scope</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/empty-worker.js';
+    var script_url = new URL(script, location.href);
+    var expected_scope = new URL('./', script_url).href;
+    return service_worker_unregister(t, expected_scope)
+      .then(function() {
+        return navigator.serviceWorker.register('resources/empty-worker.js');
+      }).then(function(registration) {
+        assert_equals(registration.scope, expected_scope,
+                      'The default scope should be URL("./", script_url)');
+        return registration.unregister();
+      }).then(function() {
+        t.done();
+      });
+  }, 'default scope');
+
+promise_test(function(t) {
+    // This script must be different than the 'default scope' test, or else
+    // the scopes will collide.
+    var script = 'resources/empty.js';
+    var script_url = new URL(script, location.href);
+    var expected_scope = new URL('./', script_url).href;
+    return service_worker_unregister(t, expected_scope)
+      .then(function() {
+        return navigator.serviceWorker.register('resources/empty.js',
+                                                { scope: undefined });
+      }).then(function(registration) {
+        assert_equals(registration.scope, expected_scope,
+                      'The default scope should be URL("./", script_url)');
+        return registration.unregister();
+      }).then(function() {
+        t.done();
+      });
+  }, 'undefined scope');
+
+promise_test(function(t) {
+    var script = 'resources/simple-fetch-worker.js';
+    var script_url = new URL(script, location.href);
+    var expected_scope = new URL('./', script_url).href;
+    return service_worker_unregister(t, expected_scope)
+      .then(function() {
+        return navigator.serviceWorker.register('resources/empty.js',
+                                                { scope: null });
+      })
+      .then(
+        function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, registration.scope);
+            });
+
+          assert_unreached('register should fail');
+        },
+        function(error) {
+          assert_equals(error.name, 'SecurityError',
+                        'passing a null scope should be interpreted as ' +
+                        'scope="null" which violates the path restriction');
+          t.done();
+        });
+  }, 'null scope');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
new file mode 100644
index 0000000..6eb00f3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var script1 = normalizeURL('resources/empty-worker.js');
+var script2 = normalizeURL('resources/empty-worker.js?new');
+
+async_test(function(t) {
+    var scope = 'resources/scope/register-new-script-concurrently';
+    var register_promise1;
+    var register_promise2;
+
+    service_worker_unregister(t, scope)
+      .then(function() {
+          register_promise1 = navigator.serviceWorker.register(script1,
+                                                               {scope: scope});
+          register_promise2 = navigator.serviceWorker.register(script2,
+                                                               {scope: scope});
+          return register_promise1;
+        })
+      .then(function(registration) {
+          assert_equals(registration.installing.scriptURL, script1,
+                        'on first register, first script should be installing');
+          assert_equals(registration.waiting, null,
+                        'on first register, waiting should be null');
+          assert_equals(registration.active, null,
+                        'on first register, active should be null');
+          return register_promise2;
+        })
+      .then(function(registration) {
+          assert_equals(
+              registration.installing.scriptURL, script2,
+              'on second register, second script should be installing');
+          // Spec allows racing: the first register may have finished
+          // or the second one could have terminated the installing worker.
+          assert_true(registration.waiting == null ||
+                      registration.waiting.scriptURL == script1,
+                      'on second register, .waiting should be null or the ' +
+                      'first script');
+          assert_true(registration.active == null ||
+                      (registration.waiting == null &&
+                       registration.active.scriptURL == script1),
+                      'on second register, .active should be null or the ' +
+                      'first script');
+          return registration.unregister();
+        })
+      .then(function() {
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register different scripts concurrently');
+
+async_test(function(t) {
+    var scope = 'resources/scope/register-then-register-new-script';
+    var registration;
+
+    service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'on activated, installing should be null');
+          assert_equals(registration.waiting, null,
+                        'on activated, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on activated, the first script should be active');
+          return navigator.serviceWorker.register(script2, {scope:scope});
+        })
+      .then(function(r) {
+          registration = r;
+          assert_equals(registration.installing.scriptURL, script2,
+                        'on second register, the second script should be ' +
+                        'installing');
+          assert_equals(registration.waiting, null,
+                        'on second register, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on second register, the first script should be ' +
+                        'active');
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'on installed, installing should be null');
+          assert_equals(registration.waiting.scriptURL, script2,
+                        'on installed, the second script should be waiting');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on installed, the first script should be active');
+          return registration.unregister();
+        })
+      .then(function() {
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register then register new script URL');
+
+async_test(function(t) {
+    var scope = 'resources/scope/register-then-register-new-script-404';
+    var registration;
+
+    service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'on activated, installing should be null');
+          assert_equals(registration.waiting, null,
+                        'on activated, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on activated, the first script should be active');
+          return navigator.serviceWorker.register('this-will-404.js',
+                                                  {scope:scope});
+        })
+      .then(
+        function() { assert_unreached('register should reject'); },
+        function(error) {
+          assert_equals(registration.installing, null,
+                        'on rejected, installing should be null');
+          assert_equals(registration.waiting, null,
+                        'on rejected, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on rejected, the first script should be active');
+          return registration.unregister();
+        })
+      .then(function() {
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register then register new script URL that 404s');
+
+async_test(function(t) {
+    var scope = 'resources/scope/register-then-register-new-script-reject-install';
+    var reject_script = normalizeURL('resources/reject-install-worker.js');
+    var registration;
+
+    service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'on activated, installing should be null');
+          assert_equals(registration.waiting, null,
+                        'on activated, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on activated, the first script should be active');
+          return navigator.serviceWorker.register(reject_script, {scope:scope});
+        })
+      .then(function(r) {
+          registration = r;
+          assert_equals(registration.installing.scriptURL, reject_script,
+                        'on update, the second script should be installing');
+          assert_equals(registration.waiting, null,
+                        'on update, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on update, the first script should be active');
+          return wait_for_state(t, registration.installing, 'redundant');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'on redundant, installing should be null');
+          assert_equals(registration.waiting, null,
+                        'on redundant, waiting should be null');
+          assert_equals(registration.active.scriptURL, script1,
+                        'on redundant, the first script should be active');
+          return registration.unregister();
+        })
+      .then(function() {
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register then register new script that does not install');
+
+async_test(function(t) {
+    var scope = 'resources/scope/register-new-script-controller';
+    var iframe;
+    var registration;
+
+    service_worker_unregister_and_register(t, script1, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          iframe = frame;
+          return navigator.serviceWorker.register(script2, { scope: scope })
+        })
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          var sw_container = iframe.contentWindow.navigator.serviceWorker;
+          assert_equals(sw_container.controller.scriptURL, script1,
+                        'the old version should control the old doc');
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          var sw_container = frame.contentWindow.navigator.serviceWorker;
+          assert_equals(sw_container.controller.scriptURL, script1,
+                        'the old version should control a new doc');
+          var onactivated_promise = wait_for_state(t,
+                                                   registration.waiting,
+                                                   'activated');
+          frame.remove();
+          iframe.remove();
+          return onactivated_promise;
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          var sw_container = frame.contentWindow.navigator.serviceWorker;
+          assert_equals(sw_container.controller.scriptURL, script2,
+                        'the new version should control a new doc');
+          frame.remove();
+          return registration.unregister();
+        })
+      .then(function() {
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register same-scope new script url effect on controller');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
new file mode 100644
index 0000000..0920b5c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Service Worker: Register wait-forever-in-install-worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var bad_script = 'resources/wait-forever-in-install-worker.js';
+    var good_script = 'resources/empty-worker.js';
+    var scope = 'resources/wait-forever-in-install-worker';
+    var other_scope = 'resources/wait-forever-in-install-worker-other';
+    var registration;
+    var registerPromise;
+
+    return navigator.serviceWorker.register(bad_script, {scope: scope})
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          assert_equals(registration.installing.scriptURL,
+                        normalizeURL(bad_script));
+
+          // This register job should not start until the first
+          // register for the same scope completes.
+          registerPromise =
+            navigator.serviceWorker.register(good_script, {scope: scope});
+
+          // In order to test that the above register does not complete
+          // we will perform a register() on a different scope.  The
+          // assumption here is that the previous register call would
+          // have completed in the same timeframe if it was able to do
+          // so.
+          return navigator.serviceWorker.register(good_script,
+                                                  {scope: other_scope});
+        })
+      .then(function(swr) {
+          return swr.unregister();
+        })
+      .then(function() {
+          assert_equals(registration.installing.scriptURL,
+                        normalizeURL(bad_script));
+          registration.installing.postMessage('STOP_WAITING');
+          return registerPromise;
+        })
+      .then(function(swr) {
+          assert_equals(registration.installing.scriptURL,
+                        normalizeURL(good_script));
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+  }, 'register worker that calls waitUntil with a promise that never ' +
+     'resolves in oninstall');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html
new file mode 100644
index 0000000..759b424
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (basic)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const script = 'resources/registration-worker.js';
+
+promise_test(async (t) => {
+  const scope = 'resources/registration/normal';
+  const registration = await navigator.serviceWorker.register(script, {scope});
+  t.add_cleanup(() => registration.unregister());
+  assert_true(
+    registration instanceof ServiceWorkerRegistration,
+    'Successfully registered.');
+}, 'Registering normal scope');
+
+promise_test(async (t) => {
+  const scope = 'resources/registration/scope-with-fragment#ref';
+  const registration = await navigator.serviceWorker.register(script, {scope});
+  t.add_cleanup(() => registration.unregister());
+  assert_true(
+    registration instanceof ServiceWorkerRegistration,
+    'Successfully registered.');
+  assert_equals(
+    registration.scope,
+    normalizeURL('resources/registration/scope-with-fragment'),
+    'A fragment should be removed from scope');
+}, 'Registering scope with fragment');
+
+promise_test(async (t) => {
+  const scope = 'resources/';
+  const registration = await navigator.serviceWorker.register(script, {scope})
+  t.add_cleanup(() => registration.unregister());
+  assert_true(
+    registration instanceof ServiceWorkerRegistration,
+    'Successfully registered.');
+}, 'Registering same scope as the script directory');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html
new file mode 100644
index 0000000..1af4582
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: registration end-to-end</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var t = async_test('Registration: end-to-end');
+t.step(function() {
+
+    var scope = 'resources/in-scope/';
+    var serviceWorkerStates = [];
+    var lastServiceWorkerState = '';
+    var receivedMessageFromPort = '';
+
+    assert_true(navigator.serviceWorker instanceof ServiceWorkerContainer);
+    assert_equals(typeof navigator.serviceWorker.register, 'function');
+    assert_equals(typeof navigator.serviceWorker.getRegistration, 'function');
+
+    service_worker_unregister_and_register(
+        t, 'resources/end-to-end-worker.js', scope)
+      .then(onRegister)
+      .catch(unreached_rejection(t));
+
+    function sendMessagePort(worker, from) {
+        var messageChannel = new MessageChannel();
+        worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+        return messageChannel.port1;
+    }
+
+    function onRegister(registration) {
+        var sw = registration.installing;
+        serviceWorkerStates.push(sw.state);
+        lastServiceWorkerState = sw.state;
+
+        var sawMessage = new Promise(t.step_func(function(resolve) {
+            sendMessagePort(sw, 'registering doc').onmessage = t.step_func(function (e) {
+                receivedMessageFromPort = e.data;
+                resolve();
+            });
+        }));
+
+        var sawActive = new Promise(t.step_func(function(resolve) {
+            sw.onstatechange = t.step_func(function() {
+                serviceWorkerStates.push(sw.state);
+
+                switch (sw.state) {
+                case 'installed':
+                    assert_equals(lastServiceWorkerState, 'installing');
+                    break;
+                case 'activating':
+                    assert_equals(lastServiceWorkerState, 'installed');
+                    break;
+                case 'activated':
+                    assert_equals(lastServiceWorkerState, 'activating');
+                    break;
+                default:
+                    // We won't see 'redundant' because onstatechange is
+                    // overwritten before calling unregister.
+                    assert_unreached('Unexpected state: ' + sw.state);
+                }
+
+                lastServiceWorkerState = sw.state;
+                if (sw.state === 'activated')
+                    resolve();
+            });
+        }));
+
+        Promise.all([sawMessage, sawActive]).then(t.step_func(function() {
+            assert_array_equals(serviceWorkerStates,
+                                ['installing', 'installed', 'activating', 'activated'],
+                                'Service worker should pass through all states');
+
+            assert_equals(receivedMessageFromPort, 'Ack for: registering doc');
+
+            var sawRedundant = new Promise(t.step_func(function(resolve) {
+                sw.onstatechange = t.step_func(function() {
+                    assert_equals(sw.state, 'redundant');
+                    resolve();
+                });
+            }));
+            registration.unregister();
+            sawRedundant.then(t.step_func(function() {
+                t.done();
+            }));
+        }));
+    }
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html
new file mode 100644
index 0000000..5bcfd66
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var scope = 'resources/in-scope/';
+    return service_worker_unregister_and_register(
+        t, 'resources/events-worker.js', scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+          });
+
+          return onRegister(registration.installing);
+        });
+
+    function sendMessagePort(worker, from) {
+        var messageChannel = new MessageChannel();
+        worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+        return messageChannel.port1;
+    }
+
+    function onRegister(sw) {
+        return new Promise(function(resolve) {
+            sw.onstatechange = function() {
+                if (sw.state === 'activated')
+                    resolve();
+            };
+        }).then(function() {
+            return new Promise(function(resolve) {
+                sendMessagePort(sw, 'registering doc').onmessage = resolve;
+            });
+        }).then(function(e) {
+                assert_array_equals(e.data.events,
+                                    ['install', 'activate'],
+                                   'Worker should see install then activate events');
+        });
+    }
+}, 'Registration: events');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html
new file mode 100644
index 0000000..ae39ddf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Registration for iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Set script url and scope url relative to the iframe's document's url. Assert
+// the implementation parses the urls against the iframe's document's url.
+async_test(function(t) {
+  const url = 'resources/blank.html';
+  const iframe_scope = 'registration-with-valid-scope';
+  const scope = normalizeURL('resources/' + iframe_scope);
+  const iframe_script = 'empty-worker.js';
+  const script = normalizeURL('resources/' + iframe_script);
+  var frame;
+  var registration;
+
+  service_worker_unregister(t, scope)
+    .then(function() { return with_iframe(url); })
+    .then(function(f) {
+        frame = f;
+        return frame.contentWindow.navigator.serviceWorker.register(
+            iframe_script,
+            { scope: iframe_scope });
+      })
+    .then(function(r) {
+        registration = r;
+        return wait_for_state(t, r.installing, 'activated');
+      })
+    .then(function() {
+        assert_equals(registration.scope, scope,
+                      'registration\'s scope must be parsed against the ' +
+                      '"relevant global object"');
+        assert_equals(registration.active.scriptURL, script,
+                      'worker\'s scriptURL must be parsed against the ' +
+                      '"relevant global object"');
+        return registration.unregister();
+      })
+    .then(function() {
+        frame.remove();
+        t.done();
+      })
+    .catch(unreached_rejection(t));
+  }, 'register method should use the "relevant global object" to parse its ' +
+     'scriptURL and scope - normal case');
+
+// Set script url and scope url relative to the parent frame's document's url.
+// Assert the implementation throws a TypeError exception.
+async_test(function(t) {
+  const url = 'resources/blank.html';
+  const iframe_scope = 'resources/registration-with-scope-to-non-existing-url';
+  const scope = normalizeURL('resources/' + iframe_scope);
+  const script = 'resources/empty-worker.js';
+  var frame;
+  var registration;
+
+  service_worker_unregister(t, scope)
+    .then(function() { return with_iframe(url); })
+    .then(function(f) {
+        frame = f;
+        return frame.contentWindow.navigator.serviceWorker.register(
+            script,
+            { scope: iframe_scope });
+      })
+    .then(
+      function() {
+        assert_unreached('register() should reject');
+      },
+      function(e) {
+        assert_equals(e.name, 'TypeError',
+                      'register method with scriptURL and scope parsed to ' +
+                      'nonexistent location should reject with TypeError');
+        frame.remove();
+        t.done();
+      })
+    .catch(unreached_rejection(t));
+  }, 'register method should use the "relevant global object" to parse its ' +
+     'scriptURL and scope - error case');
+
+// Set the scope url to a non-subdirectory of the script url. Assert the
+// implementation throws a SecurityError exception.
+async_test(function(t) {
+  const url = 'resources/blank.html';
+  const scope = 'registration-with-disallowed-scope';
+  const iframe_scope = '../' + scope;
+  const script = 'empty-worker.js';
+  var frame;
+  var registration;
+
+  service_worker_unregister(t, scope)
+    .then(function() { return with_iframe(url); })
+    .then(function(f) {
+        frame = f;
+        return frame.contentWindow.navigator.serviceWorker.register(
+            script,
+            { scope: iframe_scope });
+      })
+    .then(
+      function() {
+        assert_unreached('register() should reject');
+      },
+      function(e) {
+        assert_equals(e.name, 'SecurityError',
+                      'The scope set to a non-subdirectory of the scriptURL ' +
+                      'should reject with SecurityError');
+        frame.remove();
+        t.done();
+      })
+    .catch(unreached_rejection(t));
+  }, 'A scope url should start with the given script url');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html
new file mode 100644
index 0000000..3a21aac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (MIME types)</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-mime-types.js"></script>
+<script>
+registration_tests_mime_types((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html
new file mode 100644
index 0000000..25d758e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<title>Service Worker: Schedule Job algorithm</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests for https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
+// Non-equivalent register jobs should not be coalesced.
+const scope = 'resources/';
+const script1 = 'resources/empty.js';
+const script2 = 'resources/empty.js?change';
+
+async function cleanup() {
+  const registration = await navigator.serviceWorker.getRegistration(scope);
+  if (registration)
+    await registration.unregister();
+}
+
+function absolute_url(url) {
+  return new URL(url, self.location).toString();
+}
+
+// Test that a change to `script` starts a new register job.
+promise_test(async t => {
+  await cleanup();
+  t.add_cleanup(cleanup);
+
+  // Make a registration.
+  const registration = await
+      navigator.serviceWorker.register(script1, {scope});
+
+  // Schedule two more register jobs.
+  navigator.serviceWorker.register(script1, {scope});
+  await navigator.serviceWorker.register(script2, {scope});
+
+  // The jobs should not have been coalesced.
+  const worker = get_newest_worker(registration);
+  assert_equals(worker.scriptURL, absolute_url(script2));
+}, 'different scriptURL');
+
+// Test that a change to `updateViaCache` starts a new register job.
+promise_test(async t => {
+  await cleanup();
+  t.add_cleanup(cleanup);
+
+  // Check defaults.
+  const registration = await
+      navigator.serviceWorker.register(script1, {scope});
+  assert_equals(registration.updateViaCache, 'imports');
+
+  // Schedule two more register jobs.
+  navigator.serviceWorker.register(script1, {scope});
+  await navigator.serviceWorker.register(script1, {scope,
+      updateViaCache: 'none'});
+
+  // The jobs should not have been coalesced.
+  assert_equals(registration.updateViaCache, 'none');
+}, 'different updateViaCache');
+
+// Test that a change to `type` starts a new register job.
+promise_test(async t => {
+  await cleanup();
+  t.add_cleanup(cleanup);
+
+  const scriptForTypeCheck = 'resources/type-check-worker.js';
+  // Check defaults.
+  const registration = await
+      navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+
+  let worker_type = await new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    // The jobs should not have been coalesced. get_newest_worker() helps the
+    // test fail with stable output on browers that incorrectly coalesce
+    // register jobs, since then sometimes registration is not a new worker as
+    // expected.
+    const worker = get_newest_worker(registration);
+    // The argument of postMessage doesn't matter for this case.
+    worker.postMessage('');
+  });
+
+  assert_equals(worker_type, 'classic');
+
+  // Schedule two more register jobs.
+  navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+  await navigator.serviceWorker.register(scriptForTypeCheck, {scope, type: 'module'});
+
+  worker_type = await new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    // The jobs should not have been coalesced. get_newest_worker() helps the
+    // test fail with stable output on browers that incorrectly coalesce
+    // register jobs, since then sometimes registration is not a new worker as
+    // expected.
+    const worker = get_newest_worker(registration);
+    // The argument of postMessage doesn't matter for this case.
+    worker.postMessage('');
+  });
+
+  assert_equals(worker_type, 'module');
+}, 'different type');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html
new file mode 100644
index 0000000..5c75295
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// https://w3c.github.io/ServiceWorker/#path-restriction
+// is applied to top-level scripts in
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+// but not to submodules imported from top-level scripts.
+async function runTest(t, script, scope) {
+  const script_url = new URL(script, location.href);
+  await service_worker_unregister(t, scope);
+  const registration = await
+      navigator.serviceWorker.register(script, {type: 'module'});
+  t.add_cleanup(_ => registration.unregister());
+  const msg = await new Promise(resolve => {
+    registration.installing.postMessage('ping');
+    navigator.serviceWorker.onmessage = resolve;
+  });
+  assert_equals(msg.data, 'pong');
+}
+
+promise_test(async t => {
+    await runTest(t,
+        'resources/scope2/imported-module-script.js',
+        'resources/scope2/');
+  }, 'imported-module-script.js works when used as top-level');
+
+promise_test(async t => {
+    await runTest(t,
+        'resources/scope1/module-worker-importing-scope2.js',
+        'resources/scope1/');
+  }, 'static imports to outside path restriction should be allowed');
+
+promise_test(async t => {
+    await runTest(t,
+       'resources/scope1/module-worker-importing-redirect-to-scope2.js',
+       'resources/scope1/');
+  }, 'static imports redirecting to outside path restriction should be allowed');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html
new file mode 100644
index 0000000..141875f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scope)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-scope.js"></script>
+<script>
+registration_tests_scope((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html
new file mode 100644
index 0000000..9e39a1f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (module script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+    (script, options) => navigator.serviceWorker.register(
+        script,
+        Object.assign({type: 'module'}, options)),
+    'module');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html
new file mode 100644
index 0000000..bda61ad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scriptURL)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script-url.js"></script>
+<script>
+registration_tests_script_url((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html
new file mode 100644
index 0000000..f1e51fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+    (script, options) => navigator.serviceWorker.register(script, options),
+    'classic'
+);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html
new file mode 100644
index 0000000..860c2d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (SecurityError)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-security-error.js"></script>
+<script>
+registration_tests_security_error((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html
new file mode 100644
index 0000000..f7b52d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+promise_test(function(t) {
+    var scope = 'resources/scope/installing-waiting-active-after-registration';
+    var worker_url = 'resources/empty-worker.js';
+    var expected_url = normalizeURL(worker_url);
+    var newest_worker;
+    var registration;
+
+    return service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return r.unregister();
+            });
+          registration = r;
+          newest_worker = registration.installing;
+          assert_equals(registration.installing.scriptURL, expected_url,
+                        'installing before updatefound');
+          assert_equals(registration.waiting, null,
+                        'waiting before updatefound');
+          assert_equals(registration.active, null,
+                        'active before updatefound');
+          return wait_for_update(t, registration);
+        })
+      .then(function() {
+          assert_equals(registration.installing, newest_worker,
+                        'installing after updatefound');
+          assert_equals(registration.waiting, null,
+                        'waiting after updatefound');
+          assert_equals(registration.active, null,
+                        'active after updatefound');
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'installing after installed');
+          assert_equals(registration.waiting, newest_worker,
+                        'waiting after installed');
+          assert_equals(registration.active, null,
+                        'active after installed');
+          return wait_for_state(t, registration.waiting, 'activated');
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'installing after activated');
+          assert_equals(registration.waiting, null,
+                        'waiting after activated');
+          assert_equals(registration.active, newest_worker,
+                        'active after activated');
+          return Promise.all([
+              wait_for_state(t, registration.active, 'redundant'),
+              registration.unregister()
+            ]);
+        })
+      .then(function() {
+          assert_equals(registration.installing, null,
+                        'installing after redundant');
+          assert_equals(registration.waiting, null,
+                        'waiting after redundant');
+          // According to spec, Clear Registration runs Update State which is
+          // immediately followed by setting active to null, which means by the
+          // time the event loop turns and the Promise for statechange is
+          // resolved, this will be gone.
+          assert_equals(registration.active, null,
+                        'active should be null after redundant');
+        });
+  }, 'installing/waiting/active after registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html
new file mode 100644
index 0000000..b2f6bbc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration-updateViaCache</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+  const UPDATE_VIA_CACHE_VALUES = [undefined, 'imports', 'all', 'none'];
+  const SCRIPT_URL = 'resources/update-max-aged-worker.py';
+  const SCOPE = 'resources/blank.html';
+
+  async function cleanup() {
+    const reg = await navigator.serviceWorker.getRegistration(SCOPE);
+    if (!reg) return;
+    if (reg.scope == new URL(SCOPE, location).href) {
+      return reg.unregister();
+    };
+  }
+
+  function getScriptTimes(sw, testName) {
+    return new Promise(resolve => {
+      navigator.serviceWorker.addEventListener('message', function listener(event) {
+        if (event.data.test !== testName) return;
+        navigator.serviceWorker.removeEventListener('message', listener);
+        resolve({
+          mainTime: event.data.mainTime,
+          importTime: event.data.importTime
+        });
+      });
+
+      sw.postMessage('');
+    });
+  }
+
+  // Test creating registrations & triggering an update.
+  for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+    const testName = `register-with-updateViaCache-${updateViaCache}`;
+
+    promise_test(async t => {
+      await cleanup();
+
+      const opts = {scope: SCOPE};
+
+      if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+      const reg = await navigator.serviceWorker.register(
+        `${SCRIPT_URL}?test=${testName}`,
+        opts
+      );
+
+      assert_equals(reg.updateViaCache, updateViaCache || 'imports', "reg.updateViaCache");
+
+      const sw = reg.installing || reg.waiting || reg.active;
+      await wait_for_state(t, sw, 'activated');
+      const values = await getScriptTimes(sw, testName);
+      await reg.update();
+
+      if (updateViaCache == 'all') {
+        assert_equals(reg.installing, null, "No new service worker");
+      }
+      else {
+        const newWorker = reg.installing;
+        assert_true(!!newWorker, "New worker installing");
+        const newValues = await getScriptTimes(newWorker, testName);
+
+        if (!updateViaCache || updateViaCache == 'imports') {
+          assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+          assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+        }
+        else if (updateViaCache == 'none') {
+          assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+          assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+        }
+        else {
+          // We should have handled all of the possible values for updateViaCache.
+          // If this runs, something's gone very wrong.
+          throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+        }
+      }
+
+      await cleanup();
+    }, testName);
+  }
+
+  // Test changing the updateViaCache value of an existing registration.
+  for (const updateViaCache1 of UPDATE_VIA_CACHE_VALUES) {
+    for (const updateViaCache2 of UPDATE_VIA_CACHE_VALUES) {
+      const testName = `register-with-updateViaCache-${updateViaCache1}-then-${updateViaCache2}`;
+
+      promise_test(async t => {
+        await cleanup();
+
+        const fullScriptUrl = `${SCRIPT_URL}?test=${testName}`;
+        let opts = {scope: SCOPE};
+        if (updateViaCache1) opts.updateViaCache = updateViaCache1;
+
+        const reg = await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+        const sw = reg.installing;
+        await wait_for_state(t, sw, 'activated');
+        const values = await getScriptTimes(sw, testName);
+
+        const frame = await with_iframe(SCOPE);
+        const reg_in_frame = await frame.contentWindow.navigator.serviceWorker.getRegistration(normalizeURL(SCOPE));
+        assert_equals(reg_in_frame.updateViaCache, updateViaCache1 || 'imports', "reg_in_frame.updateViaCache");
+
+        opts = {scope: SCOPE};
+        if (updateViaCache2) opts.updateViaCache = updateViaCache2;
+
+        await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+        const expected_updateViaCache = updateViaCache2 || 'imports';
+
+        assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache updated");
+        // If the update happens via the cache, the scripts will come back byte-identical.
+        // We bypass the byte-identical check if the script URL has changed, but not if
+        // only the updateViaCache value has changed.
+        if (updateViaCache2 == 'all') {
+          assert_equals(reg.installing, null, "No new service worker");
+        }
+        // If there's no change to the updateViaCache value, register should be a no-op.
+        // The default value should behave as 'imports'.
+        else if ((updateViaCache1 || 'imports') == (updateViaCache2 || 'imports')) {
+          assert_equals(reg.installing, null, "No new service worker");
+        }
+        else {
+          const newWorker = reg.installing;
+          assert_true(!!newWorker, "New worker installing");
+          const newValues = await getScriptTimes(newWorker, testName);
+
+          if (!updateViaCache2 || updateViaCache2 == 'imports') {
+            assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+            assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+          }
+          else if (updateViaCache2 == 'none') {
+            assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+            assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+          }
+          else {
+            // We should have handled all of the possible values for updateViaCache2.
+            // If this runs, something's gone very wrong.
+            throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+          }
+        }
+
+        // Wait for all registration related tasks on |frame| to complete.
+        await wait_for_activation_on_sample_scope(t, frame.contentWindow);
+        // The updateViaCache change should have been propagated to all
+        // corresponding JS registration objects.
+        assert_equals(reg_in_frame.updateViaCache, expected_updateViaCache, "reg_in_frame.updateViaCache updated");
+        frame.remove();
+
+        await cleanup();
+      }, testName);
+    }
+  }
+
+  // Test accessing updateViaCache of an unregistered registration.
+  for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+    const testName = `access-updateViaCache-after-unregister-${updateViaCache}`;
+
+    promise_test(async t => {
+      await cleanup();
+
+      const opts = {scope: SCOPE};
+
+      if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+      const reg = await navigator.serviceWorker.register(
+        `${SCRIPT_URL}?test=${testName}`,
+        opts
+      );
+
+      const expected_updateViaCache = updateViaCache || 'imports';
+      assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+      await reg.unregister();
+
+      // Keep the original value.
+      assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+      await cleanup();
+    }, testName);
+  }
+
+  promise_test(async t => {
+    await cleanup();
+    t.add_cleanup(cleanup);
+
+    const registration = await navigator.serviceWorker.register(
+        'resources/empty.js',
+        {scope: SCOPE});
+    assert_equals(registration.updateViaCache, 'imports',
+                  'before update attempt');
+
+    const fail = navigator.serviceWorker.register(
+        'resources/malformed-worker.py?parse-error',
+        {scope: SCOPE, updateViaCache: 'none'});
+    await promise_rejects_js(t, TypeError, fail);
+    assert_equals(registration.updateViaCache, 'imports',
+                  'after update attempt');
+  }, 'updateViaCache is not updated if register() rejects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html b/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html
new file mode 100644
index 0000000..8002ad9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Rejection Types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+(function() {
+    var t = async_test('Rejections are DOMExceptions');
+    t.step(function() {
+
+        navigator.serviceWorker.register('http://example.com').then(
+            t.step_func(function() { assert_unreached('Registration should fail'); }),
+            t.step_func(function(reason) {
+                assert_true(reason instanceof DOMException);
+                assert_true(reason instanceof Error);
+                t.done();
+            }));
+    });
+}());
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html b/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html
new file mode 100644
index 0000000..a39cead
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.request passed to onfetch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(t => {
+    var url = 'resources/request-end-to-end-worker.js';
+    var scope = 'resources/blank.html';
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(r => {
+          add_completion_callback(() => { r.unregister(); });
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(() => { return with_iframe(scope); })
+      .then(frame => {
+          add_completion_callback(() => { frame.remove(); });
+
+          var result = JSON.parse(frame.contentDocument.body.textContent);
+          assert_equals(result.url, frame.src, 'request.url');
+          assert_equals(result.method, 'GET', 'request.method');
+          assert_equals(result.referrer, location.href, 'request.referrer');
+          assert_equals(result.mode, 'navigate', 'request.mode');
+          assert_equals(result.request_construct_error, '',
+                        'Constructing a Request with a Request whose mode ' +
+                        'is navigate and non-empty RequestInit must not throw a ' +
+                        'TypeError.')
+          assert_equals(result.credentials, 'include', 'request.credentials');
+          assert_equals(result.redirect, 'manual', 'request.redirect');
+          assert_equals(result.headers['user-agent'], undefined,
+                        'Default User-Agent header should not be passed to ' +
+                        'onfetch event.')
+          assert_equals(result.append_header_error, 'TypeError',
+                        'Appending a new header to the request must throw a ' +
+                        'TypeError.')
+        });
+  }, 'Test FetchEvent.request passed to onfetch');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html
new file mode 100644
index 0000000..5c2b1eb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const {REMOTE_ORIGIN} = get_host_info();
+
+/*
+  This test does the following:
+  - Loads a service worker
+  - Loads an iframe in the service worker's scope
+  - The service worker tries to fetch a resource which is either:
+    - constructed inside the service worker
+    - fetched from a different URL ny the service worker
+    - Streamed from a differend URL by the service worker
+    - Passes through
+  - By default the RT entry should have encoded/decoded body size. except for
+    the case where the response is an opaque pass-through.
+*/
+function test_scenario({tao, mode, name}) {
+    promise_test(async (t) => {
+        const uid = token();
+        const worker_url = `resources/fetch-response.js?uid=${uid}`;
+        const scope = `resources/fetch-response.html?uid=${uid}`;
+        const iframe = document.createElement('iframe');
+        const path = name === "passthrough" ? `element-timing/resources/TAOImage.py?origin=*&tao=${
+            tao === "pass" ? "wildcard" : "none"})}` : name;
+
+        iframe.src = `${scope}&path=${encodeURIComponent(
+            `${mode === "same-origin" ? "" : REMOTE_ORIGIN}/${path}`)}&mode=${mode}`;
+        const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+        t.add_cleanup(() => registration.unregister());
+        t.add_cleanup(() => iframe.remove());
+        await wait_for_state(t, registration.installing, 'activated');
+        const waitForMessage = new Promise(resolve =>
+          window.addEventListener('message', ({data}) => resolve(data)));
+        document.body.appendChild(iframe);
+        const {buffer, entry} = await waitForMessage;
+        const expectPass = name !== "passthrough" || mode !== "no-cors";
+        assert_equals(buffer.byteLength, expectPass ? entry.decodedBodySize : 0);
+        assert_equals(buffer.byteLength, expectPass ? entry.encodedBodySize : 0);
+    }, `Response body size: ${name}, ${mode}, TAO ${tao}`);
+}
+for (const mode of ["cors", "no-cors", "same-origin"]) {
+  for (const tao of ["pass", "fail"])
+    for (const name of ['constructed', 'forward', 'stream', 'passthrough']) {
+      test_scenario({tao, mode, name});
+    }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html
new file mode 100644
index 0000000..2155d7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+
+<body>
+<script>
+function test_sw_resource_timing({ mode }) {
+    promise_test(async t => {
+      const worker_url = `resources/worker-fetching-cross-origin.js?mode=${mode}`;
+      const scope = 'resources/iframe-with-image.html';
+      const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+      await wait_for_state(t, registration.installing, 'activated');
+      const frame = await with_iframe(scope);
+      const frame_performance = frame.contentWindow.performance;
+      // Check that there is one entry for which the timing allow check algorithm failed.
+      const entries = frame_performance.getEntriesByType('resource');
+      assert_equals(entries.length, 1);
+      const entry = entries[0];
+      assert_equals(entry.redirectStart, 0, 'redirectStart should be 0 in cross-origin request.');
+      assert_equals(entry.redirectEnd, 0, 'redirectEnd should be 0 in cross-origin request.');
+      assert_equals(entry.domainLookupStart, entry.fetchStart, 'domainLookupStart should be 0 in cross-origin request.');
+      assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
+      assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
+      assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
+      assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+      assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
+      assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+      frame.remove();
+      await registration.unregister();
+  }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
+}
+
+test_sw_resource_timing({ mode: "cors" });
+test_sw_resource_timing({ mode: "no-cors" });
+
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
new file mode 100644
index 0000000..8d4f0be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test various interactions between fetch, service-workers and resource timing</title>
+<meta charset="utf-8" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="help" href="https://w3c.github.io/resource-timing/" >
+<!--
+    This test checks that the different properties in a PerformanceResourceTimingEntry
+    measure what they are supposed to measure according to spec.
+
+    It is achieved by generating programmatic delays and redirects inside a service worker,
+    and checking how the different metrics respond to the delays and redirects.
+
+    The deltas are not measured precisely, but rather relatively to the delay.
+    The delay needs to be long enough so that it's clear that what's measured is the test's
+    programmatic delay and not arbitrary system delays.
+-->
+</head>
+
+<body>
+<script>
+
+const delay = 200;
+const absolutePath = `${base_path()}/simple.txt`
+function toSequence({before, after, entry}) {
+    /*
+        The order of keys is the same as in this chart:
+        https://w3c.github.io/resource-timing/#attribute-descriptions
+    */
+    const keys = [
+        'startTime',
+        'redirectStart',
+        'redirectEnd',
+        'workerStart',
+        'fetchStart',
+        'connectStart',
+        'requestStart',
+        'responseStart',
+        'responseEnd'
+    ];
+
+    let cursor = before;
+    const step = value => {
+        // A zero/null value, reflect that in the sequence
+        if (!value)
+            return value;
+
+        // Value is the same as before
+        if (value === cursor)
+            return "same";
+
+        // Oops, value is in the wrong place
+        if (value < cursor)
+            return "back";
+
+        // Delta is greater than programmatic delay, this is where the delay is measured.
+        if ((value - cursor) >= delay)
+            return "delay";
+
+        // Some small delta, probably measuring an actual networking stack delay
+        return "tick";
+    }
+
+    const res = keys.map(key => {
+        const value = step(entry[key]);
+        if (entry[key])
+            cursor = entry[key];
+        return [key, value];
+    });
+
+    return Object.fromEntries([...res, ['after', step(after)]]);
+}
+async function testVariant(t, variant) {
+    const worker_url = 'resources/fetch-variants-worker.js';
+    const url = encodeURIComponent(`simple.txt?delay=${delay}&variant=${variant}`);
+    const scope = `resources/iframe-with-fetch-variants.html?url=${url}`;
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+    await wait_for_state(t, registration.installing, 'activated');
+    const frame = await with_iframe(scope);
+    t.add_cleanup(() => frame.remove());
+    const result = await new Promise(resolve => window.addEventListener('message', message => {
+        resolve(message.data);
+    }))
+
+    return toSequence(result);
+}
+
+promise_test(async t => {
+    const result = await testVariant(t, 'redirect');
+    assert_equals(result.redirectStart, 0);
+}, 'Redirects done from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+    const result = await testVariant(t, 'forward');
+    assert_equals(result.connectStart, 'same');
+}, 'Connection info from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+    const result = await testVariant(t, 'forward');
+    assert_not_equals(result.requestStart, 'back');
+}, 'requestStart should never be before fetchStart');
+
+promise_test(async t => {
+    const result = await testVariant(t, 'delay-after-fetch');
+    const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+    assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (after internal fetching) should be accessible through `responseStart`');
+
+promise_test(async t => {
+    const result = await testVariant(t, 'delay-before-fetch');
+    const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+    assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (before internal fetching) should be measured before responseStart in the client ResourceTiming entry');
+</script>
+
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html
new file mode 100644
index 0000000..9808ae5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function resourceUrl(path) {
+  return "https://{{host}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+function crossOriginUrl(path) {
+  return "https://{{hosts[alt][]}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+// Verify existance of a PerformanceEntry and the order between the timings.
+//
+// |options| has these properties:
+// performance: Performance interface to verify existance of the entry.
+// resource: the path to the resource.
+// mode: 'cross-origin' to load the resource from third-party origin.
+// description: the description passed to each assertion.
+// should_no_performance_entry: no entry is expected to be recorded when it's
+//                              true.
+function verify(options) {
+  const url = options.mode === 'cross-origin' ? crossOriginUrl(options.resource)
+    : resourceUrl(options.resource);
+  const entryList = options.performance.getEntriesByName(url, 'resource');
+  if (options.should_no_performance_entry) {
+    // The performance timeline may not have an entry for a resource
+    // which failed to load.
+    assert_equals(entryList.length, 0, options.description);
+    return;
+  }
+
+  assert_equals(entryList.length, 1, options.description);
+  const entry = entryList[0];
+  assert_equals(entry.entryType, 'resource', options.description);
+
+  // workerStart is recorded between startTime and fetchStart.
+  assert_greater_than(entry.workerStart, 0, options.description);
+  assert_greater_than_equal(entry.workerStart, entry.startTime, options.description);
+  assert_less_than_equal(entry.workerStart, entry.fetchStart, options.description);
+
+  if (options.mode === 'cross-origin') {
+    assert_equals(entry.responseStart, 0, options.description);
+    assert_greater_than_equal(entry.responseEnd, entry.fetchStart, options.description);
+  } else {
+    assert_greater_than_equal(entry.responseStart, entry.fetchStart, options.description);
+    assert_greater_than_equal(entry.responseEnd, entry.responseStart, options.description);
+  }
+
+  // responseEnd follows fetchStart.
+  assert_greater_than(entry.responseEnd, entry.fetchStart, options.description);
+  // duration always has some value.
+  assert_greater_than(entry.duration, 0, options.description);
+
+  if (options.resource.indexOf('redirect.py') != -1) {
+    assert_less_than_equal(entry.workerStart, entry.redirectStart,
+      options.description);
+  } else {
+    assert_equals(entry.redirectStart, 0, options.description);
+  }
+}
+
+promise_test(async (t) => {
+  const worker_url = 'resources/resource-timing-worker.js';
+  const scope = 'resources/resource-timing-iframe.sub.html';
+
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  const performance = frame.contentWindow.performance;
+  verify({
+    performance: performance,
+    resource: 'resources/sample.js',
+    mode: 'same-origin',
+    description: 'Generated response',
+  });
+  verify({
+    performance: performance,
+    resource: 'resources/empty.js',
+    mode: 'same-origin',
+    description: 'Network fallback',
+  });
+  verify({
+    performance: performance,
+    resource: 'resources/redirect.py?Redirect=empty.js',
+    mode: 'same-origin',
+    description: 'Redirect',
+  });
+  verify({
+    performance: performance,
+    resource: 'resources/square.png',
+    mode: 'same-origin',
+    description: 'Network fallback image',
+  });
+  // Test that worker start is available on cross-origin no-cors
+  // subresources.
+  verify({
+    performance: performance,
+    resource: 'resources/square.png',
+    mode: 'cross-origin',
+    description: 'Network fallback cross-origin image',
+  });
+
+  // Tests for resouces which failed to load.
+  verify({
+    performance: performance,
+    resource: 'resources/missing.jpg',
+    mode: 'same-origin',
+    description: 'Network fallback load failure',
+  });
+  verify({
+    performance: performance,
+    resource: 'resources/missing.jpg',
+    mode: 'cross-origin',
+    description: 'Network fallback cross-origin load failure',
+  });
+  // Tests for respondWith(fetch()).
+  verify({
+    performance: performance,
+    resource: 'resources/missing.jpg?SWRespondsWithFetch',
+    mode: 'same-origin',
+    description: 'Resource in iframe, nonexistent but responded with fetch to another.',
+  });
+  verify({
+    performance: performance,
+    resource: 'resources/sample.txt?SWFetched',
+    mode: 'same-origin',
+    description: 'Resource fetched as response from missing.jpg?SWRespondsWithFetch.',
+    should_no_performance_entry: true,
+  });
+  // Test for a normal resource that is unaffected by the Service Worker.
+  verify({
+    performance: performance,
+    resource: 'resources/empty-worker.js',
+    mode: 'same-origin',
+    description: 'Resource untouched by the Service Worker.',
+  });
+}, 'Controlled resource loads');
+
+test(() => {
+  const url = resourceUrl('resources/test-helpers.sub.js');
+  const entry = window.performance.getEntriesByName(url, 'resource')[0];
+  assert_equals(entry.workerStart, 0, 'Non-controlled');
+}, 'Non-controlled resource loads');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/404.py b/third_party/web_platform_tests/service-workers/service-worker/resources/404.py
new file mode 100644
index 0000000..1ee4af1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/404.py
@@ -0,0 +1,5 @@
+# iframe does not fire onload event if the response's content-type is not
+# text/plain or text/html so this script exists if you want to test a 404 load
+# in an iframe.
+def main(req, res):
+    return 404, [(b'Content-Type', b'text/plain')], b"Page not found"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
new file mode 100644
index 0000000..1e0c620
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// dynamically add an about:blank iframe
+var f = document.createElement('iframe');
+f.onload = nestedLoaded;
+document.body.appendChild(f);
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return f.contentWindow;
+}
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
new file mode 100644
index 0000000..16f7e7c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here.  We want to
+//       test the case where the initial about:blank document is not
+//       directly accessed before load.
+</script>
+<iframe id="nested" onload="nestedLoaded()"></iframe>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
new file mode 100644
index 0000000..a29ff9d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
@@ -0,0 +1,31 @@
+def main(request, response):
+  if b'nested' in request.GET:
+    return (
+      [(b'Content-Type', b'text/html')],
+      b'failed: nested frame was not intercepted by the service worker'
+    )
+
+  return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here.  We want to
+//       test the case where the initial about:blank document is not
+//       directly accessed before load.
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
new file mode 100644
index 0000000..30fbbbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
@@ -0,0 +1,49 @@
+def main(request, response):
+  if b'nested' in request.GET:
+    return (
+      [(b'Content-Type', b'text/html')],
+      b'failed: nested frame was not intercepted by the service worker'
+    )
+
+  return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true&amp;ping=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return document.getElementById('nested').contentWindow;
+}
+
+// This modifies the nested iframe immediately and does not wait for it to
+// load.  This effectively modifies the global for the initial about:blank
+// document.  Any modifications made here should be preserved after the
+// frame loads because the global should be re-used.
+let win = nested();
+if (win.location.href !== 'about:blank') {
+  parent.postMessage({
+    type: 'NESTED_LOADED',
+    result: 'failed: nested iframe does not have an initial about:blank URL'
+  }, '*');
+} else {
+  win.navigator.serviceWorker.addEventListener('message', evt => {
+    if (evt.data.type === 'PING') {
+      evt.source.postMessage({
+        type: 'PONG',
+        location: win.location.toString()
+      });
+    }
+  });
+  win.navigator.serviceWorker.startMessages();
+}
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
new file mode 100644
index 0000000..04c12a6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
@@ -0,0 +1,32 @@
+def main(request, response):
+  if b'nested' in request.GET:
+    return (
+      [(b'Content-Type', b'text/html')],
+      b'failed: nested frame was not intercepted by the service worker'
+    )
+
+  return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+let popup = window.open('?nested=true');
+popup.onload = nestedLoaded;
+
+addEventListener('unload', evt => {
+  popup.close();
+}, { once: true });
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested popup window.
+function nested() {
+  return popup;
+}
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
new file mode 100644
index 0000000..0122a00
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe id="nested" srcdoc="<div></div>" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here.  We want to
+//       test the case where the initial about:blank document is not
+//       directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
new file mode 100644
index 0000000..8950915
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+  parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="empty.html?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+  return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here.  We want to
+//       test the case where the initial about:blank document is not
+//       directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
new file mode 100644
index 0000000..f43598e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
@@ -0,0 +1,95 @@
+// Helper routine to find a client that matches a particular URL.  Note, we
+// require that Client to be controlled to avoid false matches with other
+// about:blank windows the browser might have.  The initial about:blank should
+// inherit the controller from its parent.
+async function getClientByURL(url) {
+  let list = await clients.matchAll();
+  return list.find(client => client.url === url);
+}
+
+// Helper routine to perform a ping-pong with the given target client.  We
+// expect the Client to respond with its location URL.
+async function pingPong(target) {
+  function waitForPong() {
+    return new Promise(resolve => {
+      self.addEventListener('message', function onMessage(evt) {
+        if (evt.data.type === 'PONG') {
+          resolve(evt.data.location);
+        }
+      });
+    });
+  }
+
+  target.postMessage({ type: 'PING' })
+  return await waitForPong(target);
+}
+
+addEventListener('fetch', async evt => {
+  let url = new URL(evt.request.url);
+  if (!url.searchParams.get('nested')) {
+    return;
+  }
+
+  evt.respondWith(async function() {
+    // Find the initial about:blank document.
+    const client = await getClientByURL('about:blank');
+    if (!client) {
+      return new Response('failure: could not find about:blank client');
+    }
+
+    // If the nested frame is configured to support a ping-pong, then
+    // ping it now to verify its message listener exists.  We also
+    // verify the Client's idea of its own location URL while we are doing
+    // this.
+    if (url.searchParams.get('ping')) {
+      const loc = await pingPong(client);
+      if (loc !== 'about:blank') {
+        return new Response(`failure: got location {$loc}, expected about:blank`);
+      }
+    }
+
+    // Finally, allow the nested frame to complete loading.  We place the
+    // Client ID we found for the initial about:blank in the body.
+    return new Response(client.id);
+  }());
+});
+
+addEventListener('message', evt => {
+  if (evt.data.type !== 'GET_CLIENT_ID') {
+    return;
+  }
+
+  evt.waitUntil(async function() {
+    let url = new URL(evt.data.url);
+
+    // Find the given Client by its URL.
+    let client = await getClientByURL(evt.data.url);
+    if (!client) {
+      evt.source.postMessage({
+        type: 'GET_CLIENT_ID',
+        result: `failure: could not find ${evt.data.url} client`
+      });
+      return;
+    }
+
+    // If the Client supports a ping-pong, then do it now to verify
+    // the message listener exists and its location matches the
+    // Client object.
+    if (url.searchParams.get('ping')) {
+      let loc = await pingPong(client);
+      if (loc !== evt.data.url) {
+        evt.source.postMessage({
+          type: 'GET_CLIENT_ID',
+          result: `failure: got location ${loc}, expected ${evt.data.url}`
+        });
+        return;
+      }
+    }
+
+    // Finally, send the client ID back.
+    evt.source.postMessage({
+      type: 'GET_CLIENT_ID',
+      result: client.id
+    });
+  }());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js
new file mode 100644
index 0000000..189b1c8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js
@@ -0,0 +1 @@
+export default 'hello again!';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js
new file mode 100644
index 0000000..789a89b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js
@@ -0,0 +1 @@
+export default 'hello!';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html b/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
new file mode 100644
index 0000000..1931c77
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
@@ -0,0 +1,20 @@
+import time
+
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Cache-Control', b'max-age=0'),
+               (b'Access-Control-Allow-Origin', b'*')]
+
+    imported_content_type = b''
+    if b'imported' in request.GET:
+        imported_content_type = request.GET[b'imported']
+
+    imported_content = b'default'
+    if imported_content_type == b'time':
+        imported_content = b'%f' % time.time()
+
+    body = b'''
+    // %s
+    ''' % (imported_content)
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py
new file mode 100644
index 0000000..10f3bce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py
@@ -0,0 +1,38 @@
+import time
+
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Cache-Control', b'max-age=0')]
+
+    main_content_type = b''
+    if b'main' in request.GET:
+        main_content_type = request.GET[b'main']
+
+    main_content = b'default'
+    if main_content_type == b'time':
+        main_content = b'%f' % time.time()
+
+    imported_request_path = b''
+    if b'path' in request.GET:
+        imported_request_path = request.GET[b'path']
+
+    imported_request_type = b''
+    if b'imported' in request.GET:
+        imported_request_type = request.GET[b'imported']
+
+    imported_request = b''
+    if imported_request_type == b'time':
+        imported_request = b'?imported=time'
+
+    if b'type' in request.GET and request.GET[b'type'] == b'module':
+        body = b'''
+        // %s
+        import '%sbytecheck-worker-imported-script.py%s';
+        ''' % (main_content, imported_request_path, imported_request)
+    else:
+        body = b'''
+        // %s
+        importScripts('%sbytecheck-worker-imported-script.py%s');
+        ''' % (main_content, imported_request_path, imported_request)
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
new file mode 100644
index 0000000..12ae1a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerScript =
+  `self.onmessage = async (e) => {
+    const url = new URL(e.data, '${baseLocation}').href;
+    const response = await fetch(url);
+    const text = await response.text();
+    self.postMessage(text);
+  };`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+  return new Promise((resolve) => {
+    worker.onmessage = (e) => resolve(e.data);
+    worker.postMessage(url);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
new file mode 100644
index 0000000..2fa15db
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+// An iframe that starts a nested worker. Our parent frame (the test page) calls
+// fetch_in_worker() to ask the nested worker to perform a fetch to see whether
+// it's controlled by a service worker.
+var worker = new Worker('./claim-nested-worker-fetch-parent-worker.js');
+
+function fetch_in_worker(url) {
+  return new Promise((resolve) => {
+    worker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.postMessage(url);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
new file mode 100644
index 0000000..f5ff7c2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
@@ -0,0 +1,12 @@
+try {
+  var worker = new Worker('./claim-worker-fetch-worker.js');
+
+  self.onmessage = (event) => {
+    worker.postMessage(event.data);
+  }
+  worker.onmessage = (event) => {
+    self.postMessage(event.data);
+  };
+} catch (e) {
+  self.postMessage("Fail: " + e.data);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
new file mode 100644
index 0000000..ad865b8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new SharedWorker('./claim-shared-worker-fetch-worker.js');
+
+function fetch_in_shared_worker(url) {
+  return new Promise((resolve) => {
+    worker.port.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.port.postMessage(url);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
new file mode 100644
index 0000000..ddc8bea
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
@@ -0,0 +1,8 @@
+self.onconnect = (event) => {
+  var port = event.ports[0];
+  event.ports[0].onmessage = (evt) => {
+    fetch(evt.data)
+      .then(response => response.text())
+      .then(text => port.postMessage(text));
+  };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
new file mode 100644
index 0000000..4150d7e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+
+function send_result(result) {
+  window.parent.postMessage({message: result},
+                            host_info['HTTPS_ORIGIN']);
+}
+
+function executeTask(params) {
+  // Execute task for each parameter
+  if (params.has('register')) {
+    var worker_url = decodeURIComponent(params.get('register'));
+    var scope = decodeURIComponent(params.get('scope'));
+    navigator.serviceWorker.register(worker_url, {scope: scope})
+      .then(r => send_result('registered'));
+  } else if (params.has('redirected')) {
+    send_result('redirected');
+  } else if (params.has('update')) {
+    var scope = decodeURIComponent(params.get('update'));
+    navigator.serviceWorker.getRegistration(scope)
+      .then(r => r.update())
+      .then(() => send_result('updated'));
+  } else if (params.has('unregister')) {
+    var scope = decodeURIComponent(params.get('unregister'));
+    navigator.serviceWorker.getRegistration(scope)
+      .then(r => r.unregister())
+      .then(succeeded => {
+          if (succeeded) {
+            send_result('unregistered');
+          } else {
+            send_result('failure: unregister');
+          }
+        });
+  } else {
+    send_result('unknown parameter: ' + params.toString());
+  }
+}
+
+var params = new URLSearchParams(location.search.slice(1));
+executeTask(params);
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
new file mode 100644
index 0000000..92c5d15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new Worker('./claim-worker-fetch-worker.js');
+
+function fetch_in_worker(url) {
+  return new Promise((resolve) => {
+    worker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.postMessage(url);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
new file mode 100644
index 0000000..7080181
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
@@ -0,0 +1,5 @@
+self.onmessage = (event) => {
+  fetch(event.data)
+    .then(response => response.text())
+    .then(text => self.postMessage(text));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js
new file mode 100644
index 0000000..1800407
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('message', function(event) {
+    self.clients.claim()
+      .then(function(result) {
+          if (result !== undefined) {
+              event.data.port.postMessage(
+                  'FAIL: claim() should be resolved with undefined');
+              return;
+          }
+          event.data.port.postMessage('PASS');
+        })
+      .catch(function(error) {
+          event.data.port.postMessage('FAIL: exception: ' + error.name);
+        });
+  });
+
+self.addEventListener('fetch', function(event) {
+    if (!/404/.test(event.request.url))
+      event.respondWith(new Response('Intercepted!'));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js
new file mode 100644
index 0000000..36a32b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js
@@ -0,0 +1 @@
+importScripts('./imported-classic-script.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js
new file mode 100644
index 0000000..ec71b34
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js
@@ -0,0 +1,27 @@
+self.onmessage = function(e) {
+  var port = e.data.port;
+  var message = [];
+
+  var promise = Promise.resolve()
+    .then(function() {
+        // 1st matchAll()
+        return self.clients.matchAll().then(function(clients) {
+            clients.forEach(function(client) {
+                message.push(client.id);
+              });
+          });
+      })
+    .then(function() {
+        // 2nd matchAll()
+        return self.clients.matchAll().then(function(clients) {
+            clients.forEach(function(client) {
+                message.push(client.id);
+              });
+          });
+      })
+    .then(function() {
+        // Send an array containing ids of clients from 1st and 2nd matchAll()
+        port.postMessage(message);
+      });
+  e.waitUntil(promise);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html
new file mode 100644
index 0000000..7e186f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script>
+  fetch("clientId")
+    .then(function(response) {
+      return response.text();
+    })
+    .then(function(text) {
+      parent.postMessage({id: text}, "*");
+    });
+</script>
+<body style="background-color: red;"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js
new file mode 100644
index 0000000..6101d5d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js
@@ -0,0 +1,92 @@
+importScripts("worker-testharness.js");
+importScripts("test-helpers.sub.js");
+importScripts("/common/get-host-info.sub.js")
+importScripts("testharness-helpers.js")
+
+setup({ explicit_done: true });
+
+self.onfetch = function(e) {
+  if (e.request.url.indexOf("client-navigate-frame.html") >= 0) {
+    return;
+  }
+  e.respondWith(new Response(e.clientId));
+};
+
+function pass(test, url) {
+  return { result: test,
+           url: url };
+}
+
+function fail(test, reason) {
+  return { result: "FAILED " + test + " " + reason }
+}
+
+self.onmessage = function(e) {
+  var port = e.data.port;
+  var test = e.data.test;
+  var clientId = e.data.clientId;
+  var clientUrl = "";
+  if (test === "test_client_navigate_success") {
+    promise_test(function(t) {
+      this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+      return self.clients.get(clientId)
+                 .then(client => client.navigate("client-navigated-frame.html"))
+                 .then(client => {
+                   clientUrl = client.url;
+                   assert_true(client instanceof WindowClient);
+                 })
+                 .catch(unreached_rejection(t));
+    }, "Return value should be instance of WindowClient");
+    done();
+  } else if (test === "test_client_navigate_cross_origin") {
+    promise_test(function(t) {
+      this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+      var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+      var url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+      return self.clients.get(clientId)
+                 .then(client => client.navigate(url))
+                 .then(client => {
+                   clientUrl = (client && client.url) || "";
+                   assert_equals(client, null,
+                                 'cross-origin navigate resolves with null');
+                 })
+                 .catch(unreached_rejection(t));
+    }, "Navigating to different origin should resolve with null");
+    done();
+  } else if (test === "test_client_navigate_about_blank") {
+    promise_test(function(t) {
+      this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+      return self.clients.get(clientId)
+                 .then(client => promise_rejects_js(t, TypeError, client.navigate("about:blank")))
+                 .catch(unreached_rejection(t));
+    }, "Navigating to about:blank should reject with TypeError");
+    done();
+  } else if (test === "test_client_navigate_mixed_content") {
+    promise_test(function(t) {
+      this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+      var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+      // Insecure URL should fail since the frame is owned by a secure parent
+      // and navigating to http:// would create a mixed-content violation.
+      var url = get_host_info()['HTTP_REMOTE_ORIGIN'] + path;
+      return self.clients.get(clientId)
+                 .then(client => promise_rejects_js(t, TypeError, client.navigate(url)))
+                 .catch(unreached_rejection(t));
+    }, "Navigating to mixed-content iframe should reject with TypeError");
+    done();
+  } else if (test === "test_client_navigate_redirect") {
+    var host_info = get_host_info();
+    var url = new URL(host_info['HTTPS_REMOTE_ORIGIN']).toString() +
+              new URL("client-navigated-frame.html", location).pathname.substring(1);
+    promise_test(function(t) {
+      this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+      return self.clients.get(clientId)
+                 .then(client => client.navigate("redirect.py?Redirect=" + url))
+                 .then(client => {
+                   clientUrl = (client && client.url) || ""
+                   assert_equals(client, null);
+                 })
+                 .catch(unreached_rejection(t));
+    }, "Redirecting to another origin should resolve with null");
+    done();
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html
new file mode 100644
index 0000000..307f7f9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body style="background-color: green;"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
new file mode 100644
index 0000000..00f6ace
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+// Return a URL of a client when it's successful.
+function createAndFetchFromBlobWorker() {
+  const fetchURL = new URL('get-worker-client-url.txt', window.location).href;
+  const workerScript =
+    `self.onmessage = async (e) => {
+      const response = await fetch(e.data.url);
+      const text = await response.text();
+      self.postMessage({"result": text, "expected": self.location.href});
+    };`;
+  const blob = new Blob([workerScript], { type: 'text/javascript' });
+  const blobUrl = URL.createObjectURL(blob);
+
+  const worker = new Worker(blobUrl);
+  return new Promise((resolve, reject) => {
+    worker.onmessage = e => resolve(e.data);
+    worker.onerror = e => reject(e.message);
+    worker.postMessage({"url": fetchURL});
+  });
+}
+
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
new file mode 100644
index 0000000..fd754f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
@@ -0,0 +1,10 @@
+addEventListener('fetch', e => {
+  if (e.request.url.includes('get-worker-client-url')) {
+    e.respondWith((async () => {
+      const clients = await self.clients.matchAll({type: 'worker'});
+      if (clients.length != 1)
+        return new Response('one worker client should exist');
+      return new Response(clients[0].url);
+    })());
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html
new file mode 100644
index 0000000..7468a66
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+  document.addEventListener('freeze', () => {
+      opener.postMessage('frozen', "*");
+  });
+
+  window.onmessage = (e) => {
+    if (e.data == 'freeze') {
+      test_driver.freeze();
+    }
+  };
+  opener.postMessage('loaded', '*');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+  if (e.data.cmd == 'GetClientId') {
+    fetch('clientId')
+        .then(function(response) {
+          return response.text();
+        })
+        .then(function(text) {
+          e.data.port.postMessage({clientId: text});
+        });
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
new file mode 100644
index 0000000..4324e6d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+fetch('clientId')
+  .then(function(response) {
+      return response.text();
+    })
+  .then(function(text) {
+      parent.postMessage({clientId: text}, '*');
+    });
+
+onmessage = function(e) {
+  if (e.data == 'StartWorker') {
+    var w = new Worker('clients-get-client-types-frame-worker.js');
+    w.postMessage({cmd:'GetClientId', port:e.ports[0]}, [e.ports[0]]);
+  }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
new file mode 100644
index 0000000..fadef97
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
@@ -0,0 +1,10 @@
+onconnect = function(e) {
+  var port = e.ports[0];
+  fetch('clientId')
+    .then(function(response) {
+        return response.text();
+      })
+    .then(function(text) {
+        port.postMessage({clientId: text});
+      });
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+  if (e.data.cmd == 'GetClientId') {
+    fetch('clientId')
+        .then(function(response) {
+          return response.text();
+        })
+        .then(function(text) {
+          e.data.port.postMessage({clientId: text});
+        });
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
new file mode 100644
index 0000000..e16bb11
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var scope = 'blank.html?clients-get';
+var script = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(scope)
+  .then(function(reg) {
+      if (reg)
+        return reg.unregister();
+    })
+  .then(function() {
+      return navigator.serviceWorker.register(script, {scope: scope});
+    })
+  .then(function(reg) {
+      registration = reg;
+      worker = reg.installing;
+      return new Promise(function(resolve) {
+          worker.addEventListener('statechange', function() {
+              if (worker.state == 'activated')
+                resolve();
+            });
+        });
+    });
+
+window.addEventListener('message', function(e) {
+  var cross_origin_client_ids = [];
+  cross_origin_client_ids.push(e.data.clientId);
+  wait_for_worker_promise
+    .then(function() {
+        return with_iframe(scope);
+      })
+    .then(function(iframe) {
+        add_completion_callback(function() { iframe.remove(); });
+        navigator.serviceWorker.onmessage = function(e) {
+          registration.unregister();
+          window.parent.postMessage(
+            { type: 'clientId', value: e.data }, host_info['HTTPS_ORIGIN']
+          );
+        };
+        registration.active.postMessage({clientIds: cross_origin_client_ids});
+      });
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html
new file mode 100644
index 0000000..27143d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+
+  fetch("clientId")
+    .then(function(response) {
+        return response.text();
+      })
+    .then(function(text) {
+        parent.postMessage({clientId: text}, "*");
+      });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html
new file mode 100644
index 0000000..6342fe0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'blank.html?clients-get';
+var SCRIPT = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+  .then(function(reg) {
+      if (reg)
+        return reg.unregister();
+    })
+  .then(function() {
+      return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+    })
+  .then(function(reg) {
+      registration = reg;
+      worker = reg.installing;
+      return new Promise(function(resolve) {
+          worker.addEventListener('statechange', function() {
+              if (worker.state == 'activated')
+                resolve();
+            });
+        });
+    });
+
+function send_result(result) {
+  window.parent.postMessage(
+      {result: result},
+      host_info['HTTPS_ORIGIN']);
+}
+
+window.addEventListener("message", on_message, false);
+
+function on_message(e) {
+  if (e.origin != host_info['HTTPS_ORIGIN']) {
+    console.error('invalid origin: ' + e.origin);
+    return;
+  }
+  if (e.data.message == 'get_client_id') {
+    var otherOriginClientId = e.data.clientId;
+    wait_for_worker_promise
+      .then(function() {
+          return with_iframe(SCOPE);
+        })
+      .then(function(iframe) {
+          var channel = new MessageChannel();
+          channel.port1.onmessage = function(e) {
+            navigator.serviceWorker.getRegistration(SCOPE)
+              .then(function(reg) {
+                  reg.unregister();
+                  send_result(e.data);
+                });
+          };
+          iframe.contentWindow.navigator.serviceWorker.controller.postMessage(
+              {port:channel.port2, clientId: otherOriginClientId,
+               message: 'get_other_client_id'}, [channel.port2]);
+        })
+  }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
new file mode 100644
index 0000000..5a46ff9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
@@ -0,0 +1,60 @@
+let savedPort = null;
+let savedResultingClientId = null;
+
+async function getTestingPage() {
+  const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+  for (let c of clientList) {
+    if (c.url.endsWith('clients-get.https.html')) {
+      c.focus();
+      return c;
+    }
+  }
+  return null;
+}
+
+async function destroyResultingClient(testingPage) {
+  const destroyedPromise = new Promise(resolve => {
+    self.addEventListener('message', e => {
+      if (e.data.msg == 'resultingClientDestroyed') {
+        resolve();
+      }
+    }, {once: true});
+  });
+  testingPage.postMessage({ msg: 'destroyResultingClient' });
+  return destroyedPromise;
+}
+
+self.addEventListener('fetch', async (e) => {
+  let { resultingClientId } = e;
+  savedResultingClientId = resultingClientId;
+
+  if (e.request.url.endsWith('simple.html?fail')) {
+    e.waitUntil((async () => {
+      const testingPage = await getTestingPage();
+      await destroyResultingClient(testingPage);
+      testingPage.postMessage({ msg: 'resultingClientDestroyedAck',
+                                resultingDestroyedClientId: savedResultingClientId });
+    })());
+    return;
+  }
+
+  e.respondWith(fetch(e.request));
+});
+
+self.addEventListener('message', (e) => {
+  let { msg, resultingClientId } = e.data;
+  e.waitUntil((async () => {
+    if (msg == 'getIsResultingClientUndefined') {
+      const client = await self.clients.get(resultingClientId);
+      let isUndefined = typeof client == 'undefined';
+      e.source.postMessage({ msg: 'getIsResultingClientUndefined',
+        isResultingClientUndefined: isUndefined });
+      return;
+    }
+    if (msg == 'getResultingClientId') {
+      e.source.postMessage({ msg: 'getResultingClientId',
+                             resultingClientId: savedResultingClientId });
+      return;
+    }
+  })());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js
new file mode 100644
index 0000000..8effa56
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js
@@ -0,0 +1,41 @@
+// This worker is designed to expose information about clients that is only available from Service Worker contexts.
+//
+// In the case of the `onfetch` handler, it provides the `clientId` property of
+// the `event` object. In the case of the `onmessage` handler, it provides the
+// Client instance attributes of the requested clients.
+self.onfetch = function(e) {
+  if (/\/clientId$/.test(e.request.url)) {
+    e.respondWith(new Response(e.clientId));
+    return;
+  }
+};
+
+self.onmessage = function(e) {
+  var client_ids = e.data.clientIds;
+  var message = [];
+
+  e.waitUntil(Promise.all(
+      client_ids.map(function(client_id) {
+          return self.clients.get(client_id);
+        }))
+      .then(function(clients) {
+          // No matching client for a given id or a matched client is off-origin
+          // from the service worker.
+          if (clients.length == 1 && clients[0] == undefined) {
+            e.source.postMessage(clients[0]);
+          } else {
+            clients.forEach(function(client) {
+                if (client instanceof Client) {
+                  message.push([client.visibilityState,
+                                client.focused,
+                                client.url,
+                                client.type,
+                                client.frameType]);
+                } else {
+                  message.push(client);
+                }
+              });
+            e.source.postMessage(message);
+          }
+        }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
new file mode 100644
index 0000000..ee89a0d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script>
+const workerScript = `
+  self.onmessage = (e) => {
+    self.postMessage("Worker is ready.");
+  };
+`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function waitForWorker() {
+  return new Promise(resolve => {
+    worker.onmessage = resolve;
+    worker.postMessage("Ping to worker.");
+  });
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
new file mode 100644
index 0000000..5a3f04d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
@@ -0,0 +1,3 @@
+onmessage = function(e) {
+  postMessage(e.data);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
new file mode 100644
index 0000000..7607b03
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
+<!--
+  Change the page URL using the History API to ensure that ServiceWorkerClient
+  uses the creation URL.
+-->
+<body onload="history.pushState({}, 'title', 'url-modified-via-pushstate.html')">
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
new file mode 100644
index 0000000..1ae72fb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
@@ -0,0 +1,4 @@
+onconnect = function(e) {
+  var port = e.ports[0];
+  port.postMessage('started');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
new file mode 100644
index 0000000..f1559ac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
@@ -0,0 +1,11 @@
+importScripts('test-helpers.sub.js');
+
+var page_url = normalizeURL('../clients-matchall-on-evaluation.https.html');
+
+self.clients.matchAll({includeUncontrolled: true})
+  .then(function(clients) {
+      clients.forEach(function(client) {
+          if (client.url == page_url)
+            client.postMessage('matched');
+        });
+    });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js
new file mode 100644
index 0000000..13e111a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js
@@ -0,0 +1,40 @@
+self.onmessage = function(e) {
+  var port = e.data.port;
+  var options = e.data.options;
+
+  e.waitUntil(self.clients.matchAll(options)
+    .then(function(clients) {
+        var message = [];
+        clients.forEach(function(client) {
+            var frame_type = client.frameType;
+            if (client.url.indexOf('clients-matchall-include-uncontrolled.https.html') > -1 &&
+                client.frameType == 'auxiliary') {
+              // The test tab might be opened using window.open() by the test framework.
+              // In that case, just pretend it's top-level!
+              frame_type = 'top-level';
+            }
+            if (e.data.includeLifecycleState) {
+              message.push({visibilityState: client.visibilityState,
+                            focused: client.focused,
+                            url: client.url,
+                            lifecycleState: client.lifecycleState,
+                            type: client.type,
+                            frameType: frame_type});
+            } else {
+              message.push([client.visibilityState,
+                            client.focused,
+                            client.url,
+                            client.type,
+                            frame_type]);
+            }
+          });
+        // Sort by url
+        if (!e.data.disableSort) {
+          message.sort(function(a, b) { return a[2] > b[2] ? 1 : -1; });
+        }
+        port.postMessage(message);
+      })
+    .catch(e => {
+        port.postMessage('clients.matchAll() rejected: ' + e);
+      }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt
new file mode 100644
index 0000000..1cd89bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt
@@ -0,0 +1 @@
+plaintext
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers
new file mode 100644
index 0000000..f7985fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers
@@ -0,0 +1,3 @@
+Content-Type: text/plain
+Access-Control-Allow-Origin: *
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt
new file mode 100644
index 0000000..ff333bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt
@@ -0,0 +1,2 @@
+this file is served without Access-Control-Allow-Origin headers so it should not
+be readable from cross-origin.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js
new file mode 100644
index 0000000..57e4882
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js
@@ -0,0 +1,22 @@
+const childWorkerScript = `
+  self.onmessage = async (e) => {
+    const response = await fetch(e.data);
+    const text = await response.text();
+    self.postMessage(text);
+  };
+`;
+const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const childWorker = new Worker(blobUrl);
+
+// When a message comes from the parent frame, sends a resource url to the child
+// worker.
+self.onmessage = (e) => {
+  childWorker.postMessage(e.data);
+};
+
+// When a message comes from the child worker, sends a content of fetch() to the
+// parent frame.
+childWorker.onmessage = (e) => {
+  self.postMessage(e.data);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
new file mode 100644
index 0000000..b51c451
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script>
+const workerUrl = '../out-of-scope/sample-synthesized-worker.js?dedicated';
+const worker = new Worker(workerUrl);
+const workerPromise = new Promise(resolve => {
+  worker.onmessage = e => {
+    // `e.data` is 'worker loading intercepted by service worker' when a worker
+    // is intercepted by a service worker.
+    resolve(e.data);
+  }
+  worker.onerror = _ => {
+    resolve('worker loading was not intercepted by service worker');
+  }
+});
+
+function getWorkerPromise() {
+  return workerPromise;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py
new file mode 100644
index 0000000..70ae4b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py
@@ -0,0 +1,16 @@
+# This is a copy of fetch/api/resources/echo-content.py since it's more
+# convenient in this directory due to service worker's path restriction.
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+
+    headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+               (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+               (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+
+               # Avoid any kind of content sniffing on the response.
+               (b"Content-Type", b"text/plain")]
+
+    content = request.body
+
+    return headers, content
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py
new file mode 100644
index 0000000..73e8caf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py
@@ -0,0 +1,24 @@
+def main(request, response):
+    headers = [("Content-Type", "text/javascript")]
+
+    values = []
+    for key in request.cookies:
+        for cookie in request.cookies.get_list(key):
+            values.append('"%s": "%s"' % (key, cookie.value))
+
+    # Update the counter to change the script body for every request to trigger
+    # update of the service worker.
+    key = request.GET['key']
+    counter = request.server.stash.take(key)
+    if counter is None:
+        counter = 0
+    counter += 1
+    request.server.stash.put(key, counter)
+
+    body = """
+// %d
+self.addEventListener('message', e => {
+  e.source.postMessage({%s})
+});""" % (counter, ','.join(values))
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
new file mode 100644
index 0000000..bbbd35f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
@@ -0,0 +1,3 @@
+addEventListener('message', evt => {
+  evt.source.postMessage(evt.data);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
new file mode 100644
index 0000000..ffcdb75
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
@@ -0,0 +1,14 @@
+// This worker intercepts a request for EMBED/OBJECT and responds with a
+// response that indicates that interception occurred. The tests expect
+// that interception does not occur.
+self.addEventListener('fetch', e => {
+    if (e.request.url.indexOf('embedded-content-from-server.html') != -1) {
+      e.respondWith(fetch('embedded-content-from-service-worker.html'));
+      return;
+    }
+
+    if (e.request.url.indexOf('green.png') != -1) {
+      e.respondWith(Promise.reject('network error to show interception occurred'));
+      return;
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..7b8b257
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<embed type="image/png" src="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    if (!navigator.serviceWorker.controller)
+      resolve('FAIL: this iframe is not controlled');
+
+    const elem = document.querySelector('embed');
+    elem.addEventListener('load', e => {
+        resolve('request was not intercepted');
+      });
+    elem.addEventListener('error', e => {
+        resolve('FAIL: request was intercepted');
+      });
+  });
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..3914991
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    report_result = resolve;
+  });
+</script>
+
+<embed src="embedded-content-from-server.html"></embed>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5e86f67
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    report_result = resolve;
+  });
+
+let el = document.createElement('embed');
+el.src = "/common/blank.html";
+el.addEventListener('load', _ => {
+  window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html
new file mode 100644
index 0000000..ff50a9c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was not intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
new file mode 100644
index 0000000..2e2b923
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was intercepted by service worker');
+</script>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js
new file mode 100644
index 0000000..92abac7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js
@@ -0,0 +1,8 @@
+addEventListener('fetch', evt => {
+  if (evt.request.url.endsWith('slow')) {
+    // Performance.now() might be a bit better here, but Date.now() has
+    // better compat in workers right now.
+    let start = Date.now();
+    while(Date.now() - start < 2000);
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js
new file mode 100644
index 0000000..49ceb26
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js
@@ -0,0 +1 @@
+// Do nothing.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html
new file mode 100644
index 0000000..6feb119
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<body>
+hello world
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html b/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html
new file mode 100644
index 0000000..512bd14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<script>
+  // The state variable is used by handle_message to record the time
+  // at which a message was handled. It's updated by the scripts
+  // loaded by the <script> tags at the bottom of the file as well as
+  // by the event listener added here.
+  var state = 'init';
+  addEventListener('DOMContentLoaded', () => state = 'loaded');
+
+  // We expect to get three ping messages from the service worker.
+  const expected = ['init', 'install', 'start'];
+  let promises = {};
+  let resolvers = {};
+  expected.forEach(name => {
+    promises[name] = new Promise(resolve => resolvers[name] = resolve);
+  });
+
+  // Once all messages have been dispatched, the state in which each
+  // of them was dispatched is recorded in the draft. At that point
+  // the draft becomes the final report.
+  var draft = {};
+  var report = Promise.all(Object.values(promises)).then(() => window.draft);
+
+  // This message handler is installed by the 'install' script.
+  function handle_message(event) {
+    const data = event.data.data;
+    draft[data] = state;
+    resolvers[data]();
+  }
+</script>
+
+<!--
+  The controlling service worker will delay the response to these
+  fetch requests until the test instructs it how to reply. Note that
+  the event loop keeps spinning while the parser is blocked.
+-->
+<script src="empty.js?key=install"></script>
+<script src="empty.js?key=start"></script>
+<script src="empty.js?key=finish"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js
new file mode 100644
index 0000000..d45a505
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js
@@ -0,0 +1,7 @@
+onmessage = function(e) {
+  var message = e.data;
+  if (typeof message === 'object' && 'port' in message) {
+    var response = 'Ack for: ' + message.from;
+    message.port.postMessage(response);
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js
new file mode 100644
index 0000000..80a2188
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js
@@ -0,0 +1,12 @@
+var eventsSeen = [];
+
+function handler(event) { eventsSeen.push(event.type); }
+
+['activate', 'install'].forEach(function(type) {
+    self.addEventListener(type, handler);
+  });
+
+onmessage = function(e) {
+  var message = e.data;
+  message.port.postMessage({events: eventsSeen});
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
new file mode 100644
index 0000000..8a975b0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
@@ -0,0 +1,210 @@
+// This worker calls waitUntil() and respondWith() asynchronously and
+// reports back to the test whether they threw.
+//
+// These test cases are confusing. Bear in mind that the event is active
+// (calling waitUntil() is allowed) if:
+// * The pending promise count is not 0, or
+// * The event dispatch flag is set.
+
+// Controlled by 'init'/'done' messages.
+var resolveLockPromise;
+var port;
+
+self.addEventListener('message', function(event) {
+    var waitPromise;
+    var resolveTestPromise;
+
+    switch (event.data.step) {
+      case 'init':
+        event.waitUntil(new Promise((res) => { resolveLockPromise = res; }));
+        port = event.data.port;
+        break;
+      case 'done':
+        resolveLockPromise();
+        break;
+
+      // Throws because waitUntil() is called in a task after event dispatch
+      // finishes.
+      case 'no-current-extension-different-task':
+        async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+        break;
+
+      // OK because waitUntil() is called in a microtask that runs after the
+      // event handler runs, while the event dispatch flag is still set.
+      case 'no-current-extension-different-microtask':
+        async_microtask_waituntil(event).then(reportResultExpecting('OK'));
+        break;
+
+      // OK because the second waitUntil() is called while the first waitUntil()
+      // promise is still pending.
+      case 'current-extension-different-task':
+        event.waitUntil(new Promise((res) => { resolveTestPromise = res; }));
+        async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise);
+        break;
+
+      // OK because all promises involved resolve "immediately", so the second
+      // waitUntil() is called during the microtask checkpoint at the end of
+      // event dispatching, when the event dispatch flag is still set.
+      case 'during-event-dispatch-current-extension-expired-same-microtask-turn':
+        waitPromise = Promise.resolve();
+        event.waitUntil(waitPromise);
+        waitPromise.then(() => { return sync_waituntil(event); })
+          .then(reportResultExpecting('OK'))
+        break;
+
+      // OK for the same reason as above.
+      case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+        waitPromise = Promise.resolve();
+        event.waitUntil(waitPromise);
+        waitPromise.then(() => { return async_microtask_waituntil(event); })
+          .then(reportResultExpecting('OK'))
+        break;
+
+
+      // OK because the pending promise count is decremented in a microtask
+      // queued upon fulfillment of the first waitUntil() promise, so the second
+      // waitUntil() is called while the pending promise count is still
+      // positive.
+      case 'after-event-dispatch-current-extension-expired-same-microtask-turn':
+        waitPromise = makeNewTaskPromise();
+        event.waitUntil(waitPromise);
+        waitPromise.then(() => { return sync_waituntil(event); })
+          .then(reportResultExpecting('OK'))
+        break;
+
+      // Throws because the second waitUntil() is called after the pending
+      // promise count was decremented to 0.
+      case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+        waitPromise = makeNewTaskPromise();
+        event.waitUntil(waitPromise);
+        waitPromise.then(() => { return async_microtask_waituntil(event); })
+          .then(reportResultExpecting('InvalidStateError'))
+        break;
+
+      // Throws because the second waitUntil() is called in a new task, after
+      // first waitUntil() promise settled and the event dispatch flag is unset.
+      case 'current-extension-expired-different-task':
+        event.waitUntil(Promise.resolve());
+        async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+        break;
+
+      case 'script-extendable-event':
+        self.dispatchEvent(new ExtendableEvent('nontrustedevent'));
+        break;
+    }
+
+    event.source.postMessage('ACK');
+  });
+
+self.addEventListener('fetch', function(event) {
+  const path = new URL(event.request.url).pathname;
+  const step = path.substring(path.lastIndexOf('/') + 1);
+  let response;
+  switch (step) {
+    // OK because waitUntil() is called while the respondWith() promise is still
+    // unsettled, so the pending promise count is positive.
+    case 'pending-respondwith-async-waituntil':
+      var resolveFetch;
+      response = new Promise((res) => { resolveFetch = res; });
+      event.respondWith(response);
+      async_task_waituntil(event)
+        .then(reportResultExpecting('OK'))
+        .then(() => { resolveFetch(new Response('OK')); });
+      break;
+
+    // OK because all promises involved resolve "immediately", so waitUntil() is
+    // called during the microtask checkpoint at the end of event dispatching,
+    // when the event dispatch flag is still set.
+    case 'during-event-dispatch-respondwith-microtask-sync-waituntil':
+      response = Promise.resolve(new Response('RESP'));
+      event.respondWith(response);
+      response.then(() => { return sync_waituntil(event); })
+        .then(reportResultExpecting('OK'));
+      break;
+
+    // OK because all promises involved resolve "immediately", so waitUntil() is
+    // called during the microtask checkpoint at the end of event dispatching,
+    // when the event dispatch flag is still set.
+    case 'during-event-dispatch-respondwith-microtask-async-waituntil':
+      response = Promise.resolve(new Response('RESP'));
+      event.respondWith(response);
+      response.then(() => { return async_microtask_waituntil(event); })
+        .then(reportResultExpecting('OK'));
+      break;
+
+    // OK because the pending promise count is decremented in a microtask queued
+    // upon fulfillment of the respondWith() promise, so waitUntil() is called
+    // while the pending promise count is still positive.
+    case 'after-event-dispatch-respondwith-microtask-sync-waituntil':
+      response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+      event.respondWith(response);
+      response.then(() => { return sync_waituntil(event); })
+        .then(reportResultExpecting('OK'));
+      break;
+
+
+    // Throws because waitUntil() is called after the pending promise count was
+    // decremented to 0.
+    case 'after-event-dispatch-respondwith-microtask-async-waituntil':
+      response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+      event.respondWith(response);
+      response.then(() => { return async_microtask_waituntil(event); })
+        .then(reportResultExpecting('InvalidStateError'))
+      break;
+  }
+});
+
+self.addEventListener('nontrustedevent', function(event) {
+    sync_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+  });
+
+function reportResultExpecting(expectedResult) {
+  return function (result) {
+    port.postMessage({result : result, expected: expectedResult});
+    return result;
+  };
+}
+
+function sync_waituntil(event) {
+  return new Promise((res, rej) => {
+    try {
+      event.waitUntil(Promise.resolve());
+      res('OK');
+    } catch (error) {
+      res(error.name);
+    }
+  });
+}
+
+function async_microtask_waituntil(event) {
+  return new Promise((res, rej) => {
+    Promise.resolve().then(() => {
+      try {
+        event.waitUntil(Promise.resolve());
+        res('OK');
+      } catch (error) {
+        res(error.name);
+      }
+    });
+  });
+}
+
+function async_task_waituntil(event) {
+  return new Promise((res, rej) => {
+    setTimeout(() => {
+      try {
+        event.waitUntil(Promise.resolve());
+        res('OK');
+      } catch (error) {
+        res(error.name);
+      }
+    }, 0);
+  });
+}
+
+// Returns a promise that settles in a separate task.
+function makeNewTaskPromise() {
+  return new Promise(resolve => {
+    setTimeout(resolve, 0);
+  });
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js
new file mode 100644
index 0000000..20a9eb0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js
@@ -0,0 +1,87 @@
+var pendingPorts = [];
+var portResolves = [];
+
+onmessage = function(e) {
+  var message = e.data;
+  if ('port' in message) {
+    var resolve = self.portResolves.shift();
+    if (resolve)
+      resolve(message.port);
+    else
+      self.pendingPorts.push(message.port);
+  }
+};
+
+function fulfillPromise() {
+  return new Promise(function(resolve) {
+      // Make sure the oninstall/onactivate callback returns first.
+      Promise.resolve().then(function() {
+          var port = self.pendingPorts.shift();
+          if (port)
+            resolve(port);
+          else
+            self.portResolves.push(resolve);
+        });
+    }).then(function(port) {
+        port.postMessage('SYNC');
+        return new Promise(function(resolve) {
+            port.onmessage = function(e) {
+              if (e.data == 'ACK')
+                resolve();
+            };
+          });
+      });
+}
+
+function rejectPromise() {
+  return new Promise(function(resolve, reject) {
+      // Make sure the oninstall/onactivate callback returns first.
+      Promise.resolve().then(reject);
+    });
+}
+
+function stripScopeName(url) {
+  return url.split('/').slice(-1)[0];
+}
+
+oninstall = function(e) {
+  switch (stripScopeName(self.location.href)) {
+    case 'install-fulfilled':
+      e.waitUntil(fulfillPromise());
+      break;
+    case 'install-rejected':
+      e.waitUntil(rejectPromise());
+      break;
+    case 'install-multiple-fulfilled':
+      e.waitUntil(fulfillPromise());
+      e.waitUntil(fulfillPromise());
+      break;
+    case 'install-reject-precedence':
+      // Three "extend lifetime promises" are needed to verify that the user
+      // agent waits for all promises to settle even in the event of rejection.
+      // The first promise is fulfilled on demand by the client, the second is
+      // immediately scheduled for rejection, and the third is fulfilled on
+      // demand by the client (but only after the first promise has been
+      // fulfilled).
+      //
+      // User agents which simply expose `Promise.all` semantics in this case
+      // (by entering the "redundant state" following the rejection of the
+      // second promise but prior to the fulfillment of the third) can be
+      // identified from the client context.
+      e.waitUntil(fulfillPromise());
+      e.waitUntil(rejectPromise());
+      e.waitUntil(fulfillPromise());
+      break;
+  }
+};
+
+onactivate = function(e) {
+  switch (stripScopeName(self.location.href)) {
+    case 'activate-fulfilled':
+      e.waitUntil(fulfillPromise());
+      break;
+    case 'activate-rejected':
+      e.waitUntil(rejectPromise());
+      break;
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
new file mode 100644
index 0000000..517f289
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
@@ -0,0 +1,5 @@
+importScripts('worker-testharness.js');
+
+this.addEventListener('fetch', function(event) {
+    event.respondWith(new Response('ERROR'));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html
new file mode 100644
index 0000000..ee29680
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html
@@ -0,0 +1,16 @@
+<script>
+// Set authentication info
+window.addEventListener("message", function(evt) {
+    var port = evt.ports[0];
+    document.cookie = 'cookie=' + evt.data.cookie;
+    var xhr = new XMLHttpRequest();
+    xhr.addEventListener('load', function() {
+        port.postMessage({msg: 'LOGIN FINISHED'});
+      }, false);
+    xhr.open('GET',
+             './fetch-access-control.py?Auth',
+             true,
+             evt.data.username, evt.data.password);
+    xhr.send();
+  }, false);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py
new file mode 100644
index 0000000..446af87
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py
@@ -0,0 +1,109 @@
+import json
+import os
+from base64 import decodebytes
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+    headers = []
+    headers.append((b'X-ServiceWorker-ServerHeader', b'SetInTheServer'))
+
+    if b"ACAOrigin" in request.GET:
+        for item in request.GET[b"ACAOrigin"].split(b","):
+            headers.append((b"Access-Control-Allow-Origin", item))
+
+    for suffix in [b"Headers", b"Methods", b"Credentials"]:
+        query = b"ACA%s" % suffix
+        header = b"Access-Control-Allow-%s" % suffix
+        if query in request.GET:
+            headers.append((header, request.GET[query]))
+
+    if b"ACEHeaders" in request.GET:
+        headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+    if (b"Auth" in request.GET and not request.auth.username) or b"AuthFail" in request.GET:
+        status = 401
+        headers.append((b'WWW-Authenticate', b'Basic realm="Restricted"'))
+        body = b'Authentication canceled'
+        return status, headers, body
+
+    if b"PNGIMAGE" in request.GET:
+        headers.append((b"Content-Type", b"image/png"))
+        body = decodebytes(b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1B"
+                           b"AACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/KfgQLABKXJBqMG"
+                           b"jBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=")
+        return headers, body
+
+    if b"VIDEO" in request.GET:
+        headers.append((b"Content-Type", b"video/ogg"))
+        body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read()
+        length = len(body)
+        # If "PartialContent" is specified, the requestor wants to test range
+        # requests. For the initial request, respond with "206 Partial Content"
+        # and don't send the entire content. Then expect subsequent requests to
+        # have a "Range" header with a byte range. Respond with that range.
+        if b"PartialContent" in request.GET:
+          if length < 1:
+            return 500, headers, b"file is too small for range requests"
+          start = 0
+          end = length - 1
+          if b"Range" in request.headers:
+            range_header = request.headers[b"Range"]
+            prefix = b"bytes="
+            split_header = range_header[len(prefix):].split(b"-")
+            # The first request might be "bytes=0-". We want to force a range
+            # request, so just return the first byte.
+            if split_header[0] == b"0" and split_header[1] == b"":
+              end = start
+            # Otherwise, it is a range request. Respect the values sent.
+            if split_header[0] != b"":
+              start = int(split_header[0])
+            if split_header[1] != b"":
+              end = int(split_header[1])
+          else:
+            # The request doesn't have a range. Force a range request by
+            # returning the first byte.
+            end = start
+
+          headers.append((b"Accept-Ranges", b"bytes"))
+          headers.append((b"Content-Length", isomorphic_encode(str(end -start + 1))))
+          headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end, length)))
+          chunk = body[start:(end + 1)]
+          return 206, headers, chunk
+        return headers, body
+
+    username = request.auth.username if request.auth.username else b"undefined"
+    password = request.auth.password if request.auth.username else b"undefined"
+    cookie = request.cookies[b'cookie'].value if b'cookie' in request.cookies else b"undefined"
+
+    files = []
+    for key, values in request.POST.items():
+        assert len(values) == 1
+        value = values[0]
+        if not hasattr(value, u"file"):
+            continue
+        data = value.file.read()
+        files.append({u"key": isomorphic_decode(key),
+                      u"name": value.file.name,
+                      u"type": value.type,
+                      u"error": 0, #TODO,
+                      u"size": len(data),
+                      u"content": data})
+
+    get_data = {isomorphic_decode(key):isomorphic_decode(request.GET[key]) for key, value in request.GET.items()}
+    post_data = {isomorphic_decode(key):isomorphic_decode(request.POST[key]) for key, value in request.POST.items()
+                 if not hasattr(request.POST[key], u"file")}
+    headers_data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+    data = {u"jsonpResult": u"success",
+            u"method": request.method,
+            u"headers": headers_data,
+            u"body": isomorphic_decode(request.body),
+            u"files": files,
+            u"GET": get_data,
+            u"POST": post_data,
+            u"username": isomorphic_decode(username),
+            u"password": isomorphic_decode(password),
+            u"cookie": isomorphic_decode(cookie)}
+
+    return headers, u"report( %s )" % json.dumps(data)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
new file mode 100644
index 0000000..17723dc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', (event) => {
+  url = new URL(event.request.url);
+  if (url.search == '?PNGIMAGE') {
+    localUrl = new URL(url.pathname + url.search, self.location);
+    event.respondWith(fetch(localUrl));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
new file mode 100644
index 0000000..75d766c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
@@ -0,0 +1,70 @@
+<html>
+<title>iframe for fetch canvas tainting test</title>
+<script>
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+// Creates an image/video element with src=|url| and an optional |cross_origin|
+// attibute. Tries to read from the image/video using a canvas element. Returns
+// NOT_TAINTED if it could be read, TAINTED if it could not be read, and
+// LOAD_ERROR if loading the image/video failed.
+function create_test_case_promise(url, cross_origin) {
+  return new Promise(resolve => {
+      if (url.indexOf('PNGIMAGE') != -1) {
+        const img = document.createElement('img');
+        if (cross_origin != '') {
+          img.crossOrigin = cross_origin;
+        }
+        img.onload = function() {
+          try {
+            const canvas = document.createElement('canvas');
+            canvas.width = 100;
+            canvas.height = 100;
+            const context = canvas.getContext('2d');
+            context.drawImage(img, 0, 0);
+            context.getImageData(0, 0, 100, 100);
+            resolve(NOT_TAINTED);
+          } catch (e) {
+            resolve(TAINTED);
+          }
+        };
+        img.onerror = function() {
+          resolve(LOAD_ERROR);
+        }
+        img.src = url;
+        return;
+      }
+
+      if (url.indexOf('VIDEO') != -1) {
+        const video = document.createElement('video');
+        video.autoplay = true;
+        video.muted = true;
+        if (cross_origin != '') {
+          video.crossOrigin = cross_origin;
+        }
+        video.onplay = function() {
+          try {
+            const canvas = document.createElement('canvas');
+            canvas.width = 100;
+            canvas.height = 100;
+            const context = canvas.getContext('2d');
+            context.drawImage(video, 0, 0);
+            context.getImageData(0, 0, 100, 100);
+            resolve(NOT_TAINTED);
+          } catch (e) {
+            resolve(TAINTED);
+          }
+        };
+        video.onerror = function() {
+          resolve(LOAD_ERROR);
+        }
+        video.src = url;
+        return;
+      }
+
+      resolve('unknown resource type');
+  });
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
new file mode 100644
index 0000000..2aada36
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
@@ -0,0 +1,241 @@
+// This is the main driver of the canvas tainting tests.
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+let frame;
+
+// Creates a single promise_test.
+function canvas_taint_test(url, cross_origin, expected_result) {
+  promise_test(t => {
+      return frame.contentWindow.create_test_case_promise(url, cross_origin)
+        .then(result => {
+          assert_equals(result, expected_result);
+        });
+    }, 'url "' + url + '" with crossOrigin "' + cross_origin + '" should be ' +
+           expected_result);
+}
+
+
+// Runs all the tests. The given |params| has these properties:
+// * |resource_path|: the relative path to the (image/video) resource to test.
+// * |cache|: when true, the service worker bounces responses into
+//   Cache Storage and back out before responding with them.
+function do_canvas_tainting_tests(params) {
+  const host_info = get_host_info();
+  let resource_path = params.resource_path;
+  if (params.cache)
+    resource_path += "&cache=true";
+  const resource_url = host_info['HTTPS_ORIGIN'] + resource_path;
+  const remote_resource_url = host_info['HTTPS_REMOTE_ORIGIN'] + resource_path;
+
+  // Set up the service worker and the frame.
+  promise_test(function(t) {
+      const SCOPE = 'resources/fetch-canvas-tainting-iframe.html';
+      const SCRIPT = 'resources/fetch-rewrite-worker.js';
+      const host_info = get_host_info();
+
+      // login_https() is needed because some test cases use credentials.
+      return login_https(t)
+        .then(function() {
+            return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+          })
+        .then(function(registration) {
+            promise_test(() => {
+                if (frame)
+                  frame.remove();
+                return registration.unregister();
+              }, 'restore global state');
+
+            return wait_for_state(t, registration.installing, 'activated');
+          })
+        .then(function() { return with_iframe(SCOPE); })
+        .then(f => {
+            frame = f;
+          });
+    }, 'initialize global state');
+
+  // Reject tests. Add '&reject' so the service worker responds with a rejected promise.
+  // A load error is expected.
+  canvas_taint_test(resource_url + '&reject', '', LOAD_ERROR);
+  canvas_taint_test(resource_url + '&reject', 'anonymous', LOAD_ERROR);
+  canvas_taint_test(resource_url + '&reject', 'use-credentials', LOAD_ERROR);
+
+  // Fallback tests. Add '&ignore' so the service worker does not respond to the fetch
+  // request, and we fall back to network.
+  canvas_taint_test(resource_url + '&ignore', '', NOT_TAINTED);
+  canvas_taint_test(remote_resource_url + '&ignore', '', TAINTED);
+  canvas_taint_test(remote_resource_url + '&ignore', 'anonymous', LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+          '&ignore',
+      'anonymous',
+      NOT_TAINTED);
+  canvas_taint_test(remote_resource_url + '&ignore', 'use-credentials', LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+          '&ignore',
+      'use-credentials',
+      LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+          '&ACACredentials=true&ignore',
+      'use-credentials',
+      NOT_TAINTED);
+
+  // Credential tests (with fallback). Add '&Auth' so the server requires authentication.
+  // Furthermore, add '&ignore' so the service worker falls back to network.
+  canvas_taint_test(resource_url + '&Auth&ignore', '', NOT_TAINTED);
+  canvas_taint_test(remote_resource_url + '&Auth&ignore', '', TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&Auth&ignore', 'anonymous', LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&Auth&ignore',
+      'use-credentials',
+      LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+      '&ignore',
+      'use-credentials',
+      LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+      '&ACACredentials=true&ignore',
+      'use-credentials',
+      NOT_TAINTED);
+
+  // In the following tests, the service worker provides a response.
+  // Add '&url' so the service worker responds with fetch(url).
+  // Add '&mode' to configure the fetch request options.
+
+  // Basic response tests. Set &url to the original url.
+  canvas_taint_test(
+      resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+      '',
+      NOT_TAINTED);
+  canvas_taint_test(
+      resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+      'anonymous',
+      NOT_TAINTED);
+  canvas_taint_test(
+      resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+      'use-credentials',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=same-origin&url=' +
+          encodeURIComponent(resource_url),
+      '',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=same-origin&url=' +
+          encodeURIComponent(resource_url),
+      'anonymous',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=same-origin&url=' +
+          encodeURIComponent(resource_url),
+      'use-credentials',
+      NOT_TAINTED);
+
+  // Opaque response tests. Set &url to the cross-origin URL, and &mode to
+  // 'no-cors' so we expect an opaque response.
+  canvas_taint_test(
+      resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      '',
+      TAINTED);
+  canvas_taint_test(
+      resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      'anonymous',
+      LOAD_ERROR);
+  canvas_taint_test(
+      resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      'use-credentials',
+      LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      '',
+      TAINTED);
+  canvas_taint_test(
+      remote_resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      'anonymous',
+      LOAD_ERROR);
+  canvas_taint_test(
+      remote_resource_url +
+          '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+      'use-credentials',
+      LOAD_ERROR);
+
+  // CORS response tests. Set &url to the cross-origin URL, and &mode
+  // to 'cors' to attempt a CORS request.
+  canvas_taint_test(
+      resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      '',
+      LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+                   // with an Access-Control-Allow-Credentials header.
+  canvas_taint_test(
+      resource_url + '&mode=cors&credentials=same-origin&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      '',
+      NOT_TAINTED);
+  canvas_taint_test(
+      resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'anonymous',
+      NOT_TAINTED);
+  canvas_taint_test(
+      resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'use-credentials',
+      LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+                   // with an Access-Control-Allow-Credentials header.
+  canvas_taint_test(
+      resource_url + '&mode=cors&url=' +
+          encodeURIComponent(
+              remote_resource_url +
+              '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'use-credentials',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      '',
+      LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+                   // with an Access-Control-Allow-Credentials header.
+  canvas_taint_test(
+      remote_resource_url + '&mode=cors&credentials=same-origin&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      '',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'anonymous',
+      NOT_TAINTED);
+  canvas_taint_test(
+      remote_resource_url + '&mode=cors&url=' +
+          encodeURIComponent(remote_resource_url +
+                             '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'use-credentials',
+      LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+                   // with an Access-Control-Allow-Credentials header.
+  canvas_taint_test(
+      remote_resource_url + '&mode=cors&url=' +
+          encodeURIComponent(
+              remote_resource_url +
+              '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+      'use-credentials',
+      NOT_TAINTED);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
new file mode 100644
index 0000000..145952a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', (e) => {
+    e.respondWith(fetch(e.request));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
new file mode 100644
index 0000000..d88c510
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
@@ -0,0 +1,170 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var path = base_path() + 'fetch-access-control.py';
+var host_info = get_host_info();
+var SUCCESS = 'SUCCESS';
+var FAIL = 'FAIL';
+
+function create_test_case_promise(url, with_credentials) {
+  return new Promise(function(resolve) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        if (xhr.status == 200) {
+          resolve(SUCCESS);
+        } else {
+          resolve("STATUS" + xhr.status);
+        }
+      }
+      xhr.onerror = function() {
+        resolve(FAIL);
+      }
+      xhr.responseType = 'text';
+      xhr.withCredentials = with_credentials;
+      xhr.open('GET', url, true);
+      xhr.send();
+    });
+}
+
+window.addEventListener('message', async (evt) => {
+    var port = evt.ports[0];
+    var url = host_info['HTTPS_ORIGIN'] + path;
+    var remote_url = host_info['HTTPS_REMOTE_ORIGIN'] + path;
+    var TEST_CASES = [
+      // Reject tests
+      [url + '?reject', false, FAIL],
+      [url + '?reject', true, FAIL],
+      [remote_url + '?reject', false, FAIL],
+      [remote_url + '?reject', true, FAIL],
+      // Event handler exception tests
+      [url + '?throw', false, SUCCESS],
+      [url + '?throw', true, SUCCESS],
+      [remote_url + '?throw', false, FAIL],
+      [remote_url + '?throw', true, FAIL],
+      // Reject(resolve-null) tests
+      [url + '?resolve-null', false, FAIL],
+      [url + '?resolve-null', true, FAIL],
+      [remote_url + '?resolve-null', false, FAIL],
+      [remote_url + '?resolve-null', true, FAIL],
+      // Fallback tests
+      [url + '?ignore', false, SUCCESS],
+      [url + '?ignore', true, SUCCESS],
+      [remote_url + '?ignore', false, FAIL, true],  // Executed in serial.
+      [remote_url + '?ignore', true, FAIL, true],  // Executed in serial.
+      [
+        remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+        false, SUCCESS
+      ],
+      [
+        remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+        true, FAIL, true  // Executed in serial.
+      ],
+      [
+        remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+        '&ACACredentials=true&ignore',
+        true, SUCCESS
+      ],
+      // Credential test (fallback)
+      [url + '?Auth&ignore', false, SUCCESS],
+      [url + '?Auth&ignore', true, SUCCESS],
+      [remote_url + '?Auth&ignore', false, FAIL],
+      [remote_url + '?Auth&ignore', true, FAIL],
+      [
+        remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+        false, 'STATUS401'
+      ],
+      [
+        remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+        true, FAIL, true  // Executed in serial.
+      ],
+      [
+        remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+        '&ACACredentials=true&ignore',
+        true, SUCCESS
+      ],
+      // Basic response
+      [
+        url + '?mode=same-origin&url=' + encodeURIComponent(url),
+        false, SUCCESS
+      ],
+      [
+        url + '?mode=same-origin&url=' + encodeURIComponent(url),
+        false, SUCCESS
+      ],
+      [
+        remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+        false, SUCCESS
+      ],
+      [
+        remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+        false, SUCCESS
+      ],
+      // Opaque response
+      [
+        url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+        false, FAIL
+      ],
+      [
+        url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+        false, FAIL
+      ],
+      [
+        remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+        false, FAIL
+      ],
+      [
+        remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+        false, FAIL
+      ],
+      // CORS response
+      [
+        url + '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN']),
+        false, SUCCESS
+      ],
+      [
+        url + '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN']),
+        true, FAIL
+      ],
+      [
+        url + '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN'] +
+                           '&ACACredentials=true'),
+        true, SUCCESS
+      ],
+      [
+        remote_url + '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN']),
+        false, SUCCESS
+      ],
+      [
+        remote_url +
+        '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN']),
+        true, FAIL
+      ],
+      [
+        remote_url +
+        '?mode=cors&url=' +
+        encodeURIComponent(remote_url + '?ACAOrigin=' +
+                           host_info['HTTPS_ORIGIN'] +
+                           '&ACACredentials=true'),
+        true, SUCCESS
+      ]
+    ];
+
+    let counter = 0;
+    for (let test of TEST_CASES) {
+      let result = await create_test_case_promise(test[0], test[1]);
+      let testName = 'test ' + (++counter) + ': ' + test[0] + ' with credentials ' + test[1] + ' must be ' + test[2];
+      port.postMessage({testName: testName, result: result === test[2]});
+    }
+    port.postMessage('done');
+  }, false);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html
new file mode 100644
index 0000000..33bf041
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html
@@ -0,0 +1,16 @@
+<script>
+var meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', decodeURIComponent(location.search.substring(1)));
+document.head.appendChild(meta);
+
+function load_image(url) {
+  return new Promise(function(resolve, reject) {
+      var img = document.createElement('img');
+      document.body.appendChild(img);
+      img.onload = resolve;
+      img.onerror = reject;
+      img.src = url;
+    });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
new file mode 100644
index 0000000..5a1c7b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
@@ -0,0 +1 @@
+Content-Security-Policy: img-src https://{{host}}:{{ports[https][0]}}; connect-src 'unsafe-inline' 'self'
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js
new file mode 100644
index 0000000..788252c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+
+function doTest(event)
+{
+    if (!event.request.url.includes("fetch-error-test"))
+        return;
+
+    let counter = 0;
+    const stream = new ReadableStream({ pull: controller => {
+        switch (++counter) {
+        case 1:
+            controller.enqueue(new Uint8Array([1]));
+            return;
+        default:
+            // We asynchronously error the stream so that there is ample time to resolve the fetch promise and call text() on the response.
+            step_timeout(() => controller.error("Sorry"), 50);
+        }
+    }});
+    event.respondWith(new Response(stream));
+}
+
+self.addEventListener("fetch", doTest);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
new file mode 100644
index 0000000..a5a44a5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
@@ -0,0 +1,6 @@
+importScripts('/resources/testharness.js');
+
+promise_test(async () => {
+  await new Promise(handler => { step_timeout(handler, 0); });
+  self.addEventListener('fetch', () => {});
+}, 'fetch event added asynchronously does not throw');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
new file mode 100644
index 0000000..bf8a6d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener('load', function(event) {
+          if (request.status == 200)
+            resolve(request.response);
+          else
+            reject(new Error('fetch_url: ' + request.statusText + " : " + url));
+        });
+      request.addEventListener('error', function(event) {
+          reject(new Error('fetch_url encountered an error: ' + url));
+        });
+      request.addEventListener('abort', function(event) {
+          reject(new Error('fetch_url was aborted: ' + url));
+        });
+      request.open('GET', url);
+      request.send();
+    });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
new file mode 100644
index 0000000..dc3f1a1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
@@ -0,0 +1,66 @@
+// This worker attempts to call respondWith() asynchronously after the
+// fetch event handler finished. It reports back to the test whether
+// an exception was thrown.
+
+// These get reset at the start of a test case.
+let reportResult;
+
+// The test page sends a message to tell us that a new test case is starting.
+// We expect a fetch event after this.
+self.addEventListener('message', (event) => {
+  // Ensure tests run mutually exclusive.
+  if (reportResult) {
+    event.source.postMessage('testAlreadyRunning');
+    return;
+  }
+
+  const resultPromise = new Promise((resolve) => {
+    reportResult = resolve;
+    // Tell the client that everything is initialized and that it's safe to
+    // proceed with the test without relying on the order of events (which some
+    // browsers like Chrome may not guarantee).
+    event.source.postMessage('messageHandlerInitialized');
+  });
+
+  // Keep the worker alive until the test case finishes, and report
+  // back the result to the test page.
+  event.waitUntil(resultPromise.then(result => {
+    reportResult = null;
+    event.source.postMessage(result);
+  }));
+});
+
+// Calls respondWith() and reports back whether an exception occurred.
+function tryRespondWith(event) {
+  try {
+    event.respondWith(new Response());
+    reportResult({didThrow: false});
+  } catch (error) {
+    reportResult({didThrow: true, error: error.name});
+  }
+}
+
+function respondWithInTask(event) {
+  setTimeout(() => {
+    tryRespondWith(event);
+  }, 0);
+}
+
+function respondWithInMicrotask(event) {
+  Promise.resolve().then(() => {
+    tryRespondWith(event);
+  });
+}
+
+self.addEventListener('fetch', function(event) {
+  const path = new URL(event.request.url).pathname;
+  const test = path.substring(path.lastIndexOf('/') + 1);
+
+  // If this is a test case, try respondWith() and report back to the test page
+  // the result.
+  if (test == 'respondWith-in-task') {
+    respondWithInTask(event);
+  } else if (test == 'respondWith-in-microtask') {
+    respondWithInMicrotask(event);
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
new file mode 100644
index 0000000..53ee149
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
@@ -0,0 +1,37 @@
+// This worker reports back the final state of FetchEvent.handled (RESOLVED or
+// REJECTED) to the test.
+
+self.addEventListener('message', function(event) {
+  self.port = event.data.port;
+});
+
+self.addEventListener('fetch', function(event) {
+  try {
+    event.handled.then(() => {
+      self.port.postMessage('RESOLVED');
+    }, () => {
+      self.port.postMessage('REJECTED');
+    });
+  } catch (e) {
+    self.port.postMessage('FAILED');
+    return;
+  }
+
+  const search = new URL(event.request.url).search;
+  switch (search) {
+    case '?respondWith-not-called':
+      break;
+    case '?respondWith-not-called-and-event-canceled':
+      event.preventDefault();
+      break;
+    case '?respondWith-called-and-promise-resolved':
+      event.respondWith(Promise.resolve(new Response('body')));
+      break;
+    case '?respondWith-called-and-promise-resolved-to-invalid-response':
+      event.respondWith(Promise.resolve('invalid response'));
+      break;
+    case '?respondWith-called-and-promise-rejected':
+      event.respondWith(Promise.reject(new Error('respondWith rejected')));
+      break;
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
new file mode 100644
index 0000000..f6c1919
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener('load', function(event) {
+          resolve();
+        });
+      request.addEventListener('error', function(event) {
+          reject();
+        });
+      request.open('GET', url);
+      request.send();
+    });
+}
+
+function make_test(testcase) {
+  var name = testcase.name;
+  return fetch_url(window.location.href + '?' + name)
+    .then(
+      function() {
+          if (testcase.expect_load)
+            return Promise.resolve();
+          return Promise.reject(new Error(
+              name + ': expected network error but loaded'));
+        },
+      function() {
+          if (!testcase.expect_load)
+            return Promise.resolve();
+          return Promise.reject(new Error(
+              name + ': expected to load but got network error'));
+        });
+}
+
+function run_tests() {
+  var tests = [
+    { name: 'prevent-default-and-respond-with', expect_load: true },
+    { name: 'prevent-default', expect_load: false },
+    { name: 'reject', expect_load: false },
+    { name: 'unused-body', expect_load: true },
+    { name: 'used-body', expect_load: false },
+    { name: 'unused-fetched-body', expect_load: true },
+    { name: 'used-fetched-body', expect_load: false },
+    { name: 'throw-exception', expect_load: true },
+  ].map(make_test);
+
+  Promise.all(tests)
+    .then(function() {
+        window.parent.notify_test_done('PASS');
+      })
+    .catch(function(error) {
+        window.parent.notify_test_done('FAIL: ' + error.message);
+      });
+}
+
+if (!navigator.serviceWorker.controller)
+  window.parent.notify_test_done('FAIL: no controller');
+else
+  run_tests();
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
new file mode 100644
index 0000000..5bfe3a0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
@@ -0,0 +1,49 @@
+// Test that multiple fetch handlers do not confuse the implementation.
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+    var testcase = new URL(event.request.url).search;
+    switch (testcase) {
+    case '?reject':
+      event.respondWith(Promise.reject());
+      break;
+    case '?prevent-default':
+      event.preventDefault();
+      break;
+    case '?prevent-default-and-respond-with':
+      event.preventDefault();
+      break;
+    case '?unused-body':
+      event.respondWith(new Response('body'));
+      break;
+    case '?used-body':
+      var res = new Response('body');
+      res.text();
+      event.respondWith(res);
+      break;
+    case '?unused-fetched-body':
+      event.respondWith(fetch('other.html').then(function(res){
+          return res;
+        }));
+      break;
+    case '?used-fetched-body':
+      event.respondWith(fetch('other.html').then(function(res){
+          res.text();
+          return res;
+        }));
+      break;
+    case '?throw-exception':
+      throw('boom');
+      break;
+    }
+  });
+
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+    var testcase = new URL(event.request.url).search;
+    if (testcase == '?prevent-default-and-respond-with')
+      event.respondWith(new Response('responding!'));
+  });
+
+self.addEventListener('fetch', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
new file mode 100644
index 0000000..376bdbe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', () => {
+  // Do nothing.
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
new file mode 100644
index 0000000..0ebd1ca
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener('load', function(event) {
+          resolve();
+        });
+      request.addEventListener('error', function(event) {
+          reject();
+        });
+      request.open('GET', url);
+      request.send();
+    });
+}
+
+function make_test(testcase) {
+  var name = testcase.name;
+  return fetch_url(window.location.href + '?' + name)
+    .then(
+      function() {
+          if (testcase.expect_load)
+            return Promise.resolve();
+          return Promise.reject(new Error(
+              name + ': expected network error but loaded'));
+        },
+      function() {
+          if (!testcase.expect_load)
+            return Promise.resolve();
+          return Promise.reject(new Error(
+              name + ': expected to load but got network error'));
+        });
+}
+
+function run_tests() {
+  var tests = [
+    { name: 'response-object', expect_load: true },
+    { name: 'response-promise-object', expect_load: true },
+    { name: 'other-value', expect_load: false },
+  ].map(make_test);
+
+  Promise.all(tests)
+    .then(function() {
+        window.parent.notify_test_done('PASS');
+      })
+    .catch(function(error) {
+        window.parent.notify_test_done('FAIL: ' + error.message);
+      });
+}
+
+if (!navigator.serviceWorker.controller)
+  window.parent.notify_test_done('FAIL: no controller');
+else
+  run_tests();
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
new file mode 100644
index 0000000..712c4b7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
@@ -0,0 +1,14 @@
+self.addEventListener('fetch', function(event) {
+    var testcase = new URL(event.request.url).search;
+    switch (testcase) {
+    case '?response-object':
+      event.respondWith(new Response('body'));
+      break;
+    case '?response-promise-object':
+      event.respondWith(Promise.resolve(new Response('body')));
+      break;
+    case '?other-value':
+      event.respondWith(new Object());
+      break;
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
new file mode 100644
index 0000000..d3ba8a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+    if (!event.request.url.match(/body-in-chunk$/))
+        return;
+    event.respondWith(fetch("../../../fetch/api/resources/trickle.py?count=4&delay=50"));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
new file mode 100644
index 0000000..ff24aed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
@@ -0,0 +1,45 @@
+'use strict';
+
+addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+  const type = url.searchParams.get('type');
+
+  if (!type) return;
+
+  if (type === 'string') {
+    event.respondWith(new Response('PASS'));
+  }
+  else if (type === 'blob') {
+    event.respondWith(
+      new Response(new Blob(['PASS']))
+    );
+  }
+  else if (type === 'buffer-view') {
+    const encoder = new TextEncoder();
+    event.respondWith(
+      new Response(encoder.encode('PASS'))
+    );
+  }
+  else if (type === 'buffer') {
+    const encoder = new TextEncoder();
+    event.respondWith(
+      new Response(encoder.encode('PASS').buffer)
+    );
+  }
+  else if (type === 'form-data') {
+    const body = new FormData();
+    body.set('result', 'PASS');
+    event.respondWith(
+      new Response(body)
+    );
+  }
+  else if (type === 'search-params') {
+    const body = new URLSearchParams();
+    body.set('result', 'PASS');
+    event.respondWith(
+      new Response(body, {
+        headers: { 'Content-Type': 'text/plain' }
+      })
+    );
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
new file mode 100644
index 0000000..b7307f2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
@@ -0,0 +1,28 @@
+let waitUntilResolve;
+
+let bodyController;
+
+self.addEventListener('message', evt => {
+  if (evt.data === 'done') {
+    bodyController.close();
+    waitUntilResolve();
+  }
+});
+
+self.addEventListener('fetch', evt => {
+  if (!evt.request.url.includes('partial-stream.txt')) {
+    return;
+  }
+
+  evt.waitUntil(new Promise(resolve => waitUntilResolve = resolve));
+
+  let body = new ReadableStream({
+    start: controller => {
+      let encoder = new TextEncoder();
+      controller.enqueue(encoder.encode('partial-stream-content'));
+      bodyController = controller;
+    },
+  });
+
+  evt.respondWith(new Response(body));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
new file mode 100644
index 0000000..f954e3a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
@@ -0,0 +1,40 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+    if (!event.request.url.match(/body-stream$/))
+        return;
+
+    var counter = 0;
+    const encoder = new TextEncoder();
+    const stream = new ReadableStream({ pull: controller => {
+        switch (++counter) {
+        case 1:
+            controller.enqueue(encoder.encode(''));
+            return;
+        case 2:
+            controller.enqueue(encoder.encode('chunk #1'));
+            return;
+        case 3:
+            controller.enqueue(encoder.encode(' '));
+            return;
+        case 4:
+            controller.enqueue(encoder.encode('chunk #2'));
+            return;
+        case 5:
+            controller.enqueue(encoder.encode(' '));
+            return;
+        case 6:
+            controller.enqueue(encoder.encode('chunk #3'));
+            return;
+        case 7:
+            controller.enqueue(encoder.encode(' '));
+            return;
+        case 8:
+            controller.enqueue(encoder.encode('chunk #4'));
+            return;
+        default:
+            controller.close();
+        }
+    }});
+    event.respondWith(new Response(stream));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
new file mode 100644
index 0000000..e54cb6d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
@@ -0,0 +1,75 @@
+'use strict';
+importScripts("/resources/testharness.js");
+
+const map = new Map();
+
+self.addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+  if (!url.searchParams.has('stream')) return;
+
+  if (url.searchParams.has('observe-cancel')) {
+    const id = url.searchParams.get('id');
+    if (id === undefined) {
+      event.respondWith(new Error('error'));
+      return;
+    }
+    event.waitUntil(new Promise(resolve => {
+      map.set(id, {label: 'pending', resolve});
+    }));
+
+    const stream = new ReadableStream({
+      cancel() {
+        map.get(id).label = 'cancelled';
+      }
+    });
+    event.respondWith(new Response(stream));
+    return;
+  }
+
+  if (url.searchParams.has('query-cancel')) {
+    const id = url.searchParams.get('id');
+    if (id === undefined) {
+      event.respondWith(new Error('error'));
+      return;
+    }
+    const entry = map.get(id);
+    if (entry === undefined) {
+      event.respondWith(new Error('not found'));
+      return;
+    }
+    map.delete(id);
+    entry.resolve();
+    event.respondWith(new Response(entry.label));
+    return;
+  }
+
+  if (url.searchParams.has('use-fetch-stream')) {
+    event.respondWith(async function() {
+      const response = await fetch('pass.txt');
+      return new Response(response.body);
+    }());
+    return;
+  }
+
+  const delayEnqueue = url.searchParams.has('delay');
+
+  const stream = new ReadableStream({
+    start(controller) {
+      const encoder = new TextEncoder();
+
+      const populate = () => {
+        controller.enqueue(encoder.encode('PASS'));
+        controller.close();
+      }
+
+      if (delayEnqueue) {
+        step_timeout(populate, 16);
+      }
+      else {
+        populate();
+      }
+    }
+  });
+
+  event.respondWith(new Response(stream));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
new file mode 100644
index 0000000..d15454d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respond-with-response-body-with-invalid-chunk</title>
+<body></body>
+<script>
+'use strict';
+
+parent.set_fetch_promise(fetch('body-stream-with-invalid-chunk').then(resp => {
+    const reader = resp.body.getReader();
+    const reader_promise = reader.read();
+    parent.set_reader_promise(reader_promise);
+    // Suppress our expected error.
+    return reader_promise.catch(() => {});
+  }));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
new file mode 100644
index 0000000..0254e24
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
@@ -0,0 +1,12 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+    if (!event.request.url.match(/body-stream-with-invalid-chunk$/))
+      return;
+    const stream = new ReadableStream({start: controller => {
+        // The argument is intentionally a string, not a Uint8Array.
+        controller.enqueue('hello');
+      }});
+    const headers = { 'x-content-type-options': 'nosniff' };
+    event.respondWith(new Response(stream, { headers }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
new file mode 100644
index 0000000..18da049
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
@@ -0,0 +1,15 @@
+var result = null;
+
+self.addEventListener('message', function(event) {
+    event.data.port.postMessage(result);
+  });
+
+self.addEventListener('fetch', function(event) {
+    if (!result)
+      result = 'PASS';
+    event.respondWith(new Response());
+  });
+
+self.addEventListener('fetch', function(event) {
+    result = 'FAIL: fetch event propagated';
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js
new file mode 100644
index 0000000..813f79d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js
@@ -0,0 +1,224 @@
+function handleHeaders(event) {
+  const headers = Array.from(event.request.headers);
+  event.respondWith(new Response(JSON.stringify(headers)));
+}
+
+function handleString(event) {
+  event.respondWith(new Response('Test string'));
+}
+
+function handleBlob(event) {
+  event.respondWith(new Response(new Blob(['Test blob'])));
+}
+
+function handleReferrer(event) {
+  event.respondWith(new Response(new Blob(
+    ['Referrer: ' + event.request.referrer])));
+}
+
+function handleReferrerPolicy(event) {
+  event.respondWith(new Response(new Blob(
+    ['ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleReferrerFull(event) {
+  event.respondWith(new Response(new Blob(
+    ['Referrer: ' + event.request.referrer + '\n' +
+     'ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleClientId(event) {
+  var body;
+  if (event.clientId !== "") {
+    body = 'Client ID Found: ' + event.clientId;
+  } else {
+    body = 'Client ID Not Found';
+  }
+  event.respondWith(new Response(body));
+}
+
+function handleResultingClientId(event) {
+  var body;
+  if (event.resultingClientId !== "") {
+    body = 'Resulting Client ID Found: ' + event.resultingClientId;
+  } else {
+    body = 'Resulting Client ID Not Found';
+  }
+  event.respondWith(new Response(body));
+}
+
+function handleNullBody(event) {
+  event.respondWith(new Response());
+}
+
+function handleFetch(event) {
+  event.respondWith(fetch('other.html'));
+}
+
+function handleFormPost(event) {
+  event.respondWith(new Promise(function(resolve) {
+      event.request.text()
+        .then(function(result) {
+            resolve(new Response(event.request.method + ':' +
+                                 event.request.headers.get('Content-Type') + ':' +
+                                 result));
+          });
+    }));
+}
+
+function handleMultipleRespondWith(event) {
+  var logForMultipleRespondWith = '';
+  for (var i = 0; i < 3; ++i) {
+    logForMultipleRespondWith += '(' + i + ')';
+    try {
+      event.respondWith(new Promise(function(resolve) {
+        setTimeout(function() {
+          resolve(new Response(logForMultipleRespondWith));
+        }, 0);
+      }));
+    } catch (e) {
+      logForMultipleRespondWith += '[' + e.name + ']';
+    }
+  }
+}
+
+var lastResponseForUsedCheck = undefined;
+
+function handleUsedCheck(event) {
+  if (!lastResponseForUsedCheck) {
+    event.respondWith(fetch('other.html').then(function(response) {
+        lastResponseForUsedCheck = response;
+        return response;
+      }));
+  } else {
+    event.respondWith(new Response(
+        'bodyUsed: ' + lastResponseForUsedCheck.bodyUsed));
+  }
+}
+function handleFragmentCheck(event) {
+  var body;
+  if (event.request.url.indexOf('#') === -1) {
+    body = 'Fragment Not Found';
+  } else {
+    body = 'Fragment Found :' +
+           event.request.url.substring(event.request.url.indexOf('#'));
+  }
+  event.respondWith(new Response(body));
+}
+function handleCache(event) {
+  event.respondWith(new Response(event.request.cache));
+}
+function handleEventSource(event) {
+  if (event.request.mode === 'navigate') {
+    return;
+  }
+  var data = {
+    mode: event.request.mode,
+    cache: event.request.cache,
+    credentials: event.request.credentials
+  };
+  var body = 'data:' + JSON.stringify(data) + '\n\n';
+  event.respondWith(new Response(body, {
+      headers: { 'Content-Type': 'text/event-stream' }
+    }
+  ));
+}
+
+function handleIntegrity(event) {
+  event.respondWith(new Response(event.request.integrity));
+}
+
+function handleRequestBody(event) {
+  event.respondWith(event.request.text().then(text => {
+    return new Response(text);
+  }));
+}
+
+function handleKeepalive(event) {
+  event.respondWith(new Response(event.request.keepalive));
+}
+
+function handleIsReloadNavigation(event) {
+  const request = event.request;
+  const body =
+    `method = ${request.method}, ` +
+    `isReloadNavigation = ${request.isReloadNavigation}`;
+  event.respondWith(new Response(body));
+}
+
+function handleIsHistoryNavigation(event) {
+  const request = event.request;
+  const body =
+    `method = ${request.method}, ` +
+    `isHistoryNavigation = ${request.isHistoryNavigation}`;
+  event.respondWith(new Response(body));
+}
+
+function handleUseAndIgnore(event) {
+  const request = event.request;
+  request.text();
+  return;
+}
+
+function handleCloneAndIgnore(event) {
+  const request = event.request;
+  request.clone().text();
+  return;
+}
+
+var handle_status_count = 0;
+function handleStatus(event) {
+  handle_status_count++;
+  event.respondWith(async function() {
+    const res = await fetch(event.request);
+    const text = await res.text();
+    return new Response(`${text}. Request was sent ${handle_status_count} times.`,
+      {"status": new URL(event.request.url).searchParams.get("status")});
+  }());
+}
+
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    var handlers = [
+      { pattern: '?headers', fn: handleHeaders },
+      { pattern: '?string', fn: handleString },
+      { pattern: '?blob', fn: handleBlob },
+      { pattern: '?referrerFull', fn: handleReferrerFull },
+      { pattern: '?referrerPolicy', fn: handleReferrerPolicy },
+      { pattern: '?referrer', fn: handleReferrer },
+      { pattern: '?clientId', fn: handleClientId },
+      { pattern: '?resultingClientId', fn: handleResultingClientId },
+      { pattern: '?ignore', fn: function() {} },
+      { pattern: '?null', fn: handleNullBody },
+      { pattern: '?fetch', fn: handleFetch },
+      { pattern: '?form-post', fn: handleFormPost },
+      { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith },
+      { pattern: '?used-check', fn: handleUsedCheck },
+      { pattern: '?fragment-check', fn: handleFragmentCheck },
+      { pattern: '?cache', fn: handleCache },
+      { pattern: '?eventsource', fn: handleEventSource },
+      { pattern: '?integrity', fn: handleIntegrity },
+      { pattern: '?request-body', fn: handleRequestBody },
+      { pattern: '?keepalive', fn: handleKeepalive },
+      { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation },
+      { pattern: '?isHistoryNavigation', fn: handleIsHistoryNavigation },
+      { pattern: '?use-and-ignore', fn: handleUseAndIgnore },
+      { pattern: '?clone-and-ignore', fn: handleCloneAndIgnore },
+      { pattern: '?status', fn: handleStatus },
+    ];
+
+    var handler = null;
+    for (var i = 0; i < handlers.length; ++i) {
+      if (url.indexOf(handlers[i].pattern) != -1) {
+        handler = handlers[i];
+        break;
+      }
+    }
+
+    if (handler) {
+      handler.fn(event);
+    } else {
+      event.respondWith(new Response(new Blob(
+        ['Service Worker got an unexpected request: ' + url])));
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
new file mode 100644
index 0000000..5903bab
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
@@ -0,0 +1,48 @@
+skipWaiting();
+
+addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+
+  if (url.origin != location.origin) return;
+
+  if (url.pathname.endsWith('/sample.txt')) {
+    event.respondWith(new Response('intercepted'));
+    return;
+  }
+
+  if (url.pathname.endsWith('/sample.txt-inner-fetch')) {
+    event.respondWith(fetch('sample.txt'));
+    return;
+  }
+
+  if (url.pathname.endsWith('/sample.txt-inner-cache')) {
+    event.respondWith(
+      caches.open('test-inner-cache').then(cache =>
+        cache.add('sample.txt').then(() => cache.match('sample.txt'))
+      )
+    );
+    return;
+  }
+
+  if (url.pathname.endsWith('/show-notification')) {
+    // Copy the currect search string onto the icon url
+    const iconURL = new URL('notification_icon.py', location);
+    iconURL.search = url.search;
+
+    event.respondWith(
+      registration.showNotification('test', {
+        icon: iconURL
+      }).then(() => registration.getNotifications()).then(notifications => {
+        for (const n of notifications) n.close();
+        return new Response('done');
+      })
+    );
+    return;
+  }
+
+  if (url.pathname.endsWith('/notification_icon.py')) {
+    new BroadcastChannel('icon-request').postMessage('yay');
+    event.respondWith(new Response('done'));
+    return;
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
new file mode 100644
index 0000000..0d9ab6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
@@ -0,0 +1,66 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+  var host_info = get_host_info();
+  var uri = document.location + '?check-ua-header';
+
+  var headers = new Headers();
+  headers.set('User-Agent', 'custom_ua');
+
+  // Check the custom UA case
+  fetch(uri, { headers: headers }).then(function(response) {
+    return response.text();
+  }).then(function(text) {
+    if (text == 'custom_ua') {
+      parent.postMessage('PASS', '*');
+    } else {
+      parent.postMessage('withUA FAIL - expected "custom_ua", got "' + text + '"', '*');
+    }
+  }).catch(function(err) {
+    parent.postMessage('withUA FAIL - unexpected error: ' + err, '*');
+  });
+
+  // Check the default UA case
+  fetch(uri, {}).then(function(response) {
+    return response.text();
+  }).then(function(text) {
+    if (text == 'NO_UA') {
+      parent.postMessage('PASS', '*');
+    } else {
+      parent.postMessage('noUA FAIL - expected "NO_UA", got "' + text + '"', '*');
+    }
+  }).catch(function(err) {
+    parent.postMessage('noUA FAIL - unexpected error: ' + err, '*');
+  });
+
+  var uri = document.location + '?check-accept-header';
+  var headers = new Headers();
+  headers.set('Accept', 'hmm');
+
+  // Check for custom accept header
+  fetch(uri, { headers: headers }).then(function(response) {
+    return response.text();
+  }).then(function(text) {
+    if (text === headers.get('Accept')) {
+      parent.postMessage('PASS', '*');
+    } else {
+      parent.postMessage('custom accept FAIL - expected ' + headers.get('Accept') +
+                         ' got "' + text + '"', '*');
+    }
+  }).catch(function(err) {
+    parent.postMessage('custom accept FAIL - unexpected error: ' + err, '*');
+  });
+
+  // Check for default accept header
+  fetch(uri).then(function(response) {
+    return response.text();
+  }).then(function(text) {
+    if (text === '*/*') {
+      parent.postMessage('PASS', '*');
+    } else {
+      parent.postMessage('accept FAIL - expected */* got "' + text + '"', '*');
+    }
+  }).catch(function(err) {
+    parent.postMessage('accept FAIL - unexpected error: ' + err, '*');
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
new file mode 100644
index 0000000..64a634e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
@@ -0,0 +1,71 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    test2();
+  };
+  img.onerror = function() {
+    results += 'FAIL(1)';
+    test2();
+  };
+  img.src = './sample?url=' +
+            encodeURIComponent(host_info['HTTPS_ORIGIN'] + image_path);
+}
+
+function test2() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    test3();
+  };
+  img.onerror = function() {
+    results += 'FAIL(2)';
+    test3();
+  };
+  img.src = './sample?mode=no-cors&url=' +
+            encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + image_path);
+}
+
+function test3() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    results += 'FAIL(3)';
+    test4();
+  };
+  img.onerror = function() {
+    test4();
+  };
+  img.src = './sample?mode=no-cors&url=' +
+            encodeURIComponent(host_info['HTTP_ORIGIN'] + image_path);
+}
+
+function test4() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    results += 'FAIL(4)';
+    finish();
+  };
+  img.onerror = function() {
+    finish();
+  };
+  img.src = './sample?mode=no-cors&url=' +
+            encodeURIComponent(host_info['HTTP_REMOTE_ORIGIN'] + image_path);
+}
+
+function finish() {
+  results += 'finish';
+  window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
new file mode 100644
index 0000000..be0b5c8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
@@ -0,0 +1,80 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    test2();
+  };
+  img.onerror = function() {
+    results += 'FAIL(1)';
+    test2();
+  };
+  img.src = host_info['HTTPS_ORIGIN'] + image_path;
+}
+
+function test2() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    test3();
+  };
+  img.onerror = function() {
+    results += 'FAIL(2)';
+    test3();
+  };
+  img.src = host_info['HTTPS_REMOTE_ORIGIN'] + image_path;
+}
+
+function test3() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    results += 'FAIL(3)';
+    test4();
+  };
+  img.onerror = function() {
+    test4();
+  };
+  img.src = host_info['HTTP_ORIGIN'] + image_path;
+}
+
+function test4() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    results += 'FAIL(4)';
+    test5();
+  };
+  img.onerror = function() {
+    test5();
+  };
+  img.src = host_info['HTTP_REMOTE_ORIGIN'] + image_path;
+}
+
+function test5() {
+  var img = document.createElement('img');
+  document.body.appendChild(img);
+  img.onload = function() {
+    finish();
+  };
+  img.onerror = function() {
+    results += 'FAIL(5)';
+    finish();
+  };
+  img.src = './sample?generate-png';
+}
+
+function finish() {
+  results += 'finish';
+  window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
new file mode 100644
index 0000000..2831c38
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var params = get_query_params(location.href);
+var SCOPE = 'fetch-mixed-content-iframe-inscope-to-' + params['target'] + '.html';
+var URL = 'fetch-rewrite-worker.js';
+var host_info = get_host_info();
+
+window.addEventListener('message', on_message, false);
+
+navigator.serviceWorker.getRegistration(SCOPE)
+  .then(function(registration) {
+      if (registration)
+        return registration.unregister();
+    })
+  .then(function() {
+      return navigator.serviceWorker.register(URL, {scope: SCOPE});
+    })
+  .then(function(registration) {
+      return new Promise(function(resolve) {
+          registration.addEventListener('updatefound', function() {
+              resolve(registration.installing);
+            });
+        });
+    })
+  .then(function(worker) {
+      worker.addEventListener('statechange', on_state_change);
+    })
+  .catch(function(reason) {
+      window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+                                host_info['HTTPS_ORIGIN']);
+     });
+
+function on_state_change(event) {
+  if (event.target.state != 'activated')
+    return;
+  var frame = document.createElement('iframe');
+  frame.src = SCOPE;
+  document.body.appendChild(frame);
+}
+
+function on_message(e) {
+  navigator.serviceWorker.getRegistration(SCOPE)
+    .then(function(registration) {
+        if (registration)
+          return registration.unregister();
+      })
+    .then(function() {
+      window.parent.postMessage(e.data, host_info['HTTPS_ORIGIN']);
+    })
+    .catch(function(reason) {
+        window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+                                  host_info['HTTPS_ORIGIN']);
+     });
+}
+
+function get_query_params(url) {
+  var search = (new URL(url)).search;
+  if (!search) {
+    return {};
+  }
+  var ret = {};
+  var params = search.substring(1).split('&');
+  params.forEach(function(param) {
+      var element = param.split('=');
+      ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+    });
+  return ret;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
new file mode 100644
index 0000000..504e104
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title>iframe for css base url test</title>
+</head>
+<body>
+<script>
+// Load a stylesheet. Create it dynamically so we can construct the href URL
+// dynamically.
+const link = document.createElement('link');
+link.rel = 'stylesheet';
+link.type = 'text/css';
+// Add "request-url-path" to the path to help distinguish the request URL from
+// the response URL. Add |document.location.search| (chosen by the test main
+// page) to tell the service worker how to respond to the request.
+link.href = 'request-url-path/fetch-request-css-base-url-style.css' +
+    document.location.search;
+document.head.appendChild(link);
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
new file mode 100644
index 0000000..f14fcaa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
@@ -0,0 +1 @@
+body { background-image: url("./sample.png");}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
new file mode 100644
index 0000000..f3d6a73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
@@ -0,0 +1,45 @@
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+  source = event.data.port;
+  source.postMessage('pong');
+  event.waitUntil(done);
+});
+
+self.addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+
+  // For the CSS file, respond in a way that may change the response URL,
+  // depending on |url.search|.
+  const cssPath = 'request-url-path/fetch-request-css-base-url-style.css';
+  if (url.pathname.indexOf(cssPath) != -1) {
+    // Respond with a different URL, deleting "request-url-path/".
+    if (url.search == '?fetch') {
+      event.respondWith(fetch('fetch-request-css-base-url-style.css?fetch'));
+    }
+    // Respond with new Response().
+    else if (url.search == '?newResponse') {
+      const styleString = 'body { background-image: url("./sample.png");}';
+      const headers = {'content-type': 'text/css'};
+      event.respondWith(new Response(styleString, headers));
+    }
+  }
+
+  // The image request indicates what the base URL of the CSS was. Message the
+  // result back to the test page.
+  else if (url.pathname.indexOf('sample.png') != -1) {
+    // For some reason |source| is undefined here when running the test manually
+    // in Firefox. The test author experimented with both using Client
+    // (event.source) and MessagePort to try to get the test to pass, but
+    // failed.
+    source.postMessage({
+      url: event.request.url,
+      referrer: event.request.referrer
+    });
+    resolveDone();
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
new file mode 100644
index 0000000..9a7545d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
@@ -0,0 +1 @@
+#crossOriginCss { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
new file mode 100644
index 0000000..3211f78
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
@@ -0,0 +1 @@
+#crossOriginHtml { color: red; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
new file mode 100644
index 0000000..9a4aded
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
@@ -0,0 +1,17 @@
+<style type="text/css">
+#crossOriginCss { color: red; }
+#crossOriginHtml { color: blue; }
+#sameOriginCss { color: red; }
+#sameOriginHtml { color: red; }
+#synthetic { color: red; }
+</style>
+<link href="./cross-origin-css.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./cross-origin-html.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.html" rel="stylesheet" type="text/css">
+<link href="./synthetic.css?mime=no" rel="stylesheet" type="text/css">
+<h1 id=crossOriginCss>I should be blue</h1>
+<h1 id=crossOriginHtml>I should be blue</h1>
+<h1 id=sameOriginCss>I should be blue</h1>
+<h1 id=sameOriginHtml>I should be blue</h1>
+<h1 id=synthetic>I should be blue</h1>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
new file mode 100644
index 0000000..55455bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
@@ -0,0 +1 @@
+#sameOriginCss { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
new file mode 100644
index 0000000..6fad4b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
@@ -0,0 +1 @@
+#sameOriginHtml { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
new file mode 100644
index 0000000..c902366
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe: cross-origin CSS via service worker</title>
+
+<!-- Service worker responds with a cross-origin opaque response. -->
+<link href="cross-origin-css.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a cross-origin CORS approved response. -->
+<link href="cross-origin-css.css?cors" rel="stylesheet" type="text/css">
+
+<!-- Service worker falls back to network. This is a same-origin response. -->
+<link href="fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a new Response() synthetic response. -->
+<link href="synthetic.css" rel="stylesheet" type="text/css">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
new file mode 100644
index 0000000..a71e912
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
@@ -0,0 +1,65 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const HOST_INFO = get_host_info();
+const REMOTE_ORIGIN = HOST_INFO.HTTPS_REMOTE_ORIGIN;
+const BASE_PATH = base_path();
+const CSS_FILE = 'fetch-request-css-cross-origin-mime-check-cross.css';
+const HTML_FILE = 'fetch-request-css-cross-origin-mime-check-cross.html';
+
+function add_pipe_header(url_str, header) {
+  if (url_str.indexOf('?pipe=') == -1) {
+    url_str += '?pipe=';
+  } else {
+    url_str += '|';
+  }
+  url_str += `header${header}`;
+  return url_str;
+}
+
+self.addEventListener('fetch', function(event) {
+    const url = new URL(event.request.url);
+
+    const use_mime =
+        (url.searchParams.get('mime') != 'no');
+    const mime_header = '(Content-Type, text/css)';
+
+    const use_cors =
+        (url.searchParams.has('cors'));
+    const cors_header = '(Access-Control-Allow-Origin, *)';
+
+    const file = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
+
+    // Respond with a cross-origin CSS resource, using CORS if desired.
+    if (file == 'cross-origin-css.css') {
+      let fetch_url =  REMOTE_ORIGIN + BASE_PATH + CSS_FILE;
+      if (use_mime)
+        fetch_url = add_pipe_header(fetch_url, mime_header);
+      if (use_cors)
+        fetch_url = add_pipe_header(fetch_url, cors_header);
+      const mode = use_cors ? 'cors' : 'no-cors';
+      event.respondWith(fetch(fetch_url, {'mode': mode}));
+      return;
+    }
+
+    // Respond with a cross-origin CSS resource with an HTML name. This is only
+    // used in the MIME sniffing test, so MIME is never added.
+    if (file == 'cross-origin-html.css') {
+      const fetch_url = REMOTE_ORIGIN + BASE_PATH + HTML_FILE;
+      event.respondWith(fetch(fetch_url, {mode: 'no-cors'}));
+      return;
+    }
+
+    // Respond with synthetic CSS.
+    if (file == 'synthetic.css') {
+      let headers = {};
+      if (use_mime) {
+        headers['Content-Type'] = 'text/css';
+      }
+
+      event.respondWith(new Response("#synthetic { color: blue; }", {headers}));
+      return;
+    }
+
+    // Otherwise, fallback to network.
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
new file mode 100644
index 0000000..d117d0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
@@ -0,0 +1,32 @@
+<script>
+function xhr(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener(
+        'error',
+        function() { reject(new Error()); });
+      request.addEventListener(
+        'load',
+        function(event) { resolve(request.response); });
+      request.open('GET', url);
+      request.send();
+    });
+}
+
+function load_image(url, cross_origin) {
+  return new Promise(function(resolve, reject) {
+      var img = document.createElement('img');
+      document.body.appendChild(img);
+      img.onload = function() {
+        resolve();
+      };
+      img.onerror = function() {
+        reject(new Error());
+      };
+      if (cross_origin != '') {
+        img.crossOrigin = cross_origin;
+      }
+      img.src = url;
+    });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
new file mode 100644
index 0000000..3b028b2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
@@ -0,0 +1,13 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+    event.data.port.postMessage({requests: requests});
+    requests = [];
+  });
+
+self.addEventListener('fetch', function(event) {
+    requests.push({
+        url: event.request.url,
+        mode: event.request.mode
+      });
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
new file mode 100644
index 0000000..07a0842
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
@@ -0,0 +1,13 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script type="text/javascript">
+  var hostInfo = get_host_info();
+  var makeLink = function(id, url) {
+      var link = document.createElement('link');
+      link.rel = 'import'
+      link.id = id;
+      link.href = url;
+      document.documentElement.appendChild(link);
+    };
+  makeLink('same', hostInfo.HTTPS_ORIGIN + '/sample-dir/same.html');
+  makeLink('other', hostInfo.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
new file mode 100644
index 0000000..110727b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
@@ -0,0 +1,30 @@
+importScripts('/common/get-host-info.sub.js');
+var host_info = get_host_info();
+
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample-dir') == -1) {
+      return;
+    }
+    var result = 'mode=' + event.request.mode +
+      ' credentials=' + event.request.credentials;
+    if (url == host_info.HTTPS_ORIGIN + '/sample-dir/same.html') {
+      event.respondWith(new Response(
+        result +
+        '<link id="same-same" rel="import" ' +
+        'href="' + host_info.HTTPS_ORIGIN + '/sample-dir/same-same.html">' +
+        '<link id="same-other" rel="import" ' +
+        ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+        '/sample-dir/same-other.html">'));
+    } else if (url == host_info.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html') {
+      event.respondWith(new Response(
+        result +
+        '<link id="other-same" rel="import" ' +
+        ' href="' + host_info.HTTPS_ORIGIN + '/sample-dir/other-same.html">' +
+        '<link id="other-other" rel="import" ' +
+        ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+        '/sample-dir/other-other.html">'));
+    } else {
+      event.respondWith(new Response(result));
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
new file mode 100644
index 0000000..e6e9380
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
@@ -0,0 +1 @@
+<script src="./fetch-request-no-freshness-headers-script.py"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
new file mode 100644
index 0000000..bf8df15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
@@ -0,0 +1,6 @@
+def main(request, response):
+    headers = []
+    # Sets an ETag header to check the cache revalidation behavior.
+    headers.append((b"ETag", b"abc123"))
+    headers.append((b"Content-Type", b"text/javascript"))
+    return headers, b"/* empty script */"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
new file mode 100644
index 0000000..2bd59d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
@@ -0,0 +1,18 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+    event.data.port.postMessage({requests: requests});
+  });
+
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    var headers = [];
+    for (var header of event.request.headers) {
+      headers.push(header);
+    }
+    requests.push({
+        url: url,
+        headers: headers
+      });
+    event.respondWith(fetch(event.request));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
new file mode 100644
index 0000000..ffd76bf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
@@ -0,0 +1,35 @@
+<script>
+function xhr(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener(
+        'error',
+        function(event) { reject(event); });
+      request.addEventListener(
+        'load',
+        function(event) { resolve(request.response); });
+      request.open('GET', url);
+      request.send();
+    });
+}
+
+function load_image(url) {
+  return new Promise(function(resolve, reject) {
+      var img = document.createElement('img');
+      document.body.appendChild(img);
+      img.onload = resolve;
+      img.onerror = reject;
+      img.src = url;
+    });
+}
+
+function load_audio(url) {
+  return new Promise(function(resolve, reject) {
+      var audio = document.createElement('audio');
+      document.body.appendChild(audio);
+      audio.oncanplay = resolve;
+      audio.onerror = reject;
+      audio.src = url;
+    });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
new file mode 100644
index 0000000..86e9f4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
@@ -0,0 +1,87 @@
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+
+function load_image(url, cross_origin) {
+  const img = document.createElement('img');
+  if (cross_origin != '') {
+    img.crossOrigin = cross_origin;
+  }
+  img.src = url;
+}
+
+function load_script(url, cross_origin) {
+  const script = document.createElement('script');
+  script.src = url;
+  if (cross_origin != '') {
+    script.crossOrigin = cross_origin;
+  }
+  document.body.appendChild(script);
+}
+
+function load_css(url, cross_origin) {
+  const link = document.createElement('link');
+  link.rel = 'stylesheet'
+  link.href = url;
+  link.type = 'text/css';
+  if (cross_origin != '') {
+    link.crossOrigin = cross_origin;
+  }
+  document.body.appendChild(link);
+}
+
+function load_font(url) {
+  const fontFace = new FontFace('test', 'url(' + url + ')');
+  fontFace.load();
+}
+
+function load_css_image(url, type) {
+  const div = document.createElement('div');
+  document.body.appendChild(div);
+  div.style[type] = 'url(' + url + ')';
+}
+
+function load_css_image_set(url, type) {
+  const div = document.createElement('div');
+  document.body.appendChild(div);
+  div.style[type] = 'image-set(url(' + url + ') 1x)';
+  if (!div.style[type]) {
+    div.style[type] = '-webkit-image-set(url(' + url + ') 1x)';
+  }
+}
+
+function load_script_with_integrity(url, integrity) {
+  const script = document.createElement('script');
+  script.src = url;
+  script.integrity = integrity;
+  document.body.appendChild(script);
+}
+
+function load_css_with_integrity(url, integrity) {
+  const link = document.createElement('link');
+  link.rel = 'stylesheet'
+  link.href = url;
+  link.type = 'text/css';
+  link.integrity = integrity;
+  document.body.appendChild(link);
+}
+
+function load_audio(url, cross_origin) {
+  const audio = document.createElement('audio');
+  if (cross_origin != '') {
+    audio.crossOrigin = cross_origin;
+  }
+  audio.src = url;
+  document.body.appendChild(audio);
+}
+
+function load_video(url, cross_origin) {
+  const video = document.createElement('video');
+  if (cross_origin != '') {
+    video.crossOrigin = cross_origin;
+  }
+  video.src = url;
+  document.body.appendChild(video);
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
new file mode 100644
index 0000000..983cccb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
@@ -0,0 +1,26 @@
+const requests = [];
+let port = undefined;
+
+self.onmessage = e => {
+  const message = e.data;
+  if ('port' in message) {
+    port = message.port;
+    port.postMessage({ready: true});
+  }
+};
+
+self.addEventListener('fetch', e => {
+  const url = e.request.url;
+  if (!url.includes('sample?test')) {
+    return;
+  }
+  port.postMessage({
+    url: url,
+    mode: e.request.mode,
+    redirect: e.request.redirect,
+    credentials: e.request.credentials,
+    integrity: e.request.integrity,
+    destination: e.request.destination
+  });
+  e.respondWith(Promise.reject());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
new file mode 100644
index 0000000..b3ddec1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
@@ -0,0 +1,208 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function get_boundary(headers) {
+  var reg = new RegExp('multipart\/form-data; boundary=(.*)');
+  for (var i = 0; i < headers.length; ++i) {
+    if (headers[i][0] != 'content-type') {
+      continue;
+    }
+    var regResult = reg.exec(headers[i][1]);
+    if (!regResult) {
+      continue;
+    }
+    return regResult[1];
+  }
+  return '';
+}
+
+function xhr_send(url_base, method, data, with_credentials) {
+  return new Promise(function(resolve, reject) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        resolve(JSON.parse(xhr.response));
+      };
+      xhr.onerror = function() {
+        reject('XHR should succeed.');
+      };
+      xhr.responseType = 'text';
+      if (with_credentials) {
+        xhr.withCredentials = true;
+      }
+      xhr.open(method, url_base + '/sample?test', true);
+      xhr.send(data);
+    });
+}
+
+function get_sorted_header_name_list(headers) {
+  var header_names = [];
+  var idx, name;
+
+  for (idx = 0; idx < headers.length; ++idx) {
+    name = headers[idx][0];
+    // The `Accept-Language` header is optional; its presence should not
+    // influence test results.
+    //
+    // > 4. If request’s header list does not contain `Accept-Language`, user
+    // >    agents should append `Accept-Language`/an appropriate value to
+    // >    request's header list.
+    //
+    // https://fetch.spec.whatwg.org/#fetching
+    if (name === 'accept-language') {
+      continue;
+    }
+
+    header_names.push(name);
+  }
+  header_names.sort();
+  return header_names;
+}
+
+function get_header_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+    .then(function(response) {
+        assert_array_equals(
+          get_sorted_header_name_list(response.headers),
+          ["accept"],
+          'event.request has the expected headers for same-origin GET.');
+      });
+}
+
+function post_header_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', '', false)
+    .then(function(response) {
+        assert_array_equals(
+          get_sorted_header_name_list(response.headers),
+          ["accept", "content-type"],
+          'event.request has the expected headers for same-origin POST.');
+      });
+}
+
+function cross_origin_get_header_test() {
+  return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false)
+    .then(function(response) {
+        assert_array_equals(
+          get_sorted_header_name_list(response.headers),
+          ["accept"],
+          'event.request has the expected headers for cross-origin GET.');
+      });
+}
+
+function cross_origin_post_header_test() {
+  return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'POST', '', false)
+    .then(function(response) {
+        assert_array_equals(
+          get_sorted_header_name_list(response.headers),
+          ["accept", "content-type"],
+          'event.request has the expected headers for cross-origin POST.');
+      });
+}
+
+function string_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', 'test string', false)
+    .then(function(response) {
+        assert_equals(response.method, 'POST');
+        assert_equals(response.body, 'test string');
+      });
+}
+
+function blob_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', new Blob(['test blob']),
+                  false)
+    .then(function(response) {
+        assert_equals(response.method, 'POST');
+        assert_equals(response.body, 'test blob');
+      });
+}
+
+function custom_method_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'XXX', 'test string xxx', false)
+    .then(function(response) {
+        assert_equals(response.method, 'XXX');
+        assert_equals(response.body, 'test string xxx');
+      });
+}
+
+function options_method_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'OPTIONS', 'test string xxx', false)
+    .then(function(response) {
+        assert_equals(response.method, 'OPTIONS');
+        assert_equals(response.body, 'test string xxx');
+      });
+}
+
+function form_data_test() {
+    var formData = new FormData();
+    formData.append('sample string', '1234567890');
+    formData.append('sample blob', new Blob(['blob content']));
+    formData.append('sample file', new File(['file content'], 'file.dat'));
+    return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', formData, false)
+    .then(function(response) {
+        assert_equals(response.method, 'POST');
+        var boundary = get_boundary(response.headers);
+        var expected_body =
+          '--' + boundary + '\r\n' +
+          'Content-Disposition: form-data; name="sample string"\r\n' +
+          '\r\n' +
+          '1234567890\r\n' +
+          '--' + boundary + '\r\n' +
+          'Content-Disposition: form-data; name="sample blob"; ' +
+          'filename="blob"\r\n' +
+          'Content-Type: application/octet-stream\r\n' +
+          '\r\n' +
+          'blob content\r\n' +
+          '--' + boundary + '\r\n' +
+          'Content-Disposition: form-data; name="sample file"; ' +
+          'filename="file.dat"\r\n' +
+          'Content-Type: application/octet-stream\r\n' +
+          '\r\n' +
+          'file content\r\n' +
+          '--' + boundary + '--\r\n';
+        assert_equals(response.body, expected_body, "form data response content is as expected");
+      });
+}
+
+function mode_credentials_test() {
+  return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+    .then(function(response){
+        assert_equals(response.mode, 'cors');
+        assert_equals(response.credentials, 'same-origin');
+        return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', true);
+      })
+    .then(function(response){
+        assert_equals(response.mode, 'cors');
+        assert_equals(response.credentials, 'include');
+        return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false);
+      })
+    .then(function(response){
+        assert_equals(response.mode, 'cors');
+        assert_equals(response.credentials, 'same-origin');
+        return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', true);
+      })
+    .then(function(response){
+        assert_equals(response.mode, 'cors');
+        assert_equals(response.credentials, 'include');
+      });
+}
+
+function data_url_test() {
+  return new Promise(function(resolve, reject) {
+        var xhr = new XMLHttpRequest();
+        xhr.onload = function() {
+          resolve(xhr.response);
+        };
+        xhr.onerror = function() {
+          reject('XHR should succeed.');
+        };
+        xhr.responseType = 'text';
+        xhr.open('GET', 'data:text/html,Foobar', true);
+        xhr.send();
+      })
+    .then(function(data) {
+        assert_equals(data, 'Foobar');
+      });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
new file mode 100644
index 0000000..b8d3db9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
@@ -0,0 +1,19 @@
+"use strict";
+
+self.onfetch = event => {
+  if (event.request.url.endsWith("non-existent-stream-1.txt")) {
+    const rs1 = new ReadableStream();
+    event.respondWith(new Response(rs1));
+    rs1.cancel(1);
+  } else if (event.request.url.endsWith("non-existent-stream-2.txt")) {
+    const rs2 = new ReadableStream({
+      start(controller) { controller.error(1) }
+    });
+    event.respondWith(new Response(rs2));
+  } else if (event.request.url.endsWith("non-existent-stream-3.txt")) {
+    const rs3 = new ReadableStream({
+      pull(controller) { controller.error(1) }
+    });
+    event.respondWith(new Response(rs3));
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
new file mode 100644
index 0000000..900762f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted iframe</title>
+<script>
+'use strict';
+
+function performSyncXHR(url) {
+  var syncXhr = new XMLHttpRequest();
+  syncXhr.open('GET', url, false);
+  syncXhr.send();
+
+  return syncXhr;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
new file mode 100644
index 0000000..0d24ffc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
@@ -0,0 +1,41 @@
+'use strict';
+
+self.onfetch = function(event) {
+  if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+    event.respondWith(new Response('Response from service worker'));
+  } else if (event.request.url.indexOf('/iframe_page') !== -1) {
+    event.respondWith(new Response(
+        '<!DOCTYPE html>\n' +
+        '<script>\n' +
+        'function performSyncXHROnWorker(url) {\n' +
+        '  return new Promise((resolve) => {\n' +
+        '    var worker =\n' +
+        '        new Worker(\'./worker_script\');\n' +
+        '    worker.addEventListener(\'message\', (msg) => {\n' +
+        '      resolve(msg.data);\n' +
+        '    });\n' +
+        '    worker.postMessage({\n' +
+        '      url: url\n' +
+        '    });\n' +
+        '  });\n' +
+        '}\n' +
+        '</script>',
+        {
+          headers: [['content-type', 'text/html']]
+        }));
+  } else if (event.request.url.indexOf('/worker_script') !== -1) {
+    event.respondWith(new Response(
+        'self.onmessage = (msg) => {' +
+        '  const syncXhr = new XMLHttpRequest();' +
+        '  syncXhr.open(\'GET\', msg.data.url, false);' +
+        '  syncXhr.send();' +
+        '  self.postMessage({' +
+        '    status: syncXhr.status,' +
+        '    responseText: syncXhr.responseText' +
+        '  });' +
+        '}',
+        {
+          headers: [['content-type', 'application/javascript']]
+        }));
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
new file mode 100644
index 0000000..070e572
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.onfetch = function(event) {
+  if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+    event.respondWith(new Response('Response from service worker'));
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
new file mode 100644
index 0000000..4e42837
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample?test') == -1) {
+      return;
+    }
+    event.respondWith(new Promise(function(resolve) {
+        var headers = [];
+        for (var header of event.request.headers) {
+          headers.push(header);
+        }
+        event.request.text()
+          .then(function(result) {
+              resolve(new Response(JSON.stringify({
+                  method: event.request.method,
+                  mode: event.request.mode,
+                  credentials: event.request.credentials,
+                  headers: headers,
+                  body: result
+                })));
+            });
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
new file mode 100644
index 0000000..5f09efe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
new file mode 100644
index 0000000..c26eebe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
@@ -0,0 +1,53 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function xhr_send(method, data) {
+  return new Promise(function(resolve, reject) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        resolve(xhr);
+      };
+      xhr.onerror = function() {
+        reject('XHR should succeed.');
+      };
+      xhr.responseType = 'text';
+      xhr.open(method, './sample?test', true);
+      xhr.send(data);
+    });
+}
+
+function coalesce_headers_test() {
+  return xhr_send('POST', 'test string')
+  .then(function(xhr) {
+      window.parent.postMessage({results: xhr.getResponseHeader('foo')},
+                                host_info['HTTPS_ORIGIN']);
+
+      return new Promise(function(resolve) {
+          window.addEventListener('message', function handle(evt) {
+              if (evt.data !== 'ACK') {
+                return;
+              }
+
+              window.removeEventListener('message', handle);
+              resolve();
+            });
+        });
+    });
+}
+
+window.addEventListener('message', function(evt) {
+    var port;
+
+    if (evt.data !== 'START') {
+      return;
+    }
+
+    port = evt.ports[0];
+
+    coalesce_headers_test()
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
new file mode 100644
index 0000000..0301b12
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample?test') == -1) {
+      return;
+    }
+    event.respondWith(new Promise(function(resolve) {
+        var headers = new Headers;
+        headers.append('foo', 'foo');
+        headers.append('foo', 'bar');
+        resolve(new Response('hello world', {'headers': headers}));
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html
new file mode 100644
index 0000000..6d27cf1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+    const params  =new URLSearchParams(location.search);
+    const mode = params.get("mode") || "cors";
+    const path = params.get('path');
+    const bufferPromise =
+      new Promise(resolve =>
+        fetch(path, {mode})
+          .then(response => resolve(response.arrayBuffer()))
+          .catch(() => resolve(new Uint8Array())));
+
+    const entryPromise = new Promise(resolve => {
+      new PerformanceObserver(entries => {
+        const byName = entries.getEntriesByType("resource").find(e => e.name.includes(path));
+        if (byName)
+          resolve(byName);
+      }).observe({entryTypes: ["resource"]});
+    });
+
+    Promise.all([bufferPromise, entryPromise]).then(([buffer, entry]) => {
+      parent.postMessage({
+        buffer,
+        entry: entry.toJSON(),
+    }, '*');
+    });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js
new file mode 100644
index 0000000..775efc0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js
@@ -0,0 +1,35 @@
+self.addEventListener('fetch', event => {
+    const path = event.request.url.match(/\/(?<name>[^\/]+)$/);
+    switch (path?.groups?.name) {
+        case 'constructed':
+            event.respondWith(new Response(new Uint8Array([1, 2, 3])));
+            break;
+        case 'forward':
+            event.respondWith(fetch('/common/text-plain.txt'));
+            break;
+        case 'stream':
+            event.respondWith((async() => {
+                const res = await fetch('/common/text-plain.txt');
+                const body = await res.body;
+                const reader = await body.getReader();
+                const stream = new ReadableStream({
+                    async start(controller) {
+                        while (true) {
+                            const {done, value} = await reader.read();
+                            if (done)
+                                break;
+
+                            controller.enqueue(value);
+                        }
+                        controller.close();
+                        reader.releaseLock();
+                    }
+                });
+                return new Response(stream);
+            })());
+            break;
+        default:
+          event.respondWith(fetch(event.request));
+          break;
+    }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
new file mode 100644
index 0000000..64c99c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
@@ -0,0 +1,4 @@
+// This script is intended to be served with the `Referrer-Policy` header as
+// defined in the corresponding `.headers` file.
+
+importScripts('fetch-rewrite-worker.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
new file mode 100644
index 0000000..5ae4265
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
@@ -0,0 +1,2 @@
+Content-Type: application/javascript
+Referrer-Policy: origin
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
new file mode 100644
index 0000000..20a8066
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
@@ -0,0 +1,166 @@
+// By default, this worker responds to fetch events with
+// respondWith(fetch(request)). Additionally, if the request has a &url
+// parameter, it fetches the provided URL instead. Because it forwards fetch
+// events to this other URL, it is called the "fetch rewrite" worker.
+//
+// The worker also looks for other params on the request to do more custom
+// behavior, like falling back to network or throwing an error.
+
+function get_query_params(url) {
+  var search = (new URL(url)).search;
+  if (!search) {
+    return {};
+  }
+  var ret = {};
+  var params = search.substring(1).split('&');
+  params.forEach(function(param) {
+      var element = param.split('=');
+      ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+    });
+  return ret;
+}
+
+function get_request_init(base, params) {
+  var init = {};
+  init['method'] = params['method'] || base['method'];
+  init['mode'] = params['mode'] || base['mode'];
+  if (init['mode'] == 'navigate') {
+    init['mode'] = 'same-origin';
+  }
+  init['credentials'] = params['credentials'] || base['credentials'];
+  init['redirect'] = params['redirect-mode'] || base['redirect'];
+  return init;
+}
+
+self.addEventListener('fetch', function(event) {
+    var params = get_query_params(event.request.url);
+    var init = get_request_init(event.request, params);
+    var url = params['url'];
+    if (params['ignore']) {
+      return;
+    }
+    if (params['throw']) {
+      throw new Error('boom');
+    }
+    if (params['reject']) {
+      event.respondWith(new Promise(function(resolve, reject) {
+          reject();
+        }));
+      return;
+    }
+    if (params['resolve-null']) {
+      event.respondWith(new Promise(function(resolve) {
+          resolve(null);
+        }));
+      return;
+    }
+    if (params['generate-png']) {
+      var binary = atob(
+          'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAA' +
+          'RnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/Kf' +
+          'gQLABKXJBqMGjBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=');
+      var array = new Uint8Array(binary.length);
+      for(var i = 0; i < binary.length; i++) {
+        array[i] = binary.charCodeAt(i);
+      };
+      event.respondWith(new Response(new Blob([array], {type: 'image/png'})));
+      return;
+    }
+    if (params['check-ua-header']) {
+      var ua = event.request.headers.get('User-Agent');
+      if (ua) {
+        // We have a user agent!
+        event.respondWith(new Response(new Blob([ua])));
+      } else {
+        // We don't have a user-agent!
+        event.respondWith(new Response(new Blob(["NO_UA"])));
+      }
+      return;
+    }
+    if (params['check-accept-header']) {
+      var accept = event.request.headers.get('Accept');
+      if (accept) {
+        event.respondWith(new Response(accept));
+      } else {
+        event.respondWith(new Response('NO_ACCEPT'));
+      }
+      return;
+    }
+    event.respondWith(new Promise(function(resolve, reject) {
+        var request = event.request;
+        if (url) {
+          request = new Request(url, init);
+        } else if (params['change-request']) {
+          request = new Request(request, init);
+        }
+        const response_promise = params['navpreload'] ? event.preloadResponse
+                                                      : fetch(request);
+        response_promise.then(function(response) {
+          var expectedType = params['expected_type'];
+          if (expectedType && response.type !== expectedType) {
+            // Resolve a JSON object with a failure instead of rejecting
+            // in order to distinguish this from a NetworkError, which
+            // may be expected even if the type is correct.
+            resolve(new Response(JSON.stringify({
+              result: 'failure',
+              detail: 'got ' + response.type + ' Response.type instead of ' +
+                      expectedType
+            })));
+          }
+
+          var expectedRedirected = params['expected_redirected'];
+          if (typeof expectedRedirected !== 'undefined') {
+            var expected_redirected = (expectedRedirected === 'true');
+            if(response.redirected !== expected_redirected) {
+              // This is simply determining how to pass an error to the outer
+              // test case(fetch-request-redirect.https.html).
+              var execptedResolves = params['expected_resolves'];
+              if (execptedResolves === 'true') {
+                // Reject a JSON object with a failure since promise is expected
+                // to be resolved.
+                reject(new Response(JSON.stringify({
+                  result: 'failure',
+                  detail: 'got '+ response.redirected +
+                          ' Response.redirected instead of ' +
+                          expectedRedirected
+                })));
+              } else {
+                // Resolve a JSON object with a failure since promise is
+                // expected to be rejected.
+                resolve(new Response(JSON.stringify({
+                  result: 'failure',
+                  detail: 'got '+ response.redirected +
+                          ' Response.redirected instead of ' +
+                          expectedRedirected
+                })));
+              }
+            }
+          }
+
+          if (params['clone']) {
+            response = response.clone();
+          }
+
+          // |cache| means to bounce responses through Cache Storage and back.
+          if (params['cache']) {
+            var cacheName = "cached-fetches-" + performance.now() + "-" +
+                            event.request.url;
+            var cache;
+            var cachedResponse;
+            return self.caches.open(cacheName).then(function(opened) {
+              cache = opened;
+              return cache.put(request, response);
+            }).then(function() {
+              return cache.match(request);
+            }).then(function(cached) {
+              cachedResponse = cached;
+              return self.caches.delete(cacheName);
+            }).then(function() {
+               resolve(cachedResponse);
+            });
+          } else {
+            resolve(response);
+          }
+        }, reject)
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
new file mode 100644
index 0000000..123053b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript
+Service-Worker-Allowed: /
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js
new file mode 100644
index 0000000..b950b9a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js
@@ -0,0 +1,35 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+importScripts('/resources/testharness.js');
+
+const storedResponse = new Response(new Blob(['a simple text file']))
+const absolultePath = `${base_path()}/simple.txt`
+
+self.addEventListener('fetch', event => {
+    const search = new URLSearchParams(new URL(event.request.url).search.substr(1))
+    const variant = search.get('variant')
+    const delay = search.get('delay')
+    if (!variant)
+        return
+
+    switch (variant) {
+        case 'forward':
+            event.respondWith(fetch(event.request.url))
+            break
+        case 'redirect':
+            event.respondWith(fetch(`/xhr/resources/redirect.py?location=${base_path()}/simple.txt`))
+            break
+        case 'delay-before-fetch':
+            event.respondWith(
+                new Promise(resolve => {
+                    step_timeout(() => fetch(event.request.url).then(resolve), delay)
+            }))
+            break
+        case 'delay-after-fetch':
+            event.respondWith(new Promise(resolve => {
+                fetch(event.request.url)
+                    .then(response => step_timeout(() => resolve(response), delay))
+            }))
+            break
+    }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
new file mode 100644
index 0000000..92a96ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
@@ -0,0 +1,31 @@
+var activatePromiseResolve;
+
+addEventListener('activate', function(evt) {
+  evt.waitUntil(new Promise(function(resolve) {
+    activatePromiseResolve = resolve;
+  }));
+});
+
+addEventListener('message', async function(evt) {
+  switch (evt.data) {
+    case 'CLAIM':
+      evt.waitUntil(new Promise(async resolve => {
+        await clients.claim();
+        evt.source.postMessage('CLAIMED');
+        resolve();
+      }));
+      break;
+    case 'ACTIVATE':
+      if (typeof activatePromiseResolve !== 'function') {
+        throw new Error('Not activating!');
+      }
+      activatePromiseResolve();
+      break;
+    default:
+      throw new Error('Unknown message!');
+  }
+});
+
+addEventListener('fetch', function(evt) {
+  evt.respondWith(new Response('Hello world'));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html b/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html
new file mode 100644
index 0000000..cd11a30
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<form method="POST" id="form"></form>
+<script>
+function onLoad() {
+  const params = new URLSearchParams(self.location.search);
+  const form = document.getElementById('form');
+  form.action = params.get('target');
+  form.submit();
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html
new file mode 100644
index 0000000..7fc35f1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Service Worker: frame for getRegistrations()</title>
+<script>
+var scope = 'scope-for-getregistrations';
+var script = 'empty-worker.js';
+var registration;
+
+navigator.serviceWorker.register(script, { scope: scope })
+  .then(function(r) { registration = r; window.parent.postMessage('ready', '*'); })
+
+self.onmessage = function(e) {
+  if (e.data == 'unregister') {
+    registration.unregister()
+      .then(function() {
+          e.ports[0].postMessage('unregistered');
+        });
+  }
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
new file mode 100644
index 0000000..f0e6c7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
@@ -0,0 +1,107 @@
+// This worker expects a fetch event for a navigation and messages back the
+// result of clients.get(event.resultingClientId).
+
+// Resolves when the test finishes.
+let testFinishPromise;
+let resolveTestFinishPromise;
+let rejectTestFinishPromise;
+
+// Resolves to clients.get(event.resultingClientId) from the fetch event.
+let getPromise;
+let resolveGetPromise;
+let rejectGetPromise;
+
+let resultingClientId;
+
+function startTest() {
+  testFinishPromise = new Promise((resolve, reject) => {
+    resolveTestFinishPromise = resolve;
+    rejectTestFinishPromise = reject;
+  });
+
+  getPromise = new Promise((resolve, reject) => {
+    resolveGetPromise = resolve;
+    rejectGetPromise = reject;
+  });
+}
+
+async function describeGetPromiseResult(promise) {
+  const result = {};
+
+  await promise.then(
+    (client) => {
+      result.promiseState = 'fulfilled';
+      if (client === undefined) {
+        result.promiseValue = 'undefinedValue';
+      } else if (client instanceof Client) {
+        result.promiseValue = 'client';
+        result.client = {
+          id:  client.id,
+          url: client.url
+        };
+      } else {
+        result.promiseValue = 'unknown';
+      }
+    },
+    (error) => {
+      result.promiseState = 'rejected';
+    });
+
+  return result;
+}
+
+async function handleGetResultingClient(event) {
+  // Note that this message can arrive before |resultingClientId| is populated.
+  const result = await describeGetPromiseResult(getPromise);
+  // |resultingClientId| must be populated by now.
+  result.queriedId = resultingClientId;
+  event.source.postMessage(result);
+};
+
+async function handleGetClient(event) {
+  const id = event.data.id;
+  const result = await describeGetPromiseResult(self.clients.get(id));
+  result.queriedId = id;
+  event.source.postMessage(result);
+};
+
+self.addEventListener('message', (event) => {
+  if (event.data.command == 'startTest') {
+    startTest();
+    event.waitUntil(testFinishPromise);
+    event.source.postMessage('ok');
+    return;
+  }
+
+  if (event.data.command == 'finishTest') {
+    resolveTestFinishPromise();
+    event.source.postMessage('ok');
+    return;
+  }
+
+  if (event.data.command == 'getResultingClient') {
+    event.waitUntil(handleGetResultingClient(event));
+    return;
+  }
+
+  if (event.data.command == 'getClient') {
+    event.waitUntil(handleGetClient(event));
+    return;
+  }
+});
+
+async function handleFetch(event) {
+  try {
+    resultingClientId = event.resultingClientId;
+    const client = await self.clients.get(resultingClientId);
+    resolveGetPromise(client);
+  } catch (error) {
+    rejectGetPromise(error);
+  }
+}
+
+self.addEventListener('fetch', (event) => {
+  if (event.request.mode != 'navigate')
+    return;
+  event.waitUntil(handleFetch(event));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
new file mode 100644
index 0000000..bcab353
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>register, unregister, and report result to opener</title>
+<body>
+<script>
+'use strict';
+
+if (!navigator.serviceWorker) {
+  window.opener.postMessage('FAIL: navigator.serviceWorker is undefined', '*');
+} else {
+  navigator.serviceWorker.register('empty-worker.js', {scope: 'scope-register'})
+    .then(
+      registration => {
+          registration.unregister().then(() => {
+              window.opener.postMessage('OK', '*');
+            });
+        },
+      error => {
+          window.opener.postMessage('FAIL: ' + error.name, '*');
+        })
+    .catch(error => {
+        window.opener.postMessage('ERROR: ' + error.name, '*');
+      });
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
new file mode 100644
index 0000000..3a61d7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<script>
+    const url = new URL(new URLSearchParams(location.search.substr(1)).get('url'), location.href);
+    const before = performance.now();
+    fetch(url)
+        .then(r => r.text())
+        .then(() =>
+            parent.postMessage({
+                before,
+                after: performance.now(),
+                entry: performance.getEntriesByName(url)[0].toJSON()
+            }));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html
new file mode 100644
index 0000000..ce78840
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="square">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
new file mode 100644
index 0000000..d8a94ad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
@@ -0,0 +1,19 @@
+function prototypeChain(global) {
+  let result = [];
+  while (global !== null) {
+    let thrown = false;
+    let next = Object.getPrototypeOf(global);
+    try {
+      Object.setPrototypeOf(global, {});
+      result.push('mutable');
+    } catch (e) {
+      result.push('immutable');
+    }
+    global = next;
+  }
+  return result;
+}
+
+self.onmessage = function(e) {
+  e.data.postMessage(prototypeChain(self));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
new file mode 100644
index 0000000..8f0b68e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
@@ -0,0 +1,6 @@
+def main(request, response):
+  # This script generates a worker script for static imports from module
+  # service workers.
+  headers = [(b'Content-Type', b'text/javascript')]
+  body = b"import './echo-cookie-worker.py?key=%s'" % request.GET[b'key']
+  return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
new file mode 100644
index 0000000..f5eac95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
@@ -0,0 +1 @@
+importScripts(`echo-cookie-worker.py${location.search}`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py
new file mode 100644
index 0000000..b6e82f3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py
@@ -0,0 +1,10 @@
+def main(request, response):
+    if b'mime' in request.GET:
+        return (
+            [(b'Content-Type', b'application/javascript')],
+            b"importScripts('./mime-type-worker.py?mime=%s');" % request.GET[b'mime']
+        )
+    return (
+        [(b'Content-Type', b'application/javascript')],
+        b"importScripts('./mime-type-worker.py');"
+    )
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl b/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl
new file mode 100644
index 0000000..063a62d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+    <xsl:import href="xslt-pass.xsl"/>
+</xsl:stylesheet>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
new file mode 100644
index 0000000..e9899d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
@@ -0,0 +1,8 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request, and a script that is updated every time when
+// requesting it.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+const additional_key = params.get('AdditionalKey');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`,
+              `update-worker.py?Key=${additional_key}&Mode=normal`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
new file mode 100644
index 0000000..b569346
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
@@ -0,0 +1,6 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request. The resulting body also changes each time it is
+// requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js
new file mode 100644
index 0000000..19c7a4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js
@@ -0,0 +1 @@
+importScripts('404.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
new file mode 100644
index 0000000..b432854
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
@@ -0,0 +1 @@
+importScripts('https://{{domains[www1]}}:{{ports[https][0]}}/service-workers/service-worker/resources/import-scripts-version.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
new file mode 100644
index 0000000..0fdcb0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
@@ -0,0 +1,10 @@
+importScripts('/resources/testharness.js');
+
+let echo1 = null;
+let echo2 = null;
+let arg1 = 'import-scripts-get.py?output=echo1&msg=test1';
+let arg2 = 'import-scripts-get.py?output=echo2&msg=test2';
+
+importScripts(arg1, arg2);
+assert_equals(echo1, 'test1');
+assert_equals(echo2, 'test2');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py
new file mode 100644
index 0000000..d38d660
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+    return ([
+        (b'Cache-Control', b'no-cache, must-revalidate'),
+        (b'Pragma', b'no-cache'),
+        (b'Content-Type', b'application/javascript')],
+      b'echo_output = "%s";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py
new file mode 100644
index 0000000..ab7b84e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py
@@ -0,0 +1,6 @@
+def main(req, res):
+    return ([
+        (b'Cache-Control', b'no-cache, must-revalidate'),
+        (b'Pragma', b'no-cache'),
+        (b'Content-Type', b'application/javascript')],
+        b'%s = "%s";\n' % (req.GET[b'output'], req.GET[b'msg']))
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
new file mode 100644
index 0000000..d4f1f3e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
@@ -0,0 +1,49 @@
+const badMimeTypes = [
+  null,  // no MIME type
+  'text/plain',
+];
+
+const validMimeTypes = [
+  'application/ecmascript',
+  'application/javascript',
+  'application/x-ecmascript',
+  'application/x-javascript',
+  'text/ecmascript',
+  'text/javascript',
+  'text/javascript1.0',
+  'text/javascript1.1',
+  'text/javascript1.2',
+  'text/javascript1.3',
+  'text/javascript1.4',
+  'text/javascript1.5',
+  'text/jscript',
+  'text/livescript',
+  'text/x-ecmascript',
+  'text/x-javascript',
+];
+
+function importScriptsWithMimeType(mimeType) {
+  importScripts(`./mime-type-worker.py${mimeType ? '?mime=' + mimeType : ''}`);
+}
+
+importScripts('/resources/testharness.js');
+
+for (const mimeType of badMimeTypes) {
+  test(() => {
+    assert_throws_dom(
+      'NetworkError',
+      () => { importScriptsWithMimeType(mimeType); },
+      `importScripts with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''} throws NetworkError`,
+    );
+  }, `Importing script with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''}`);
+}
+
+for (const mimeType of validMimeTypes) {
+  test(() => {
+    try {
+      importScriptsWithMimeType(mimeType);
+    } catch {
+      assert_unreached(`importScripts with MIME type ${mimeType} should not throw`);
+    }
+  }, `Importing script with valid JavaScript MIME type ${mimeType}`);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
new file mode 100644
index 0000000..56c04f0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
@@ -0,0 +1 @@
+// empty script
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
new file mode 100644
index 0000000..f612ab8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
@@ -0,0 +1,7 @@
+// This worker imports a script that returns 200 on the first request and a
+// redirect on the second request. The resulting body also changes each time it
+// is requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=redirect&` +
+              `Redirect=update-worker.py?Key=${key}%26Mode=normal`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
new file mode 100644
index 0000000..d02a453
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
@@ -0,0 +1 @@
+importScripts('redirect.py?Redirect=import-scripts-version.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
new file mode 100644
index 0000000..b3b9bc4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
@@ -0,0 +1,15 @@
+importScripts('/resources/testharness.js');
+
+let version = null;
+importScripts('import-scripts-version.py');
+// Once imported, the stored script should be loaded for subsequent importScripts.
+const expected_version = version;
+
+version = null;
+importScripts('import-scripts-version.py');
+assert_equals(expected_version, version, 'second import');
+
+version = null;
+importScripts('import-scripts-version.py', 'import-scripts-version.py',
+    'import-scripts-version.py');
+assert_equals(expected_version, version, 'multiple imports');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
new file mode 100644
index 0000000..e016646
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
@@ -0,0 +1,31 @@
+importScripts('/resources/testharness.js');
+
+let echo_output = null;
+
+// Tests importing a script that sets |echo_output| to the query string.
+function test_import(str) {
+  echo_output = null;
+  importScripts('import-scripts-echo.py?msg=' + str);
+  assert_equals(echo_output, str);
+}
+
+test_import('root');
+test_import('root-and-message');
+
+self.addEventListener('install', () => {
+    test_import('install');
+    test_import('install-and-message');
+  });
+
+self.addEventListener('message', e => {
+    var error = null;
+    echo_output = null;
+
+    try {
+      importScripts('import-scripts-echo.py?msg=' + e.data);
+    } catch (e) {
+      error = e && e.name;
+    }
+
+    e.source.postMessage({ error: error, value: echo_output });
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py
new file mode 100644
index 0000000..cde2854
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py
@@ -0,0 +1,17 @@
+import datetime
+import time
+
+epoch = datetime.datetime(1970, 1, 1)
+
+def main(req, res):
+    # Artificially delay response time in order to ensure uniqueness of
+    # computed value
+    time.sleep(0.1)
+
+    now = (datetime.datetime.now() - epoch).total_seconds()
+
+    return ([
+        (b'Cache-Control', b'no-cache, must-revalidate'),
+        (b'Pragma', b'no-cache'),
+        (b'Content-Type', b'application/javascript')],
+       u'version = "%s";\n' % now)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js
new file mode 100644
index 0000000..5fc5204
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js
@@ -0,0 +1 @@
+const imported = 'A classic script.';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js
new file mode 100644
index 0000000..56d196d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js
@@ -0,0 +1 @@
+export const imported = 'A module script.';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js
new file mode 100644
index 0000000..9add476
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js
@@ -0,0 +1,57 @@
+self.addEventListener('message', function(e) {
+    var message = e.data;
+    if (message.action === 'create') {
+      e.waitUntil(deleteDB()
+          .then(doIndexedDBTest)
+          .then(function() {
+              message.port.postMessage({ type: 'created' });
+            })
+          .catch(function(reason) {
+              message.port.postMessage({ type: 'error', value: reason });
+            }));
+    } else if (message.action === 'cleanup') {
+      e.waitUntil(deleteDB()
+          .then(function() {
+              message.port.postMessage({ type: 'done' });
+            })
+          .catch(function(reason) {
+              message.port.postMessage({ type: 'error', value: reason });
+            }));
+    }
+  });
+
+function deleteDB() {
+  return new Promise(function(resolve, reject) {
+      var delete_request = indexedDB.deleteDatabase('db');
+
+      delete_request.onsuccess = resolve;
+      delete_request.onerror = reject;
+    });
+}
+
+function doIndexedDBTest(port) {
+  return new Promise(function(resolve, reject) {
+      var open_request = indexedDB.open('db');
+
+      open_request.onerror = reject;
+      open_request.onupgradeneeded = function() {
+        var db = open_request.result;
+        db.createObjectStore('store');
+      };
+      open_request.onsuccess = function() {
+        var db = open_request.result;
+        var tx = db.transaction('store', 'readwrite');
+        var store = tx.objectStore('store');
+        store.put('value', 'key');
+
+        tx.onerror = function() {
+            db.close();
+            reject(tx.error);
+          };
+        tx.oncomplete = function() {
+            db.close();
+            resolve();
+          };
+      };
+    });
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js
new file mode 100644
index 0000000..1c94ae2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js
@@ -0,0 +1,9 @@
+importScripts('worker-testharness.js');
+
+self.oninstall = function(event) {
+    assert_true(event instanceof ExtendableEvent, 'instance of ExtendableEvent');
+    assert_true(event instanceof InstallEvent, 'instance of InstallEvent');
+    assert_equals(event.type, 'install', '`type` property value');
+    assert_false(event.cancelable, '`cancelable` property value');
+    assert_false(event.bubbles, '`bubbles` property value');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html
new file mode 100644
index 0000000..ed20cd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Loading...</p>
+<script>
+async function install() {
+  let script;
+  for (const q of location.search.slice(1).split('&')) {
+    if (q.split('=')[0] === 'script') {
+      script = q.split('=')[1];
+    }
+  }
+  const scope = location.href;
+  const reg = await navigator.serviceWorker.register(script, {scope});
+  await navigator.serviceWorker.ready;
+  location.reload();
+}
+
+install();
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
new file mode 100644
index 0000000..a3f239b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
@@ -0,0 +1,59 @@
+'use strict';
+
+// This file checks additional interface requirements, on top of the basic IDL
+// that is validated in service-workers/idlharness.any.js
+
+importScripts('/resources/testharness.js');
+
+test(function() {
+    var req = new Request('http://{{host}}/',
+                          {method: 'POST',
+                           headers: [['Content-Type', 'Text/Html']]});
+    assert_equals(
+      new ExtendableEvent('ExtendableEvent').type,
+      'ExtendableEvent', 'Type of ExtendableEvent should be ExtendableEvent');
+    assert_throws_js(TypeError, function() {
+        new FetchEvent('FetchEvent');
+    }, 'FetchEvent constructor with one argument throws');
+    assert_throws_js(TypeError, function() {
+        new FetchEvent('FetchEvent', {});
+    }, 'FetchEvent constructor with empty init dict throws');
+    assert_throws_js(TypeError, function() {
+        new FetchEvent('FetchEvent', {request: null});
+    }, 'FetchEvent constructor with null request member throws');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req}).type,
+      'FetchEvent', 'Type of FetchEvent should be FetchEvent');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req}).cancelable,
+      false, 'Default FetchEvent.cancelable should be false');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req}).bubbles,
+      false, 'Default FetchEvent.bubbles should be false');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req}).clientId,
+      '', 'Default FetchEvent.clientId should be the empty string');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req, cancelable: false}).cancelable,
+      false, 'FetchEvent.cancelable should be false');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request: req, clientId : 'test-client-id'}).clientId, 'test-client-id',
+      'FetchEvent.clientId with option {clientId : "test-client-id"} should be "test-client-id"');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request : req}).request.url,
+      'http://{{host}}/',
+      'FetchEvent.request.url should return the value it was initialized to');
+    assert_equals(
+      new FetchEvent('FetchEvent', {request : req}).isReload,
+      undefined,
+      'FetchEvent.isReload should not exist');
+
+  }, 'Event constructors');
+
+test(() => {
+    assert_false('XMLHttpRequest' in self);
+  }, 'xhr is not exposed');
+
+test(() => {
+    assert_false('createObjectURL' in self.URL);
+  }, 'URL.createObjectURL is not exposed')
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
new file mode 100644
index 0000000..04a9cb5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
@@ -0,0 +1,28 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+  return new Promise(function(resolve, reject) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        if (xhr.getResponseHeader('Content-Type') !== null) {
+          reject('Content-Type must be null.');
+        }
+        resolve();
+      };
+      xhr.onerror = function() {
+        reject('XHR must succeed.');
+      };
+      xhr.responseType = 'text';
+      xhr.open(method, './sample?test', true);
+      xhr.send(data);
+    });
+}
+
+window.addEventListener('message', function(evt) {
+    var port = evt.ports[0];
+    xhr_send('POST', 'test string')
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
new file mode 100644
index 0000000..865dc30
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample?test') == -1) {
+      return;
+    }
+    event.respondWith(new Promise(function(resolve) {
+        // null byte in blob type
+        resolve(new Response(new Blob([],{type: 'a\0b'})));
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
new file mode 100644
index 0000000..05977c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
@@ -0,0 +1,9 @@
+import time
+def main(request, response):
+    response.headers.set(b"Content-Type", b"application/javascript")
+    response.headers.set(b"Transfer-encoding", b"chunked")
+    response.write_status_headers()
+
+    time.sleep(1)
+
+    response.writer.write(b"XX\r\n\r\n")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
new file mode 100644
index 0000000..a8edd06
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
@@ -0,0 +1,2 @@
+def main(request, response):
+    return [(b"Content-Type", b"application/javascript"), (b"Transfer-encoding", b"chunked")], b"XX\r\n\r\n"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
new file mode 100644
index 0000000..8f0e6ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
@@ -0,0 +1,25 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+  return new Promise(function(resolve, reject) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        reject('XHR must fail.');
+      };
+      xhr.onerror = function() {
+        resolve();
+      };
+      xhr.responseType = 'text';
+      xhr.open(method, './sample?test', true);
+      xhr.send(data);
+    });
+}
+
+window.addEventListener('message', function(evt) {
+    var port = evt.ports[0];
+    xhr_send('POST', 'test string')
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js
new file mode 100644
index 0000000..850874b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample?test') == -1) {
+      return;
+    }
+    event.respondWith(new Promise(function(resolve) {
+        var headers = new Headers;
+        headers.append('foo', 'foo');
+        headers.append('foo', 'b\0r'); // header value with a null byte
+        resolve(new Response('hello world', {'headers': headers}));
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
new file mode 100644
index 0000000..cf2fa8d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
@@ -0,0 +1,23 @@
+<script>
+function xhr_send(method, data) {
+  return new Promise(function(resolve, reject) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function() {
+        resolve();
+      };
+      xhr.onerror = function() {
+        reject('XHR must succeed.');
+      };
+      xhr.responseType = 'text';
+      xhr.open(method, './sample?test', true);
+      xhr.send(data);
+    });
+}
+
+window.addEventListener('message', function(evt) {
+    var port = evt.ports[0];
+    xhr_send('POST', 'test string')
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
new file mode 100644
index 0000000..d9ecca2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+    var url = event.request.url;
+    if (url.indexOf('sample?test') == -1) {
+      return;
+    }
+
+    event.respondWith(new Promise(function(resolve) {
+        var headers = new Headers;
+        headers.append('TEST', 'ßÀ¿'); // header value holds the Latin1 (ISO8859-1) string.
+        resolve(new Response('hello world', {'headers': headers}));
+      }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js
new file mode 100644
index 0000000..18c673b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js
@@ -0,0 +1,29 @@
+function run_test(data, sender) {
+  if (data === 'xhr') {
+    const xhr = new XMLHttpRequest();
+    xhr.open('GET', 'synthesized-response.txt', true);
+    xhr.responseType = 'text';
+    xhr.send();
+    xhr.onload = evt => sender.postMessage(xhr.responseText);
+    xhr.onerror = () => sender.postMessage('XHR failed!');
+  } else if (data === 'fetch') {
+    fetch('synthesized-response.txt')
+        .then(response => response.text())
+        .then(data => sender.postMessage(data))
+        .catch(error => sender.postMessage('Fetch failed!'));
+  } else if (data === 'importScripts') {
+    importScripts('synthesized-response.js');
+    // |message| is provided by 'synthesized-response.js';
+    sender.postMessage(message);
+  } else {
+    sender.postMessage('Unexpected message! ' + data);
+  }
+}
+
+// Entry point for dedicated workers.
+self.onmessage = evt => run_test(evt.data, self);
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+  evt.ports[0].onmessage = e => run_test(e.data, evt.ports[0]);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html b/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html
new file mode 100644
index 0000000..0cabce6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html
@@ -0,0 +1,9 @@
+<script>
+addEventListener('load', function() {
+  opener.postMessage({ type: 'LOADED' }, '*');
+});
+
+addEventListener('pageshow', function() {
+  opener.postMessage({ type: 'PAGESHOW' }, '*');
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
new file mode 100644
index 0000000..5520c3a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+const fetchURL = new URL('sample.txt', window.location).href;
+
+const frameControllerText =
+`<script>
+  let t = null;
+  try {
+    if (navigator.serviceWorker.controller) {
+      t = navigator.serviceWorker.controller.scriptURL;
+    }
+  } catch (e) {
+    t = e.message;
+  } finally {
+    parent.postMessage({ data: t }, '*');
+  }
+</` + `script>`;
+
+const frameFetchText =
+`<script>
+  fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+    return response.text();
+  }).then(text => {
+    parent.postMessage({ data: text }, '*');
+  }).catch(e => {
+    parent.postMessage({ data: e.message }, '*');
+  });
+</` + `script>`;
+
+const workerControllerText =
+`let t = navigator.serviceWorker.controller
+       ? navigator.serviceWorker.controller.scriptURL
+       : null;
+self.postMessage(t);`;
+
+const workerFetchText =
+`fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+  return response.text();
+}).then(text => {
+  self.postMessage(text);
+}).catch(e => {
+  self.postMessage(e.message);
+});`
+
+function getChildText(opts) {
+  if (opts.child === 'iframe') {
+    if (opts.check === 'controller') {
+      return frameControllerText;
+    }
+
+    if (opts.check === 'fetch') {
+      return frameFetchText;
+    }
+
+    throw('unexpected feature to check: ' + opts.check);
+  }
+
+  if (opts.child === 'worker') {
+    if (opts.check === 'controller') {
+      return workerControllerText;
+    }
+
+    if (opts.check === 'fetch') {
+      return workerFetchText;
+    }
+
+    throw('unexpected feature to check: ' + opts.check);
+  }
+
+  throw('unexpected child type ' + opts.child);
+}
+
+function makeURL(opts) {
+  let mimetype = opts.child === 'iframe' ? 'text/html'
+                                         : 'text/javascript';
+
+  if (opts.scheme === 'blob') {
+    let blob = new Blob([getChildText(opts)], { type: mimetype });
+    return URL.createObjectURL(blob);
+  }
+
+  if (opts.scheme === 'data') {
+    return `data:${mimetype},${getChildText(opts)}`;
+  }
+
+  throw(`unexpected URL scheme ${opts.scheme}`);
+}
+
+function testWorkerChild(url) {
+  let w = new Worker(url);
+  return new Promise((resolve, reject) => {
+    w.onmessage = resolve;
+    w.onerror = evt => {
+      reject(evt.message);
+    }
+  });
+}
+
+function testIframeChild(url) {
+  let frame = document.createElement('iframe');
+  frame.src = url;
+  document.body.appendChild(frame);
+
+  return new Promise(resolve => {
+    addEventListener('message', evt => {
+      resolve(evt.data);
+    }, { once: true });
+  });
+}
+
+function testURL(opts, url) {
+  if (opts.child === 'worker') {
+    return testWorkerChild(url);
+  }
+
+  if (opts.child === 'iframe') {
+    return testIframeChild(url);
+  }
+
+  throw(`unexpected child type ${opts.child}`);
+}
+
+function checkChildController(opts) {
+  let url = makeURL(opts);
+  return testURL(opts, url);
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
new file mode 100644
index 0000000..4b7aad0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
@@ -0,0 +1,5 @@
+addEventListener('fetch', evt => {
+  if (evt.request.url.includes('sample')) {
+    evt.respondWith(new Response('intercepted'));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html b/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html
new file mode 100644
index 0000000..f0ced06
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+  const params = new URLSearchParams(self.location.search);
+  self.location = params.get('target');
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis
new file mode 100644
index 0000000..bc3c68d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis
@@ -0,0 +1 @@
+HAHAHA THIS IS NOT HTTP AND THE BROWSER SHOULD CONSIDER IT A NETWORK ERROR
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py
new file mode 100644
index 0000000..319b6e2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py
@@ -0,0 +1,14 @@
+def main(request, response):
+    headers = [(b"Content-Type", b"application/javascript")]
+
+    body = {u'parse-error': u'var foo = function() {;',
+            u'undefined-error': u'foo.bar = 42;',
+            u'uncaught-exception': u'throw new DOMException("AbortError");',
+            u'caught-exception': u'try { throw new Error; } catch(e) {}',
+            u'import-malformed-script': u'importScripts("malformed-worker.py?parse-error");',
+            u'import-no-such-script': u'importScripts("no-such-script.js");',
+            u'top-level-await': u'await Promise.resolve(1);',
+            u'instantiation-error': u'import nonexistent from "./imported-module-script.js";',
+            u'instantiation-error-and-top-level-await': u'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);'}[request.url_parts.query]
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html b/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html
new file mode 100644
index 0000000..2c45c59
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<script>
+  let draft = [];
+  var resolve_manual_promise;
+  let manual_promise =
+    new Promise(resolve => resolve_manual_promise = resolve).then(() => draft.push('microtask'));
+
+  let resolve_message_promise;
+  let message_promise = new Promise(resolve => resolve_message_promise = resolve);
+  function handle_message(event) {
+    draft.push('message');
+    resolve_message_promise();
+  }
+
+  var result = Promise.all([manual_promise, message_promise]).then(() => draft);
+</script>
+
+<script src="empty.js?key=start"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js
new file mode 100644
index 0000000..5c34a7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js
@@ -0,0 +1,9 @@
+self.addEventListener('fetch', function(event) {
+    // Use an empty content-type value to force mime-sniffing.  Note, this
+    // must be passed to the constructor since the mime-type of the Response
+    // is fixed and cannot be later changed.
+    var res = new Response('<!DOCTYPE html>\n<h1 id=\'testid\'>test</h1>', {
+      headers: { 'content-type': '' }
+    });
+    event.respondWith(res);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py
new file mode 100644
index 0000000..92a602e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py
@@ -0,0 +1,4 @@
+def main(request, response):
+    if b'mime' in request.GET:
+        return [(b'Content-Type', request.GET[b'mime'])], b""
+    return [], b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py
new file mode 100644
index 0000000..ebee4ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py
@@ -0,0 +1,27 @@
+import random
+
+import time
+
+body = u'''
+onactivate = (e) => e.waitUntil(clients.claim());
+var resolve_wait_until;
+var wait_until = new Promise(resolve => {
+    resolve_wait_until = resolve;
+  });
+onmessage = (e) => {
+    if (e.data == 'wait')
+      e.waitUntil(wait_until);
+    if (e.data == 'go')
+      resolve_wait_until();
+  };'''
+
+def main(request, response):
+    headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+               (b'Pragma', b'no-cache'),
+               (b'Content-Type', b'application/javascript')]
+
+    skipWaiting = u''
+    if b'skip-waiting' in request.GET:
+        skipWaiting = u'skipWaiting();'
+
+    return headers, u'/* %s %s */ %s %s' % (time.time(), random.random(), skipWaiting, body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js
new file mode 100644
index 0000000..385fe71
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js
@@ -0,0 +1 @@
+import * as module from './imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html
new file mode 100644
index 0000000..c59b955
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html
@@ -0,0 +1,19 @@
+<script>
+function load_multipart_image(src) {
+    return new Promise((resolve, reject) => {
+        const img = document.createElement('img');
+        img.addEventListener('load', () => resolve(img));
+        img.addEventListener('error', (e) => reject(new DOMException('load failed', 'NetworkError')));
+        img.src = src;
+    });
+}
+
+function get_image_data(img) {
+    const canvas = document.createElement('canvas');
+    const context = canvas.getContext('2d');
+    context.drawImage(img, 0, 0);
+    // When |img.src| is cross origin, this should throw a SecurityError.
+    const imageData = context.getImageData(0, 0, 1, 1);
+    return imageData;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js
new file mode 100644
index 0000000..a38fe54
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js
@@ -0,0 +1,21 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const host_info = get_host_info();
+
+const multipart_image_path = base_path() + 'multipart-image.py';
+const sameorigin_url = host_info['HTTPS_ORIGIN'] + multipart_image_path;
+const cross_origin_url = host_info['HTTPS_REMOTE_ORIGIN'] + multipart_image_path;
+
+self.addEventListener('fetch', event => {
+    const url = event.request.url;
+    if (url.indexOf('cross-origin-multipart-image-with-no-cors') >= 0) {
+        event.respondWith(fetch(cross_origin_url, {mode: 'no-cors'}));
+    } else if (url.indexOf('cross-origin-multipart-image-with-cors-rejected') >= 0) {
+        event.respondWith(fetch(cross_origin_url, {mode: 'cors'}));
+    } else if (url.indexOf('cross-origin-multipart-image-with-cors-approved') >= 0) {
+        event.respondWith(fetch(cross_origin_url + '?approvecors', {mode: 'cors'}));
+    } else if (url.indexOf('same-origin-multipart-image') >= 0) {
+        event.respondWith(fetch(sameorigin_url));
+    }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py
new file mode 100644
index 0000000..9a3c035
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py
@@ -0,0 +1,23 @@
+# A request handler that serves a multipart image.
+
+import os
+
+
+BOUNDARY = b'cutHere'
+
+
+def create_part(path):
+    with open(path, u'rb') as f:
+        return b'Content-Type: image/png\r\n\r\n' + f.read() + b'--%s' % BOUNDARY
+
+
+def main(request, response):
+    content_type = b'multipart/x-mixed-replace; boundary=%s' % BOUNDARY
+    headers = [(b'Content-Type', content_type)]
+    if b'approvecors' in request.GET:
+        headers.append((b'Access-Control-Allow-Origin', b'*'))
+
+    image_path = os.path.join(request.doc_root, u'images')
+    body = create_part(os.path.join(image_path, u'red.png'))
+    body = body + create_part(os.path.join(image_path, u'red-16x16.png'))
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js
new file mode 100644
index 0000000..f961743
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js
@@ -0,0 +1,21 @@
+addEventListener('message', function(evt) {
+  if (evt.data.type === 'GET_CLIENTS') {
+    clients.matchAll(evt.data.opts).then(function(clientList) {
+      var resultList = clientList.map(function(c) {
+        return { url: c.url, frameType: c.frameType, id: c.id };
+      });
+      evt.source.postMessage({ type: 'success', detail: resultList });
+    }).catch(function(err) {
+      evt.source.postMessage({
+        type: 'failure',
+        detail: 'matchAll() rejected with "' + err + '"'
+      });
+    });
+    return;
+  }
+
+  evt.source.postMessage({
+    type: 'failure',
+    detail: 'Unexpected message type "' + evt.data.type + '"'
+  });
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py
new file mode 100644
index 0000000..5b2e044
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py
@@ -0,0 +1,19 @@
+def main(request, response):
+    response.status = (200, b"OK")
+    response.headers.set(b"Content-Type", b"text/html")
+    return b"""
+    <script>
+      self.addEventListener('load', evt => {
+        self.parent.postMessage({
+          origin: '%s',
+          referer: '%s',
+          'sec-fetch-site': '%s',
+          'sec-fetch-mode': '%s',
+          'sec-fetch-dest': '%s',
+        });
+      });
+    </script>""" % (request.headers.get(
+        b"origin", b"not set"), request.headers.get(b"referer", b"not set"),
+                    request.headers.get(b"sec-fetch-site", b"not set"),
+                    request.headers.get(b"sec-fetch-mode", b"not set"),
+                    request.headers.get(b"sec-fetch-dest", b"not set"))
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
new file mode 100644
index 0000000..39f11ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('fetch', function(event) {
+    event.respondWith(
+        fetch(event.request)
+          .then(
+              function(response) {
+                return response;
+              },
+              function(error) {
+                return new Response('Error:' + error);
+              }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py
new file mode 100644
index 0000000..d10329e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py
@@ -0,0 +1,11 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+filename = os.path.basename(isomorphic_encode(__file__))
+
+def main(request, response):
+    if request.method == u'POST':
+        return 302, [(b'Location', b'./%s?redirect' % filename)], b''
+
+    return [(b'Content-Type', b'text/plain')], request.request_path
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
new file mode 100644
index 0000000..d82571d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'navigation-redirect-scope1.py';
+var SCRIPT = 'redirect-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+  .then(function(reg) {
+      if (reg)
+        return reg.unregister();
+    })
+  .then(function() {
+      return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+    })
+  .then(function(reg) {
+      registration = reg;
+      worker = reg.installing;
+      return new Promise(function(resolve) {
+          worker.addEventListener('statechange', function() {
+              if (worker.state == 'activated')
+                resolve();
+            });
+        });
+    });
+
+function send_result(message_id, result) {
+  window.parent.postMessage(
+      {id: message_id, result: result},
+      host_info['HTTPS_ORIGIN']);
+}
+
+function get_request_infos(worker) {
+  return new Promise(function(resolve) {
+    var channel = new MessageChannel();
+    channel.port1.onmessage = (msg) => {
+      resolve(msg.data.requestInfos);
+    };
+    worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+                       [channel.port2]);
+  });
+}
+
+function get_clients(worker, actual_ids) {
+  return new Promise(function(resolve) {
+      var channel = new MessageChannel();
+      channel.port1.onmessage = (msg) => {
+        resolve(msg.data.clients);
+      };
+      worker.postMessage({
+        command: 'getClients',
+        actual_ids,
+        port: channel.port2
+      }, [channel.port2]);
+    });
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+  if (e.origin != host_info['HTTPS_ORIGIN']) {
+    console.error('invalid origin: ' + e.origin);
+    return;
+  }
+  const command = e.data.message.command;
+  if (command == 'wait_for_worker') {
+    wait_for_worker_promise.then(function() { send_result(e.data.id, 'ok'); });
+  } else if (command == 'get_request_infos') {
+    get_request_infos(worker)
+      .then(function(data) {
+          send_result(e.data.id, data);
+        });
+  } else if (command == 'get_clients') {
+    get_clients(worker, e.data.message.actual_ids)
+      .then(function(data) {
+          send_result(e.data.id, data);
+        });
+  } else if (command == 'unregister') {
+    registration.unregister()
+      .then(function() {
+          send_result(e.data.id, 'ok');
+        });
+  }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
@@ -0,0 +1,22 @@
+def main(request, response):
+    if b"url" in request.GET:
+        headers = [(b"Location", request.GET[b"url"])]
+        return 302, headers, b''
+
+    status = 200
+
+    if b"noLocationRedirect" in request.GET:
+        status = 302
+
+    return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+  window.parent.postMessage(
+      {
+        id: event.data.id,
+        result: location.href
+      }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
@@ -0,0 +1,22 @@
+def main(request, response):
+    if b"url" in request.GET:
+        headers = [(b"Location", request.GET[b"url"])]
+        return 302, headers, b''
+
+    status = 200
+
+    if b"noLocationRedirect" in request.GET:
+        status = 302
+
+    return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+  window.parent.postMessage(
+      {
+        id: event.data.id,
+        result: location.href
+      }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
@@ -0,0 +1,22 @@
+def main(request, response):
+    if b"url" in request.GET:
+        headers = [(b"Location", request.GET[b"url"])]
+        return 302, headers, b''
+
+    status = 200
+
+    if b"noLocationRedirect" in request.GET:
+        status = 302
+
+    return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+  window.parent.postMessage(
+      {
+        id: event.data.id,
+        result: location.href
+      }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
new file mode 100644
index 0000000..40e27c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var SCOPE = './redirect.py?Redirect=' + encodeURI('http://example.com');
+var SCRIPT = 'navigation-redirect-to-http-worker.js';
+var host_info = get_host_info();
+
+navigator.serviceWorker.getRegistration(SCOPE)
+  .then(function(registration) {
+      if (registration)
+        return registration.unregister();
+    })
+  .then(function() {
+      return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+    })
+  .then(function(registration) {
+      return new Promise(function(resolve) {
+          registration.addEventListener('updatefound', function() {
+              resolve(registration.installing);
+            });
+        });
+    })
+  .then(function(worker) {
+      worker.addEventListener('statechange', on_state_change);
+    })
+  .catch(function(reason) {
+      window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+                                host_info['HTTPS_ORIGIN']);
+     });
+
+function on_state_change(event) {
+  if (event.target.state != 'activated')
+    return;
+  with_iframe(SCOPE, {auto_remove: false})
+    .then(function(frame) {
+        window.parent.postMessage(
+            {results: frame.contentDocument.body.textContent},
+            host_info['HTTPS_ORIGIN']);
+      });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
new file mode 100644
index 0000000..6f2a8ae
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
@@ -0,0 +1,22 @@
+importScripts('/resources/testharness.js');
+
+self.addEventListener('fetch', function(event) {
+    event.respondWith(new Promise(function(resolve) {
+      Promise.resolve()
+        .then(function() {
+            assert_equals(
+                event.request.redirect, 'manual',
+                'The redirect mode of navigation request must be manual.');
+            return fetch(event.request);
+          })
+        .then(function(response) {
+            assert_equals(
+                response.type, 'opaqueredirect',
+                'The response type of 302 response must be opaqueredirect.');
+            resolve(new Response('OK'));
+          })
+        .catch(function(error) {
+            resolve(new Response('Failed in SW: ' + error));
+          });
+    }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
new file mode 100644
index 0000000..79c5408
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+const timings = {}
+
+const DELAY_ACTIVATION = 500
+
+self.addEventListener('activate', event => {
+    event.waitUntil(new Promise(resolve => {
+        timings.activateWorkerStart = performance.now() + performance.timeOrigin;
+
+        // This gives us enough time to ensure activation would delay fetch handling
+        step_timeout(resolve, DELAY_ACTIVATION);
+    }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin));
+})
+
+self.addEventListener('fetch', event => {
+    timings.handleFetchEvent = performance.now() + performance.timeOrigin;
+    event.respondWith(Promise.resolve(new Response(new Blob([`
+            <script>
+                parent.postMessage(${JSON.stringify(timings)}, "*")
+            </script>
+    `]))));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js
new file mode 100644
index 0000000..8539b40
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('fetch', (event) => {
+    const url = event.request.url;
+
+    // Network fallback.
+    if (url.indexOf('network-fallback') >= 0) {
+        return;
+    }
+
+    // Don't intercept redirect.
+    if (url.indexOf('redirect.py') >= 0) {
+        return;
+    }
+
+    event.respondWith(fetch(url));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
new file mode 100644
index 0000000..fc048e2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerUrl = new URL('create-blob-url-worker.js', baseLocation).href;
+const worker = new Worker(workerUrl);
+
+function fetch_in_worker(url) {
+  const resourceUrl = new URL(url, baseLocation).href;
+  return new Promise((resolve) => {
+    worker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.postMessage(resourceUrl);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html
new file mode 100644
index 0000000..f0eafcd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+  const childWorkerScript = 'self.onmessage = async (e) => {' +
+    '  const response = await fetch(e.data);' +
+    '  const text = await response.text();' +
+    '  self.postMessage(text);' +
+    '};';
+  const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+  const blobUrl = URL.createObjectURL(blob);
+  const childWorker = new Worker(blobUrl);
+
+  // When a message comes from the parent frame, sends a resource url to the
+  // child worker.
+  self.onmessage = (e) => {
+    childWorker.postMessage(e.data);
+  };
+  // When a message comes from the child worker, sends a content of fetch() to
+  // the parent frame.
+  childWorker.onmessage = (e) => {
+    self.postMessage(e.data);
+  };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+  const resourceUrl = new URL(url, baseLocation).href;
+  return new Promise((resolve) => {
+    worker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.postMessage(resourceUrl);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html
new file mode 100644
index 0000000..115ab26
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+  navigator.serviceWorker.onmessage = event => parent.postMessage(event.data, '*', event.ports);
+</script>
+<iframe id='child'></iframe>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html
new file mode 100644
index 0000000..b4832d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+async function onLoad() {
+  self.addEventListener('message', evt => {
+    if (self.opener)
+      self.opener.postMessage(evt.data, '*');
+    else
+      self.top.postMessage(evt.data, '*');
+  }, { once: true });
+  const params = new URLSearchParams(self.location.search);
+  const frame = document.createElement('iframe');
+  frame.src = params.get('target');
+  document.body.appendChild(frame);
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
new file mode 100644
index 0000000..3fad2c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+  const workerUrl =
+    new URL('postmessage-fetched-text.js', '${baseLocation}').href;
+  const childWorker = new Worker(workerUrl);
+
+  // When a message comes from the parent frame, sends a resource url to the
+  // child worker.
+  self.onmessage = (e) => {
+    childWorker.postMessage(e.data);
+  };
+  // When a message comes from the child worker, sends a content of fetch() to
+  // the parent frame.
+  childWorker.onmessage = (e) => {
+    self.postMessage(e.data);
+  };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+  const resourceUrl = new URL(url, baseLocation).href;
+  return new Promise((resolve) => {
+    worker.onmessage = (event) => {
+      resolve(event.data);
+    };
+    worker.postMessage(resourceUrl);
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js
new file mode 100644
index 0000000..ef0ed8f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js
@@ -0,0 +1,23 @@
+// Entry point for dedicated workers.
+self.onmessage = evt => {
+  try {
+    const worker = new Worker('load_worker.js');
+    worker.onmessage = evt => self.postMessage(evt.data);
+    worker.postMessage(evt.data);
+  } catch (err) {
+    self.postMessage('Unexpected error! ' + err.message);
+  }
+};
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+  evt.ports[0].onmessage = e => {
+    try {
+      const worker = new Worker('load_worker.js');
+      worker.onmessage = e => evt.ports[0].postMessage(e.data);
+      worker.postMessage(evt.data);
+    } catch (err) {
+      evt.ports[0].postMessage('Unexpected error! ' + err.message);
+    }
+  };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js b/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js
new file mode 100644
index 0000000..ecedd6c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js
@@ -0,0 +1,18 @@
+/** @type {[name: string, url: string][]} */
+const importUrlTests = [
+  ["Module URL", "./basic-module.js"],
+  // In no-dynamic-import-in-module.any.js, this module is also statically imported
+  ["Another module URL", "./basic-module-2.js"],
+  [
+    "Module data: URL",
+    "data:text/javascript;charset=utf-8," +
+      encodeURIComponent(`export default 'hello!';`),
+  ],
+];
+
+for (const [name, url] of importUrlTests) {
+  promise_test(
+    (t) => promise_rejects_js(t, TypeError, import(url), "Import must reject"),
+    name
+  );
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py b/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py
new file mode 100644
index 0000000..71f5a9d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py
@@ -0,0 +1,11 @@
+from urllib.parse import parse_qs
+
+from wptserve.utils import isomorphic_encode
+
+def main(req, res):
+  qs_cookie_val = parse_qs(req.url_parts.query).get(u'set-cookie-notification')
+
+  if qs_cookie_val:
+    res.set_cookie(b'notification', isomorphic_encode(qs_cookie_val[0]))
+
+  return b'not really an icon'
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5a20a58
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<object type="image/png" data="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    if (!navigator.serviceWorker.controller)
+      resolve('FAIL: this iframe is not controlled');
+
+    const elem = document.querySelector('object');
+    elem.addEventListener('load', e => {
+        resolve('request was not intercepted');
+      });
+    elem.addEventListener('error', e => {
+        resolve('FAIL: request was intercepted');
+      });
+  });
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..0aeb819
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    report_result = resolve;
+  });
+</script>
+
+<object data="embedded-content-from-server.html"></object>
+</body>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5c8ab79
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+    report_result = resolve;
+  });
+
+let el = document.createElement('object');
+el.data = "/common/blank.html";
+el.addEventListener('load', _ => {
+  window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..7c97014
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
@@ -0,0 +1,13 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+    var level = event.data;
+    if (level < max_nesting_level)
+      dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+    throw Error('error at level ' + level);
+  });
+
+self.addEventListener('activate', function(event) {
+    dispatchEvent(new MessageEvent('message', { data: 1 }));
+  });
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..0bd9d31
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..d56c951
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..eb12ae8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
new file mode 100644
index 0000000..1e88ac5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple activate handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
+self.addEventListener('activate', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
new file mode 100644
index 0000000..65b02b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('activate', event => {
+  event.waitUntil(new Promise(() => {
+        // Use a promise that never resolves to prevent this service worker from
+        // advancing past the 'activating' state.
+      }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
new file mode 100644
index 0000000..b905d55
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
@@ -0,0 +1,10 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+  if (event.request.url.endsWith('waituntil-forever')) {
+    event.respondWith(new Promise(() => {
+        // Use a promise that never resolves to prevent this fetch from
+        // completing.
+    }));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..6729ab6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
@@ -0,0 +1,12 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+    var level = event.data;
+    if (level < max_nesting_level)
+      dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+    throw Error('error at level ' + level);
+  });
+
+self.addEventListener('install', function(event) {
+    dispatchEvent(new MessageEvent('message', { data: 1 }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..c2c499a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..7667c27
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..8f56d1b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
new file mode 100644
index 0000000..cc2f6d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple install handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
+self.addEventListener('install', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
new file mode 100644
index 0000000..964483f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('install', event => {
+  event.waitUntil(new Promise(() => {
+        // Use a promise that never resolves to prevent this service worker from
+        // advancing past the 'installing' state.
+      }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
new file mode 100644
index 0000000..6cb8f6e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
@@ -0,0 +1,5 @@
+self.addEventListener('install', function(event) {
+  event.waitUntil(new Promise(function(aRequest, aResponse) {
+      throw new Error();
+    }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
new file mode 100644
index 0000000..6f439ae
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
@@ -0,0 +1,8 @@
+'use strict';
+
+// Use an infinite loop to prevent this service worker from advancing past the
+// 'parsed' state.
+let i = 0;
+while (true) {
+  ++i;
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
new file mode 100644
index 0000000..9c6d8bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-being-preloaded-xhr.html';
+function runTest() {
+  var l = document.createElement('link');
+  // Use link rel=preload to try to get the browser to cache the opaque
+  // response.
+  l.setAttribute('rel', 'preload');
+  l.setAttribute('href', URL);
+  l.setAttribute('as', 'fetch');
+  l.onerror = function() {
+    parent.done('FAIL: preload failed unexpectedly');
+  };
+  document.body.appendChild(l);
+  xhr = new XMLHttpRequest;
+  xhr.withCredentials = true;
+  xhr.open('GET', URL);
+  // opaque-response returns an opaque response from serviceworker and thus
+  // the XHR must fail because it is not no-cors request.
+  // Particularly, the XHR must not reuse the opaque response from the
+  // preload request.
+  xhr.onerror = function() {
+    parent.done('PASS');
+  };
+  xhr.onload = function() {
+    parent.done('FAIL: ' + xhr.responseText);
+  };
+  xhr.send();
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
new file mode 100644
index 0000000..9859bad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+
+var remoteUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] +
+  '/service-workers/service-worker/resources/simple.txt'
+
+self.addEventListener('fetch', event => {
+    if (!event.request.url.match(/opaque-response\?from=/)) {
+      return;
+    }
+
+    event.respondWith(fetch(remoteUrl, {mode: 'no-cors'}));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
new file mode 100644
index 0000000..f31ac9b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-preloaded-xhr.html';
+function runTest() {
+  var l = document.createElement('link');
+  // Use link rel=preload to try to get the browser to cache the opaque
+  // response.
+  l.setAttribute('rel', 'preload');
+  l.setAttribute('href', URL);
+  l.setAttribute('as', 'fetch');
+  l.onload = function() {
+    xhr = new XMLHttpRequest;
+    xhr.withCredentials = true;
+    xhr.open('GET', URL);
+    // opaque-response returns an opaque response from serviceworker and thus
+    // the XHR must fail because it is not no-cors request.
+    // Particularly, the XHR must not reuse the opaque response from the
+    // preload request.
+    xhr.onerror = function() {
+      parent.done('PASS');
+    };
+    xhr.onload = function() {
+      parent.done('FAIL: ' + xhr.responseText);
+    };
+    xhr.send();
+  };
+  l.onerror = function() {
+    parent.done('FAIL: preload failed unexpectedly');
+  };
+  document.body.appendChild(l);
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html
new file mode 100644
index 0000000..a57aace
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<script>
+self.addEventListener('error', evt => {
+  self.parent.postMessage({ type: 'ErrorEvent', msg: evt.message }, '*');
+});
+
+const el = document.createElement('script');
+const params = new URLSearchParams(self.location.search);
+el.src = params.get('script');
+el.addEventListener('load', evt => {
+  runScript();
+});
+document.body.appendChild(el);
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js
new file mode 100644
index 0000000..7e1c598
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js
@@ -0,0 +1,41 @@
+function runScript() {
+  throw new Error("Intentional error.");
+}
+
+function unused() {
+  // The following string is intended to be relatively large since some
+  // browsers trigger different code paths based on script size.
+  return "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a " +
+         "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+         "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+         "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+         "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+         "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+         "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+         "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+         "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+         "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+         "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+         "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+         "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+         "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+         "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+         "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+         "metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+         "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+         "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+         "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+         "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+         "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+         "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+         "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+         "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+         "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+         "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+         "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+         "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+         "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+         "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+         "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+         "metus.";
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js
new file mode 100644
index 0000000..8b89098
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js
@@ -0,0 +1,3 @@
+function runScript() {
+  throw new Error("Intentional error.");
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js
new file mode 100644
index 0000000..4d882c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js
@@ -0,0 +1,37 @@
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+const NAME = 'foo';
+const SAME_ORIGIN_BASE = new URL('./', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./',
+    get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+const urls = [
+  `${SAME_ORIGIN_BASE}opaque-script-small.js`,
+  `${SAME_ORIGIN_BASE}opaque-script-large.js`,
+  `${CROSS_ORIGIN_BASE}opaque-script-small.js`,
+  `${CROSS_ORIGIN_BASE}opaque-script-large.js`,
+];
+
+self.addEventListener('install', evt => {
+  evt.waitUntil(async function() {
+    const c = await caches.open(NAME);
+    const promises = urls.map(async function(u) {
+      const r = await fetch(u, { mode: 'no-cors' });
+      await c.put(u, r);
+    });
+    await Promise.all(promises);
+  }());
+});
+
+self.addEventListener('fetch', evt => {
+  const url = new URL(evt.request.url);
+  if (!url.pathname.includes('opaque-script-small.js') &&
+      !url.pathname.includes('opaque-script-large.js')) {
+    return;
+  }
+  evt.respondWith(async function() {
+    const c = await caches.open(NAME);
+    return c.match(evt.request);
+  }());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/other.html b/third_party/web_platform_tests/service-workers/service-worker/resources/other.html
new file mode 100644
index 0000000..b9f3504
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/other.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Other</title>
+Here's an other html file.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js b/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js
new file mode 100644
index 0000000..835046d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js
@@ -0,0 +1,58 @@
+// .body attribute of Request and Response object are experimental feture. It is
+// enabled when --enable-experimental-web-platform-features flag is set.
+// Touching this attribute can change the behavior of the objects. To avoid
+// touching it while comparing the objects in LayoutTest, we overwrite
+// assert_object_equals method.
+
+(function() {
+  var original_assert_object_equals = self.assert_object_equals;
+  function _brand(object) {
+    return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+  }
+  var assert_request_equals = function(actual, expected, prefix) {
+    if (typeof actual !== 'object') {
+      assert_equals(actual, expected, prefix);
+      return;
+    }
+    assert_true(actual instanceof Request, prefix);
+    assert_true(expected instanceof Request, prefix);
+    assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+    assert_equals(actual.method, expected.method, prefix + '.method');
+    assert_equals(actual.url, expected.url, prefix + '.url');
+    original_assert_object_equals(actual.headers, expected.headers,
+                                  prefix + '.headers');
+    assert_equals(actual.context, expected.context, prefix + '.context');
+    assert_equals(actual.referrer, expected.referrer, prefix + '.referrer');
+    assert_equals(actual.mode, expected.mode, prefix + '.mode');
+    assert_equals(actual.credentials, expected.credentials,
+                  prefix + '.credentials');
+    assert_equals(actual.cache, expected.cache, prefix + '.cache');
+  };
+  var assert_response_equals = function(actual, expected, prefix) {
+    if (typeof actual !== 'object') {
+      assert_equals(actual, expected, prefix);
+      return;
+    }
+    assert_true(actual instanceof Response, prefix);
+    assert_true(expected instanceof Response, prefix);
+    assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+    assert_equals(actual.type, expected.type, prefix + '.type');
+    assert_equals(actual.url, expected.url, prefix + '.url');
+    assert_equals(actual.status, expected.status, prefix + '.status');
+    assert_equals(actual.statusText, expected.statusText,
+                  prefix + '.statusText');
+    original_assert_object_equals(actual.headers, expected.headers,
+                                  prefix + '.headers');
+  };
+  var assert_object_equals = function(actual, expected, description) {
+    var prefix = (description ? description + ': ' : '') + _brand(expected);
+    if (expected instanceof Request) {
+      assert_request_equals(actual, expected, prefix);
+    } else if (expected instanceof Response) {
+      assert_response_equals(actual, expected, prefix);
+    } else {
+      original_assert_object_equals(actual, expected, description);
+    }
+  };
+  self.assert_object_equals = assert_object_equals;
+})();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
new file mode 100644
index 0000000..12b048e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+  <script>
+    // 1p mode will respond to requests for its current controller and
+    // postMessage when its controller changes.
+    async function onLoad1pMode(){
+      self.addEventListener('message', evt => {
+        if(!evt.data)
+          return;
+
+        if (evt.data.type === "get-controller") {
+          window.parent.postMessage({controller: navigator.serviceWorker.controller});
+        }
+      });
+
+      navigator.serviceWorker.addEventListener('controllerchange', evt => {
+        window.parent.postMessage({status: "success", context: "1p"}, '*');
+      });
+    }
+
+    // 3p mode will tell its SW to claim and then postMessage its results
+    // automatically.
+    async function onLoad3pMode() {
+      reg = await setupServiceWorker();
+
+      if(navigator.serviceWorker.controller != null){
+        //This iframe is already under control of a service worker, testing for
+        // a controller change will timeout. Return a failure.
+        window.parent.postMessage({status: "failure", context: "3p"}, '*');
+        return;
+      }
+
+      // Once this client is claimed, let the test know.
+      navigator.serviceWorker.addEventListener('controllerchange', evt => {
+        window.parent.postMessage({status: "success", context: "3p"}, '*');
+      });
+
+      // Trigger the SW to claim.
+      reg.active.postMessage({type: "claim"});
+
+    }
+
+    const request_url = new URL(window.location.href);
+    var url_search = request_url.search.substr(1);
+
+    if(url_search == "1p-mode") {
+      self.addEventListener('load', onLoad1pMode);
+    }
+    else if(url_search == "3p-mode") {
+      self.addEventListener('load', onLoad3pMode);
+    }
+    // Else do nothing.
+  </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
new file mode 100644
index 0000000..d05fef4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Innermost nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Innermost 1p iframe (A2) with 3p ancestor (A1-B-A2-A3): this iframe will
+register a service worker when it loads and then add its own iframe (A3) that
+will attempt to navigate to a url. ServiceWorker will intercept this navigation
+and resolve the ServiceWorker's internal Promise. When
+ThirdPartyStoragePartitioning is enabled, this iframe should be partitioned
+from the main frame and should not share a ServiceWorker.
+<script>
+
+async function onLoad() {
+  // Set-up the ServiceWorker for this iframe, defined in:
+  // service-workers/service-worker/resources/partitioned-utils.js
+  await setupServiceWorker();
+
+  // When the SW's iframe finishes it'll post a message. This forwards
+  // it up to the middle-iframe.
+  self.addEventListener('message', evt => {
+      window.parent.postMessage(evt.data, '*');
+  });
+
+  // Now that we have set up the ServiceWorker, we need it to
+  // intercept a navigation that will resolve its promise.
+  // To do this, we create an additional iframe to send that
+  // navigation request to resolve (`resolve.fakehtml`). If we're
+  // partitioned then there shouldn't be a promise to resolve. Defined
+  // in: service-workers/service-worker/resources/partitioned-storage-sw.js
+  const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?FromNestedFrame', self.location);
+  const frame_resolve = await new Promise(resolve => {
+    var frame = document.createElement('iframe');
+    frame.src = resolve_frame_url;
+    frame.onload = function() { resolve(frame); };
+    document.body.appendChild(frame);
+  });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
new file mode 100644
index 0000000..f748e2f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: Middle nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Middle of the nested iframes (3p ancestor or B in A1-B-A2).
+<script>
+
+async function onLoad() {
+  // The innermost iframe will recieve a message from the
+  // ServiceWorker and pass it to this iframe. We need to
+  // then pass that message to the main frame to complete
+  // the test.
+  self.addEventListener('message', evt => {
+      window.parent.postMessage(evt.data, '*');
+  });
+
+  // Embed the innermost iframe and set-up the service worker there.
+  const innermost_iframe_url = new URL('./partitioned-service-worker-nested-iframe-child.html',
+    get_host_info().HTTPS_ORIGIN + self.location.pathname);
+  var frame = document.createElement('iframe');
+  frame.src = innermost_iframe_url;
+  document.body.appendChild(frame);
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
new file mode 100644
index 0000000..747c058
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+  This iframe will register a service worker when it loads and then will use
+  getRegistrations to get a handle to the SW. It will then postMessage to the
+  SW to retrieve the SW's ID. This iframe will then forward that message up,
+  eventually, to the test.
+  <script>
+
+    async function onLoad() {
+      const scope = './partitioned-'
+      const absoluteScope = new URL(scope, window.location).href;
+
+      await setupServiceWorker();
+
+      // Once the SW sends us its ID, forward it up to the window.
+      navigator.serviceWorker.addEventListener('message', evt => {
+        window.parent.postMessage(evt.data, '*');
+      });
+
+      // Now get the SW with getRegistrations.
+      const retrieved_registrations =
+        await navigator.serviceWorker.getRegistrations();
+
+      // It's possible that other tests have left behind other service workers.
+      // This steps filters those other SWs out.
+      const filtered_registrations =
+        retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+      filtered_registrations[0].active.postMessage({type: "get-id"});
+
+    }
+
+    self.addEventListener('load', onLoad);
+  </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
new file mode 100644
index 0000000..7a2c366
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+  This iframe will register a service worker when it loads and then will use
+  getRegistrations to get a handle to the SW. It will then postMessage to the
+  SW to get the SW's clients via matchAll(). This iframe will then forward the
+  SW's response up, eventually, to the test.
+  <script>
+    async function onLoad() {
+      reg = await setupServiceWorker();
+
+      // Once the SW sends us its ID, forward it up to the window.
+      navigator.serviceWorker.addEventListener('message', evt => {
+        window.parent.postMessage(evt.data, '*');
+      });
+
+      reg.active.postMessage({type: "get-match-all"});
+
+    }
+
+    self.addEventListener('load', onLoad);
+  </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000..1b7f671
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+
+<body>
+This iframe will register a service worker when it loads and then add its own
+iframe that will attempt to navigate to a url that service worker will intercept
+and use to resolve the service worker's internal Promise.
+<script>
+
+async function onLoad() {
+  await setupServiceWorker();
+
+  // When the SW's iframe finishes it'll post a message. This forwards it up to
+  // the window.
+  self.addEventListener('message', evt => {
+      window.parent.postMessage(evt.data, '*');
+  });
+
+  // Now try to resolve the SW's promise. If we're partitioned then there
+  // shouldn't be a promise to resolve.
+  const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location);
+  const frame_resolve = await new Promise(resolve => {
+    var frame = document.createElement('iframe');
+    frame.src = resolve_frame_url;
+    frame.onload = function() { resolve(frame); };
+    document.body.appendChild(frame);
+  });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000..86384ce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P window for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This page should be opened as a third-party window. It then loads an iframe
+specified by the query parameter. Finally it forwards the postMessage from the
+iframe up to the opener (the test).
+
+<script>
+
+async function onLoad() {
+  const message_promise = new Promise(resolve => {
+    self.addEventListener('message', evt => {
+      resolve(evt.data);
+    });
+  });
+
+  const search_param = new URLSearchParams(window.location.search);
+  const iframe_url = search_param.get('target');
+
+  var frame = document.createElement('iframe');
+  frame.src = iframe_url;
+  frame.style.position = 'absolute';
+  document.body.appendChild(frame);
+
+
+  await message_promise.then(data => {
+    // We're done, forward the message and clean up.
+    window.opener.postMessage(data, '*');
+
+    frame.remove();
+  });
+}
+
+self.addEventListener('load', onLoad);
+
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000..00f7979
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js
@@ -0,0 +1,81 @@
+// Holds the promise that the "resolve.fakehtml" call attempts to resolve.
+// This is "the SW's promise" that other parts of the test refer to.
+var promise;
+// Stores the resolve funcution for the current promise.
+var pending_resolve_func = null;
+// Unique ID to determine which service worker is being used.
+const ID = Math.random();
+
+function callAndResetResolve() {
+  var local_resolve = pending_resolve_func;
+  pending_resolve_func = null;
+  local_resolve();
+}
+
+self.addEventListener('fetch', function(event) {
+  fetchEventHandler(event);
+})
+
+self.addEventListener('message', (event) => {
+  event.waitUntil(async function() {
+    if(!event.data)
+      return;
+
+    if (event.data.type === "get-id") {
+      event.source.postMessage({ID: ID});
+    }
+    else if(event.data.type === "get-match-all") {
+      clients.matchAll({includeUncontrolled: true}).then(clients_list => {
+        const url_list = clients_list.map(item => item.url);
+        event.source.postMessage({urls_list: url_list});
+      });
+    }
+    else if(event.data.type === "claim") {
+      await clients.claim();
+    }
+  }());
+});
+
+async function fetchEventHandler(event){
+  var request_url = new URL(event.request.url);
+  var url_search = request_url.search.substr(1);
+  request_url.search = "";
+  if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) {
+
+      if (pending_resolve_func != null) {
+        // Respond with an error if there is already a pending promise
+        event.respondWith(Response.error());
+        return;
+      }
+
+      // Create the new promise.
+      promise = new Promise(function(resolve) {
+        pending_resolve_func = resolve;
+      });
+      event.waitUntil(promise);
+
+      event.respondWith(new Response(`
+        <html>
+        Promise created by ${url_search}
+        <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}"
+          }, '*');</script>
+        </html>
+        `, {headers: {'Content-Type': 'text/html'}}
+      ));
+
+  }
+  else if ( request_url.href.endsWith('resolve.fakehtml') ) {
+    var has_pending = !!pending_resolve_func;
+    event.respondWith(new Response(`
+      <html>
+      Promise settled for ${url_search}
+      <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending},
+        source: "${url_search}"  }, '*');</script>
+      </html>
+    `, {headers: {'Content-Type': 'text/html'}}));
+
+    if (has_pending) {
+      callAndResetResolve();
+    }
+  }
+}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js
new file mode 100644
index 0000000..22e90be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js
@@ -0,0 +1,110 @@
+// The resolve function for the current pending event listener's promise.
+// It is nulled once the promise is resolved.
+var message_event_promise_resolve = null;
+
+function messageEventHandler(evt) {
+  if (message_event_promise_resolve) {
+    local_resolve = message_event_promise_resolve;
+    message_event_promise_resolve = null;
+    local_resolve(evt.data);
+  }
+}
+
+function makeMessagePromise() {
+  if (message_event_promise_resolve != null) {
+    // Do not create a new promise until the previous is settled.
+    return;
+  }
+
+  return new Promise(resolve => {
+    message_event_promise_resolve = resolve;
+  });
+}
+
+// Loads a url for the frame type and then returns a promise for
+// the data that was postMessage'd from the loaded frame.
+// If the frame type is 'window' then `url` is encoded into the search param
+// as the url the 3p window is meant to iframe.
+function loadAndReturnSwData(t, url, frame_type) {
+  if (frame_type !== 'iframe' && frame_type !== 'window') {
+    return;
+  }
+
+  const message_promise = makeMessagePromise();
+
+  // Create the iframe or window and then return the promise for data.
+  if ( frame_type === 'iframe' ) {
+    const frame = with_iframe(url, false);
+    t.add_cleanup(async () => {
+      const f = await frame;
+      f.remove();
+    });
+  }
+  else {
+    // 'window' case.
+    const search_param = new URLSearchParams();
+    search_param.append('target', url);
+
+    const third_party_window_url = new URL(
+    './resources/partitioned-service-worker-third-party-window.html' +
+    '?' + search_param,
+    get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+    const w = window.open(third_party_window_url);
+    t.add_cleanup(() => w.close());
+  }
+
+  return message_promise;
+}
+
+// Checks for an existing service worker registration. If not present,
+// registers and maintains a service worker. Used in windows or iframes
+// that will be partitioned from the main frame.
+async function setupServiceWorker() {
+
+  const script = './partitioned-storage-sw.js';
+  const scope = './partitioned-';
+
+  var reg = await navigator.serviceWorker.register(script, { scope: scope });
+
+  // We should keep track if we installed a worker or not. If we did then we
+  // need to uninstall it. Otherwise we let the top level test uninstall it
+  // (If partitioning is not working).
+  var installed_a_worker = true;
+  await new Promise(resolve => {
+    // Check if a worker is already activated.
+    var worker = reg.active;
+    // If so, just resolve.
+    if ( worker ) {
+      installed_a_worker = false;
+      resolve();
+      return;
+    }
+
+    //Otherwise check if one is waiting.
+    worker = reg.waiting;
+    // If not waiting, grab the installing worker.
+    if ( !worker ) {
+      worker = reg.installing;
+    }
+
+    // Resolve once it's activated.
+    worker.addEventListener('statechange', evt => {
+      if (worker.state === 'activated') {
+        resolve();
+      }
+    });
+  });
+
+  self.addEventListener('unload', async () => {
+    // If we didn't install a worker then that means the top level test did, and
+    // that test is therefore responsible for cleaning it up.
+    if ( !installed_a_worker ) {
+        return;
+    }
+
+    await reg.unregister();
+  });
+
+  return reg;
+}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js
new file mode 100644
index 0000000..5eaf48d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js
@@ -0,0 +1,3 @@
+addEventListener('fetch', evt => {
+  evt.respondWith(fetch(evt.request));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt
new file mode 100644
index 0000000..7ef22e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt
@@ -0,0 +1 @@
+PASS
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js
new file mode 100644
index 0000000..6c6dfcb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js
@@ -0,0 +1,62 @@
+importScripts('/resources/testharness.js');
+
+promise_test(function(test) {
+    var durationMsec = 100;
+    // There are limits to our accuracy here.  Timers may fire up to a
+    // millisecond early due to platform-dependent rounding.  In addition
+    // the performance API introduces some rounding as well to prevent
+    // timing attacks.
+    var accuracy = 1.5;
+    return new Promise(function(resolve) {
+        performance.mark('startMark');
+        setTimeout(resolve, durationMsec);
+      }).then(function() {
+          performance.mark('endMark');
+          performance.measure('measure', 'startMark', 'endMark');
+          var startMark = performance.getEntriesByName('startMark')[0];
+          var endMark = performance.getEntriesByName('endMark')[0];
+          var measure = performance.getEntriesByType('measure')[0];
+          assert_equals(measure.startTime, startMark.startTime);
+          assert_approx_equals(endMark.startTime - startMark.startTime,
+                               measure.duration, 0.001);
+          assert_greater_than(measure.duration, durationMsec - accuracy);
+          assert_equals(performance.getEntriesByType('mark').length, 2);
+          assert_equals(performance.getEntriesByType('measure').length, 1);
+          performance.clearMarks('startMark');
+          performance.clearMeasures('measure');
+          assert_equals(performance.getEntriesByType('mark').length, 1);
+          assert_equals(performance.getEntriesByType('measure').length, 0);
+      });
+  }, 'User Timing');
+
+promise_test(function(test) {
+    return fetch('sample.txt')
+      .then(function(resp) {
+          return resp.text();
+        })
+      .then(function(text) {
+          var expectedResources = ['testharness.js', 'sample.txt'];
+          assert_equals(performance.getEntriesByType('resource').length, expectedResources.length);
+          for (var i = 0; i < expectedResources.length; i++) {
+              var entry = performance.getEntriesByType('resource')[i];
+              assert_true(entry.name.endsWith(expectedResources[i]));
+              assert_equals(entry.workerStart, 0);
+              assert_greater_than(entry.startTime, 0);
+              assert_greater_than(entry.responseEnd, entry.startTime);
+          }
+          return new Promise(function(resolve) {
+              performance.onresourcetimingbufferfull = _ => {
+                resolve('bufferfull');
+              }
+              performance.setResourceTimingBufferSize(expectedResources.length);
+              fetch('sample.txt');
+          });
+        })
+      .then(function(result) {
+          assert_equals(result, 'bufferfull');
+          performance.clearResourceTimings();
+          assert_equals(performance.getEntriesByType('resource').length, 0);
+        })
+  }, 'Resource Timing');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js
new file mode 100644
index 0000000..9095194
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js
@@ -0,0 +1,5 @@
+self.onmessage = e => {
+  fetch(e.data)
+  .then(response => response.text())
+  .then(text => e.source.postMessage('Worker reply:' + text));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
new file mode 100644
index 0000000..87a4500
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+    var text_decoder = new TextDecoder;
+    port.postMessage({
+      content: text_decoder.decode(e.data),
+      byteLength: e.data.byteLength
+    });
+
+    // Send back the array buffer via Client.postMessage.
+    port.postMessage(e.data, {transfer: [e.data.buffer]});
+
+    port.postMessage({
+      content: text_decoder.decode(e.data),
+      byteLength: e.data.byteLength
+    });
+};
+
+self.addEventListener('message', e => {
+    if (e.ports[0]) {
+      // Wait for messages sent via MessagePort.
+      e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+      return;
+    }
+    messageHandler(e.source, e);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js
new file mode 100644
index 0000000..f088ad1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('message', event => {
+  event.source.postMessage(event.data);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js
new file mode 100644
index 0000000..9fc6717
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js
@@ -0,0 +1,5 @@
+self.onmessage = async (e) => {
+  const response = await fetch(e.data);
+  const text = await response.text();
+  self.postMessage(text);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
new file mode 100644
index 0000000..7af935f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
@@ -0,0 +1,19 @@
+self.onmessage = function(e) {
+  e.waitUntil(self.clients.matchAll().then(function(clients) {
+      clients.forEach(function(client) {
+          var messageChannel = new MessageChannel();
+          messageChannel.port1.onmessage =
+            onMessageViaMessagePort.bind(null, messageChannel.port1);
+          client.postMessage(undefined, [messageChannel.port2]);
+        });
+    }));
+};
+
+function onMessageViaMessagePort(port, e) {
+  var message = e.data;
+  if ('value' in message) {
+    port.postMessage({ack: 'Acking value: ' + message.value});
+  } else if ('done' in message) {
+    port.postMessage({done: true});
+  }
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
new file mode 100644
index 0000000..c2b0bcb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
@@ -0,0 +1,9 @@
+if ('DedicatedWorkerGlobalScope' in self &&
+    self instanceof DedicatedWorkerGlobalScope) {
+  postMessage('dedicated worker script loaded');
+} else if ('SharedWorkerGlobalScope' in self &&
+    self instanceof SharedWorkerGlobalScope) {
+  self.onconnect = evt => {
+    evt.ports[0].postMessage('shared worker script loaded');
+  };
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
new file mode 100644
index 0000000..1791306
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
@@ -0,0 +1,10 @@
+self.onmessage = function(e) {
+  e.waitUntil(self.clients.matchAll().then(function(clients) {
+      clients.forEach(function(client) {
+          client.postMessage('Sending message via clients');
+          if (!Array.isArray(clients))
+            client.postMessage('clients is not an array');
+          client.postMessage('quit');
+        });
+    }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
new file mode 100644
index 0000000..d35c1c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+    var text_decoder = new TextDecoder;
+    port.postMessage({
+      content: text_decoder.decode(e.data),
+      byteLength: e.data.byteLength
+    });
+
+    // Send back the array buffer via Client.postMessage.
+    port.postMessage(e.data, [e.data.buffer]);
+
+    port.postMessage({
+      content: text_decoder.decode(e.data),
+      byteLength: e.data.byteLength
+    });
+};
+
+self.addEventListener('message', e => {
+    if (e.ports[0]) {
+      // Wait for messages sent via MessagePort.
+      e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+      return;
+    }
+    messageHandler(e.source, e);
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js
new file mode 100644
index 0000000..858cf04
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js
@@ -0,0 +1,19 @@
+var port;
+
+// Exercise the 'onmessage' handler:
+self.onmessage = function(e) {
+  var message = e.data;
+  if ('port' in message) {
+    port = message.port;
+  }
+};
+
+// And an event listener:
+self.addEventListener('message', function(e) {
+    var message = e.data;
+    if ('value' in message) {
+      port.postMessage('Acking value: ' + message.value);
+    } else if ('done' in message) {
+      port.postMessage('quit');
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
new file mode 100644
index 0000000..cab6058
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
@@ -0,0 +1,40 @@
+// This worker is meant to test range requests where the responses come from
+// multiple origins. It forwards the first request to a cross-origin URL
+// (generating an opaque response). The server is expected to return a 206
+// Partial Content response.  Then the worker lets subsequent range requests
+// fall back to network (generating same-origin responses). The intent is to try
+// to trick the browser into treating the resource as same-origin.
+//
+// It would also be interesting to do the reverse test where the first request
+// goes to the same-origin URL, and subsequent range requests go cross-origin in
+// 'no-cors' mode to receive opaque responses. But the service worker cannot do
+// this, because in 'no-cors' mode the 'range' HTTP header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+  const old = initial;
+  initial = false;
+  return old;
+}
+
+self.addEventListener('fetch', e => {
+    const url = new URL(e.request.url);
+    if (url.search.indexOf('VIDEO') == -1) {
+      // Fall back for non-video.
+      return;
+    }
+
+    // Make the first request go cross-origin.
+    if (is_initial_request()) {
+      const cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+          url.pathname + url.search;
+      const cross_origin_request = new Request(cross_origin_url,
+          {mode: 'no-cors', headers: e.request.headers});
+      e.respondWith(fetch(cross_origin_request));
+      return;
+    }
+
+    // Fall back to same origin for subsequent range requests.
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
new file mode 100644
index 0000000..7580b0b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
@@ -0,0 +1,60 @@
+// This worker is meant to test range requests where the responses are a mix of
+// opaque ones and non-opaque ones. It forwards the first request to a
+// cross-origin URL (generating an opaque response). The server is expected to
+// return a 206 Partial Content response.  Then the worker forwards subsequent
+// range requests to that URL, with CORS sharing generating a non-opaque
+// responses. The intent is to try to trick the browser into treating the
+// resource as non-opaque.
+//
+// It would also be interesting to do the reverse test where the first request
+// uses 'cors', and subsequent range requests use 'no-cors' mode. But the
+// service worker cannot do this, because in 'no-cors' mode the 'range' HTTP
+// header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+  const old = initial;
+  initial = false;
+  return old;
+}
+
+self.addEventListener('fetch', e => {
+    const url = new URL(e.request.url);
+    if (url.search.indexOf('VIDEO') == -1) {
+      // Fall back for non-video.
+      return;
+    }
+
+    let cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+        url.pathname + url.search;
+
+    // The first request is no-cors.
+    if (is_initial_request()) {
+      const init = { mode: 'no-cors', headers: e.request.headers };
+      const cross_origin_request = new Request(cross_origin_url, init);
+      e.respondWith(fetch(cross_origin_request));
+      return;
+    }
+
+    // Subsequent range requests are cors.
+
+    // Copy headers needed for range requests.
+    let my_headers = new Headers;
+    if (e.request.headers.get('accept'))
+      my_headers.append('accept', e.request.headers.get('accept'));
+    if (e.request.headers.get('range'))
+    my_headers.append('range', e.request.headers.get('range'));
+
+    // Add &ACAOrigin to allow CORS.
+    cross_origin_url += '&ACAOrigin=' + get_host_info().HTTPS_ORIGIN;
+    // Add &ACAHeaders to allow range requests.
+    cross_origin_url += '&ACAHeaders=accept,range';
+
+    // Make the CORS request.
+    const init = { mode: 'cors', headers: my_headers };
+    const cross_origin_request = new Request(cross_origin_url, init);
+    e.respondWith(fetch(cross_origin_request));
+  });
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js
new file mode 100644
index 0000000..82e21fc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js
@@ -0,0 +1,145 @@
+// We store an empty response for each fetch event request we see
+// in this Cache object so we can get the list of urls in the
+// message event.
+var cacheName = 'urls-' + self.registration.scope;
+
+var waitUntilPromiseList = [];
+
+// Sends the requests seen by this worker. The output is:
+// {
+//   requestInfos: [
+//     {url: url1, resultingClientId: id1},
+//     {url: url2, resultingClientId: id2},
+//   ]
+// }
+async function getRequestInfos(event) {
+  // Wait for fetch events to finish.
+  await Promise.all(waitUntilPromiseList);
+  waitUntilPromiseList = [];
+
+  // Generate the message.
+  const cache = await caches.open(cacheName);
+  const requestList = await cache.keys();
+  const requestInfos = [];
+  for (let i = 0; i < requestList.length; i++) {
+    const response = await cache.match(requestList[i]);
+    const body = await response.json();
+    requestInfos[i] = {
+      url: requestList[i].url,
+      resultingClientId: body.resultingClientId
+    };
+  }
+  await caches.delete(cacheName);
+
+  event.data.port.postMessage({requestInfos});
+}
+
+// Sends the results of clients.get(id) from this worker. The
+// input is:
+// {
+//   actual_ids: {a: id1, b: id2, x: id3}
+// }
+//
+// The output is:
+// {
+//   clients: {
+//     a: {found: false},
+//     b: {found: false},
+//     x: {
+//       id: id3,
+//       url: url1,
+//       found: true
+//    }
+//   }
+// }
+async function getClients(event) {
+  // |actual_ids| is like:
+  // {a: id1, b: id2, x: id3}
+  const actual_ids = event.data.actual_ids;
+  const result = {}
+  for (let key of Object.keys(actual_ids)) {
+    const id = actual_ids[key];
+    const client = await self.clients.get(id);
+    if (client === undefined)
+      result[key] = {found: false};
+    else
+      result[key] = {found: true, url: client.url, id: client.id};
+  }
+  event.data.port.postMessage({clients: result});
+}
+
+self.addEventListener('message', async function(event) {
+  if (event.data.command == 'getRequestInfos') {
+    event.waitUntil(getRequestInfos(event));
+    return;
+  }
+
+  if (event.data.command == 'getClients') {
+    event.waitUntil(getClients(event));
+    return;
+  }
+});
+
+function get_query_params(url) {
+  var search = (new URL(url)).search;
+  if (!search) {
+    return {};
+  }
+  var ret = {};
+  var params = search.substring(1).split('&');
+  params.forEach(function(param) {
+      var element = param.split('=');
+      ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+    });
+  return ret;
+}
+
+self.addEventListener('fetch', function(event) {
+    var waitUntilPromise = caches.open(cacheName).then(function(cache) {
+      const responseBody = {};
+      responseBody['resultingClientId'] = event.resultingClientId;
+      const headers = new Headers({'Content-Type': 'application/json'});
+      const response = new Response(JSON.stringify(responseBody), {headers});
+      return cache.put(event.request, response);
+    });
+    event.waitUntil(waitUntilPromise);
+
+    var params = get_query_params(event.request.url);
+    if (!params['sw']) {
+      // To avoid races, add the waitUntil() promise to our global list.
+      // If we get a message event before we finish here, it will wait
+      // these promises to complete before proceeding to read from the
+      // cache.
+      waitUntilPromiseList.push(waitUntilPromise);
+      return;
+    }
+
+    event.respondWith(waitUntilPromise.then(async () => {
+      if (params['sw'] == 'gen') {
+        return Response.redirect(params['url']);
+      } else if (params['sw'] == 'gen-manual') {
+        // Note this differs from Response.redirect() in that relative URLs are
+        // preserved.
+        return new Response("", {
+          status: 301,
+          headers: {location: params['url']},
+        });
+      } else if (params['sw'] == 'fetch') {
+        return fetch(event.request);
+      } else if (params['sw'] == 'fetch-url') {
+        return fetch(params['url']);
+      } else if (params['sw'] == 'follow') {
+        return fetch(new Request(event.request.url, {redirect: 'follow'}));
+      } else if (params['sw'] == 'manual') {
+        return fetch(new Request(event.request.url, {redirect: 'manual'}));
+      } else if (params['sw'] == 'manualThroughCache') {
+        const url = event.request.url;
+        await caches.delete(url)
+        const cache = await self.caches.open(url);
+        const response = await fetch(new Request(url, {redirect: 'manual'}));
+        await cache.put(event.request, response);
+        return cache.match(url);
+      }
+      // unexpected... trigger an interception failure
+    }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py
new file mode 100644
index 0000000..bd559d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py
@@ -0,0 +1,27 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+    if b'Status' in request.GET:
+        status = int(request.GET[b"Status"])
+    else:
+        status = 302
+
+    headers = []
+
+    url = isomorphic_decode(request.GET[b'Redirect'])
+    headers.append((b"Location", url))
+
+    if b"ACAOrigin" in request.GET:
+        for item in request.GET[b"ACAOrigin"].split(b","):
+            headers.append((b"Access-Control-Allow-Origin", item))
+
+    for suffix in [b"Headers", b"Methods", b"Credentials"]:
+        query = b"ACA%s" % suffix
+        header = b"Access-Control-Allow-%s" % suffix
+        if query in request.GET:
+            headers.append((header, request.GET[query]))
+
+    if b"ACEHeaders" in request.GET:
+        headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+    return status, headers, b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html
new file mode 100644
index 0000000..295ff45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html
@@ -0,0 +1,39 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+  return fetch(url)
+    .then(function(res) { return res.json(); })
+    .then(function(headers) {
+        if (headers['referer'] === expected_referer) {
+          return Promise.resolve();
+        } else {
+          return Promise.reject('Referer for ' + url + ' must be ' +
+                                expected_referer + ' but got ' +
+                                headers['referer']);
+        }
+      });
+}
+
+window.addEventListener('message', function(evt) {
+    var host_info = get_host_info();
+    var port = evt.ports[0];
+    check_referer('request-headers.py?ignore=true',
+                  host_info['HTTPS_ORIGIN'] +
+                  base_path() + 'referer-iframe.html')
+      .then(function() {
+          return check_referer(
+              'request-headers.py',
+              host_info['HTTPS_ORIGIN'] +
+              base_path() + 'referer-iframe.html');
+        })
+      .then(function() {
+          return check_referer(
+              'request-headers.py?url=request-headers.py',
+              host_info['HTTPS_ORIGIN'] +
+              base_path() + 'fetch-rewrite-worker.js');
+        })
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html
new file mode 100644
index 0000000..9ef3cd1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html
@@ -0,0 +1,32 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+  return fetch(url)
+    .then(function(res) { return res.json(); })
+    .then(function(headers) {
+        if (headers['referer'] === expected_referer) {
+          return Promise.resolve();
+        } else {
+          return Promise.reject('Referer for ' + url + ' must be ' +
+                                expected_referer + ' but got ' +
+                                headers['referer']);
+        }
+      });
+}
+
+window.addEventListener('message', function(evt) {
+    var host_info = get_host_info();
+    var port = evt.ports[0];
+    check_referer('request-headers.py?ignore=true',
+                  host_info['HTTPS_ORIGIN'] +
+                  base_path() + 'referrer-policy-iframe.html')
+      .then(function() {
+          return check_referer(
+              'request-headers.py?url=request-headers.py',
+              host_info['HTTPS_ORIGIN'] + '/');
+        })
+      .then(function() { port.postMessage({results: 'finish'}); })
+      .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html
new file mode 100644
index 0000000..117f254
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<script>
+window.addEventListener('message', async function(evt) {
+  if (evt.data === 'START') {
+    var w = window.open('./');
+    var sw = w.navigator.serviceWorker;
+    w.close();
+    w = null;
+    try {
+      await sw.register('doesntmatter.js');
+    } finally {
+      parent.postMessage('OK', '*');
+    }
+  }
+});
+</script>
+</head>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html
new file mode 100644
index 0000000..f5a040e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html
@@ -0,0 +1,4 @@
+<script type="text/javascript">
+navigator.serviceWorker.register('empty-worker.js',
+                                 {scope: 'register-iframe.html'});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html
new file mode 100644
index 0000000..bf06317
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+  const params = new URLSearchParams(self.location.search);
+  const scope = self.origin + params.get('scopepath');
+  const script = './fetch-rewrite-worker.js';
+  const reg = await navigator.serviceWorker.register(script, { scope: scope });
+  // In nested cases we may be impacted by partitioning or not depending on
+  // the browser.  With partitioning we will be installing a new worker here,
+  // but without partitioning the worker will already exist.  Handle both cases.
+  if (reg.installing) {
+    await new Promise(resolve => {
+      const worker = reg.installing;
+      worker.addEventListener('statechange', evt => {
+        if (worker.state === 'activated') {
+          resolve();
+        }
+      });
+    });
+    if (reg.navigationPreload) {
+      await reg.navigationPreload.enable();
+    }
+  }
+  if (window.opener) {
+    window.opener.postMessage({ type: 'SW-REGISTERED' }, '*');
+  } else {
+    window.top.postMessage({ type: 'SW-REGISTERED' }, '*');
+  }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js
new file mode 100644
index 0000000..037e6c0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js
@@ -0,0 +1,96 @@
+// Registration tests that mostly verify the MIME type.
+//
+// This file tests every MIME type so it necessarily starts many service
+// workers, so it may be slow.
+function registration_tests_mime_types(register_method) {
+  promise_test(function(t) {
+      var script = 'resources/mime-type-worker.py';
+      var scope = 'resources/scope/no-mime-type-worker/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration of no MIME type script should fail.');
+    }, 'Registering script with no MIME type');
+
+  promise_test(function(t) {
+      var script = 'resources/mime-type-worker.py?mime=text/plain';
+      var scope = 'resources/scope/bad-mime-type-worker/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration of plain text script should fail.');
+    }, 'Registering script with bad MIME type');
+
+  /**
+   * ServiceWorkerContainer.register() should throw a TypeError, according to
+   * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts
+   *
+   * "[17] If an uncaught runtime script error occurs during the above step, then:
+   *  [17.1] Invoke Reject Job Promise with job and TypeError"
+   *
+   * (Where the "uncaught runtime script error" is thrown by an unsuccessful
+   * importScripts())
+   */
+  promise_test(function(t) {
+      var script = 'resources/import-mime-type-worker.py';
+      var scope = 'resources/scope/no-mime-type-worker/';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of no MIME type imported script should fail.');
+    }, 'Registering script that imports script with no MIME type');
+
+  promise_test(function(t) {
+      var script = 'resources/import-mime-type-worker.py?mime=text/plain';
+      var scope = 'resources/scope/bad-mime-type-worker/';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of plain text imported script should fail.');
+    }, 'Registering script that imports script with bad MIME type');
+
+  const validMimeTypes = [
+    'application/ecmascript',
+    'application/javascript',
+    'application/x-ecmascript',
+    'application/x-javascript',
+    'text/ecmascript',
+    'text/javascript',
+    'text/javascript1.0',
+    'text/javascript1.1',
+    'text/javascript1.2',
+    'text/javascript1.3',
+    'text/javascript1.4',
+    'text/javascript1.5',
+    'text/jscript',
+    'text/livescript',
+    'text/x-ecmascript',
+    'text/x-javascript'
+  ];
+
+  for (const validMimeType of validMimeTypes) {
+    promise_test(() => {
+      var script = `resources/mime-type-worker.py?mime=${validMimeType}`;
+      var scope = 'resources/scope/good-mime-type-worker/';
+
+      return register_method(script, {scope}).then(registration => {
+        assert_true(
+          registration instanceof ServiceWorkerRegistration,
+          'Successfully registered.');
+        return registration.unregister();
+      });
+    }, `Registering script with good MIME type ${validMimeType}`);
+
+    promise_test(() => {
+      var script = `resources/import-mime-type-worker.py?mime=${validMimeType}`;
+      var scope = 'resources/scope/good-mime-type-worker/';
+
+      return register_method(script, { scope }).then(registration => {
+        assert_true(
+          registration instanceof ServiceWorkerRegistration,
+          'Successfully registered.');
+        return registration.unregister();
+      });
+    }, `Registering script that imports script with good MIME type ${validMimeType}`);
+  }
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js
new file mode 100644
index 0000000..30c424b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js
@@ -0,0 +1,120 @@
+// Registration tests that mostly exercise the scope option.
+function registration_tests_scope(register_method) {
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/scope%2fencoded-slash-in-scope';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'URL-encoded slash in the scope should be rejected.');
+    }, 'Scope including URL-encoded slash');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/scope%5cencoded-slash-in-scope';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'URL-encoded backslash in the scope should be rejected.');
+    }, 'Scope including URL-encoded backslash');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'data:text/html,';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'scope URL scheme is not "http" or "https"');
+    }, 'Scope URL scheme is a data: URL');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = new URL('resources', location).href.replace('https:', 'ftp:');
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'scope URL scheme is not "http" or "https"');
+    }, 'Scope URL scheme is an ftp: URL');
+
+  promise_test(function(t) {
+      // URL-encoded full-width 'scope'.
+      var name = '%ef%bd%93%ef%bd%83%ef%bd%8f%ef%bd%90%ef%bd%85';
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/' + name + '/escaped-multibyte-character-scope';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              registration.scope,
+              normalizeURL(scope),
+              'URL-encoded multibyte characters should be available.');
+            return registration.unregister();
+          });
+    }, 'Scope including URL-encoded multibyte characters');
+
+  promise_test(function(t) {
+      // Non-URL-encoded full-width "scope".
+      var name = String.fromCodePoint(0xff53, 0xff43, 0xff4f, 0xff50, 0xff45);
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/' + name  + '/non-escaped-multibyte-character-scope';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              registration.scope,
+              normalizeURL(scope),
+              'Non-URL-encoded multibyte characters should be available.');
+            return registration.unregister();
+          });
+    }, 'Scope including non-escaped multibyte characters');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/././scope/self-reference-in-scope';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              registration.scope,
+              normalizeURL('resources/scope/self-reference-in-scope'),
+              'Scope including self-reference should be normalized.');
+            return registration.unregister();
+          });
+    }, 'Scope including self-reference');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/../resources/scope/parent-reference-in-scope';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              registration.scope,
+              normalizeURL('resources/scope/parent-reference-in-scope'),
+              'Scope including parent-reference should be normalized.');
+            return registration.unregister();
+          });
+    }, 'Scope including parent-reference');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/scope////consecutive-slashes-in-scope';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            // Although consecutive slashes in the scope are not unified, the
+            // scope is under the script directory and registration should
+            // succeed.
+            assert_equals(
+              registration.scope,
+              normalizeURL(scope),
+              'Should successfully be registered.');
+            return registration.unregister();
+          })
+    }, 'Scope including consecutive slashes');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'filesystem:' + normalizeURL('resources/scope/filesystem-scope-url');
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registering with the scope that has same-origin filesystem: URL ' +
+              'should fail with TypeError.');
+    }, 'Scope URL is same-origin filesystem: URL');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js
new file mode 100644
index 0000000..55cbe6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js
@@ -0,0 +1,82 @@
+// Registration tests that mostly exercise the scriptURL parameter.
+function registration_tests_script_url(register_method) {
+  promise_test(function(t) {
+        var script = 'resources%2fempty-worker.js';
+        var scope = 'resources/scope/encoded-slash-in-script-url';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'URL-encoded slash in the script URL should be rejected.');
+      }, 'Script URL including URL-encoded slash');
+
+  promise_test(function(t) {
+      var script = 'resources%2Fempty-worker.js';
+      var scope = 'resources/scope/encoded-slash-in-script-url';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'URL-encoded slash in the script URL should be rejected.');
+    }, 'Script URL including uppercase URL-encoded slash');
+
+  promise_test(function(t) {
+      var script = 'resources%5cempty-worker.js';
+      var scope = 'resources/scope/encoded-slash-in-script-url';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'URL-encoded backslash in the script URL should be rejected.');
+    }, 'Script URL including URL-encoded backslash');
+
+  promise_test(function(t) {
+      var script = 'resources%5Cempty-worker.js';
+      var scope = 'resources/scope/encoded-slash-in-script-url';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'URL-encoded backslash in the script URL should be rejected.');
+    }, 'Script URL including uppercase URL-encoded backslash');
+
+  promise_test(function(t) {
+      var script = 'data:application/javascript,';
+      var scope = 'resources/scope/data-url-in-script-url';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Data URLs should not be registered as service workers.');
+    }, 'Script URL is a data URL');
+
+  promise_test(function(t) {
+      var script = 'data:application/javascript,';
+      var scope = new URL('resources/scope/data-url-in-script-url', location);
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Data URLs should not be registered as service workers.');
+    }, 'Script URL is a data URL and scope URL is not relative');
+
+  promise_test(function(t) {
+      var script = 'resources/././empty-worker.js';
+      var scope = 'resources/scope/parent-reference-in-script-url';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              get_newest_worker(registration).scriptURL,
+              normalizeURL('resources/empty-worker.js'),
+              'Script URL including self-reference should be normalized.');
+            return registration.unregister();
+          });
+    }, 'Script URL including self-reference');
+
+  promise_test(function(t) {
+      var script = 'resources/../resources/empty-worker.js';
+      var scope = 'resources/scope/parent-reference-in-script-url';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_equals(
+              get_newest_worker(registration).scriptURL,
+              normalizeURL('resources/empty-worker.js'),
+              'Script URL including parent-reference should be normalized.');
+            return registration.unregister();
+          });
+    }, 'Script URL including parent-reference');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js
new file mode 100644
index 0000000..e5bdaf4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js
@@ -0,0 +1,121 @@
+// Registration tests that mostly exercise the service worker script contents or
+// response.
+function registration_tests_script(register_method, type) {
+  promise_test(function(t) {
+      var script = 'resources/invalid-chunked-encoding.py';
+      var scope = 'resources/scope/invalid-chunked-encoding/';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of invalid chunked encoding script should fail.');
+    }, 'Registering invalid chunked encoding script');
+
+  promise_test(function(t) {
+      var script = 'resources/invalid-chunked-encoding-with-flush.py';
+      var scope = 'resources/scope/invalid-chunked-encoding-with-flush/';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of invalid chunked encoding script should fail.');
+    }, 'Registering invalid chunked encoding script with flush');
+
+  promise_test(function(t) {
+      var script = 'resources/malformed-worker.py?parse-error';
+      var scope = 'resources/scope/parse-error';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of script including parse error should fail.');
+    }, 'Registering script including parse error');
+
+  promise_test(function(t) {
+      var script = 'resources/malformed-worker.py?undefined-error';
+      var scope = 'resources/scope/undefined-error';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of script including undefined error should fail.');
+    }, 'Registering script including undefined error');
+
+  promise_test(function(t) {
+      var script = 'resources/malformed-worker.py?uncaught-exception';
+      var scope = 'resources/scope/uncaught-exception';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of script including uncaught exception should fail.');
+    }, 'Registering script including uncaught exception');
+
+  if (type === 'classic') {
+    promise_test(function(t) {
+        var script = 'resources/malformed-worker.py?import-malformed-script';
+        var scope = 'resources/scope/import-malformed-script';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'Registration of script importing malformed script should fail.');
+      }, 'Registering script importing malformed script');
+  }
+
+  if (type === 'module') {
+    promise_test(function(t) {
+        var script = 'resources/malformed-worker.py?top-level-await';
+        var scope = 'resources/scope/top-level-await';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'Registration of script with top-level await should fail.');
+      }, 'Registering script with top-level await');
+
+    promise_test(function(t) {
+        var script = 'resources/malformed-worker.py?instantiation-error';
+        var scope = 'resources/scope/instantiation-error';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'Registration of script with module instantiation error should fail.');
+      }, 'Registering script with module instantiation error');
+
+    promise_test(function(t) {
+        var script = 'resources/malformed-worker.py?instantiation-error-and-top-level-await';
+        var scope = 'resources/scope/instantiation-error-and-top-level-await';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'Registration of script with module instantiation error and top-level await should fail.');
+      }, 'Registering script with module instantiation error and top-level await');
+  }
+
+  promise_test(function(t) {
+      var script = 'resources/no-such-worker.js';
+      var scope = 'resources/scope/no-such-worker';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registration of non-existent script should fail.');
+    }, 'Registering non-existent script');
+
+  if (type === 'classic') {
+    promise_test(function(t) {
+        var script = 'resources/malformed-worker.py?import-no-such-script';
+        var scope = 'resources/scope/import-no-such-script';
+        return promise_rejects_js(t,
+            TypeError,
+            register_method(script, {scope: scope}),
+            'Registration of script importing non-existent script should fail.');
+      }, 'Registering script importing non-existent script');
+  }
+
+  promise_test(function(t) {
+      var script = 'resources/malformed-worker.py?caught-exception';
+      var scope = 'resources/scope/caught-exception';
+      return register_method(script, {scope: scope})
+        .then(function(registration) {
+            assert_true(
+              registration instanceof ServiceWorkerRegistration,
+              'Successfully registered.');
+            return registration.unregister();
+          });
+    }, 'Registering script including caught exception');
+
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js
new file mode 100644
index 0000000..c45fbd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js
@@ -0,0 +1,78 @@
+// Registration tests that mostly exercise SecurityError cases.
+function registration_tests_security_error(register_method) {
+  promise_test(function(t) {
+      var script = 'resources/registration-worker.js';
+      var scope = 'resources';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registering same scope as the script directory without the last ' +
+              'slash should fail with SecurityError.');
+    }, 'Registering same scope as the script directory without the last slash');
+
+  promise_test(function(t) {
+      var script = 'resources/registration-worker.js';
+      var scope = 'different-directory/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration scope outside the script directory should fail ' +
+              'with SecurityError.');
+    }, 'Registration scope outside the script directory');
+
+  promise_test(function(t) {
+      var script = 'resources/registration-worker.js';
+      var scope = 'http://example.com/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration scope outside domain should fail with SecurityError.');
+    }, 'Registering scope outside domain');
+
+  promise_test(function(t) {
+      var script = 'http://example.com/worker.js';
+      var scope = 'http://example.com/scope/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration script outside domain should fail with SecurityError.');
+    }, 'Registering script outside domain');
+
+  promise_test(function(t) {
+      var script = 'resources/redirect.py?Redirect=' +
+                    encodeURIComponent('/resources/registration-worker.js');
+      var scope = 'resources/scope/redirect/';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Registration of redirected script should fail.');
+    }, 'Registering redirected script');
+
+  promise_test(function(t) {
+      var script = 'resources/empty-worker.js';
+      var scope = 'resources/../scope/parent-reference-in-scope';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Scope not under the script directory should be rejected.');
+    }, 'Scope including parent-reference and not under the script directory');
+
+  promise_test(function(t) {
+      var script = 'resources////empty-worker.js';
+      var scope = 'resources/scope/consecutive-slashes-in-script-url';
+      return promise_rejects_dom(t,
+          'SecurityError',
+          register_method(script, {scope: scope}),
+          'Consecutive slashes in the script url should not be unified.');
+    }, 'Script URL including consecutive slashes');
+
+  promise_test(function(t) {
+      var script = 'filesystem:' + normalizeURL('resources/empty-worker.js');
+      var scope = 'resources/scope/filesystem-script-url';
+      return promise_rejects_js(t,
+          TypeError,
+          register_method(script, {scope: scope}),
+          'Registering a script which has same-origin filesystem: URL should ' +
+              'fail with TypeError.');
+    }, 'Script URL is same-origin filesystem: URL');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js
new file mode 100644
index 0000000..44d1d27
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js
@@ -0,0 +1 @@
+// empty for now
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js
new file mode 100644
index 0000000..41f07fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js
@@ -0,0 +1,3 @@
+self.oninstall = function(event) {
+  event.waitUntil(Promise.reject());
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html b/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html
new file mode 100644
index 0000000..8a70e2a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener('message', event => {
+    var port = event.ports[0];
+    port.postMessage(event.data);
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js
new file mode 100644
index 0000000..6bd2b72
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js
@@ -0,0 +1,34 @@
+'use strict';
+
+onfetch = function(e) {
+  var headers = {};
+  for (var header of e.request.headers) {
+    var key = header[0], value = header[1];
+    headers[key] = value;
+  }
+  var append_header_error = '';
+  try {
+    e.request.headers.append('Test-Header', 'TestValue');
+  } catch (error) {
+    append_header_error = error.name;
+  }
+
+  var request_construct_error = '';
+  try {
+    new Request(e.request, {method: 'GET'});
+  } catch (error) {
+    request_construct_error = error.name;
+  }
+
+  e.respondWith(new Response(JSON.stringify({
+    url: e.request.url,
+    method: e.request.method,
+    referrer: e.request.referrer,
+    headers: headers,
+    mode: e.request.mode,
+    credentials: e.request.credentials,
+    redirect: e.request.redirect,
+    append_header_error: append_header_error,
+    request_construct_error: request_construct_error
+  })));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py b/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py
new file mode 100644
index 0000000..6ab148e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py
@@ -0,0 +1,8 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+    data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+    return [(b"Content-Type", b"application/json")], json.dumps(data)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
new file mode 100644
index 0000000..384c29b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script src="empty.js"></script>
+<script src="sample.js"></script>
+<script src="redirect.py?Redirect=empty.js"></script>
+<img src="square.png">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/square.png">
+<img src="missing.jpg">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/missing.jpg">
+<img src='missing.jpg?SWRespondsWithFetch'>
+<script src='empty-worker.js'></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js
new file mode 100644
index 0000000..b74e8cd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+  if (event.request.url.indexOf('sample.js') != -1) {
+    event.respondWith(new Promise(resolve => {
+      // Slightly delay the response so we ensure we get a non-zero
+      // duration.
+      setTimeout(_ => resolve(new Response('// Empty javascript')), 50);
+    }));
+  }
+  else if (event.request.url.indexOf('missing.jpg?SWRespondsWithFetch') != -1) {
+    event.respondWith(fetch('sample.txt?SWFetched'));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js
new file mode 100644
index 0000000..adb48de
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js
@@ -0,0 +1,40 @@
+var syncport = null;
+
+self.addEventListener('message', function(e) {
+  if ('port' in e.data) {
+    if (syncport) {
+      syncport(e.data.port);
+    } else {
+      syncport = e.data.port;
+    }
+  }
+});
+
+function sync() {
+  return new Promise(function(resolve) {
+      if (syncport) {
+        resolve(syncport);
+      } else {
+        syncport = resolve;
+      }
+    }).then(function(port) {
+      port.postMessage('SYNC');
+      return new Promise(function(resolve) {
+          port.onmessage = function(e) {
+            if (e.data === 'ACK') {
+              resolve();
+            }
+          }
+        });
+    });
+}
+
+
+self.addEventListener('fetch', function(event) {
+    // In Firefox the result would depend on a race between fetch handling
+    // and exception handling code. On the assumption that this might be a common
+    // design error, we explicitly allow the exception to be handled first.
+    event.respondWith(sync().then(() => new Response('intercepted')));
+
+    throw("error");
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
new file mode 100644
index 0000000..7be3148
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
@@ -0,0 +1,20 @@
+<script>
+var callback;
+
+// Creates a <script> element with |url| source, and returns a promise for the
+// result of the executed script. Uses JSONP because some responses to |url|
+// are opaque so their body cannot be tested directly.
+function getJSONP(url) {
+  var sc = document.createElement('script');
+  sc.src = url;
+  var promise = new Promise(function(resolve, reject) {
+      // This callback function is called by appending a script element.
+      callback = resolve;
+      sc.addEventListener(
+          'error',
+          function() { reject('Failed to load url:' + url); });
+    });
+  document.body.appendChild(sc);
+  return promise;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
new file mode 100644
index 0000000..c602109
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
@@ -0,0 +1,93 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+function getQueryParams(url) {
+  var search = (new URL(url)).search;
+  if (!search) {
+    return {};
+  }
+  var ret = {};
+  var params = search.substring(1).split('&');
+  params.forEach(function(param) {
+      var element = param.split('=');
+      ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+    });
+  return ret;
+}
+
+function createResponse(params) {
+  if (params['type'] == 'basic') {
+    return fetch('respond-with-body-accessed-response.jsonp');
+  }
+  if (params['type'] == 'opaque') {
+    return fetch(get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+          'respond-with-body-accessed-response.jsonp',
+          {mode: 'no-cors'});
+  }
+  if (params['type'] == 'default') {
+    return Promise.resolve(new Response('callback(\'OK\');'));
+  }
+
+  return Promise.reject(new Error('unexpected type :' + params['type']));
+}
+
+function cloneResponseIfNeeded(params, response) {
+  if (params['clone'] == '1') {
+    return response.clone();
+  } else if (params['clone'] == '2') {
+    response.clone();
+    return response;
+  }
+  return response;
+}
+
+function passThroughCacheIfNeeded(params, request, response) {
+  return new Promise(function(resolve) {
+      if (params['passThroughCache'] == 'true') {
+        var cache_name = request.url;
+        var cache;
+        self.caches.delete(cache_name)
+          .then(function() {
+              return self.caches.open(cache_name);
+            })
+          .then(function(c) {
+              cache = c;
+              return cache.put(request, response);
+            })
+          .then(function() {
+              return cache.match(request.url);
+            })
+          .then(function(res) {
+              // Touch .body here to test the behavior after touching it.
+              res.body;
+              resolve(res);
+            });
+      } else {
+        resolve(response);
+      }
+    })
+}
+
+self.addEventListener('fetch', function(event) {
+    if (event.request.url.indexOf('TestRequest') == -1) {
+      return;
+    }
+    var params = getQueryParams(event.request.url);
+    event.respondWith(
+        createResponse(params)
+          .then(function(response) {
+              // Touch .body here to test the behavior after touching it.
+              response.body;
+              return cloneResponseIfNeeded(params, response);
+            })
+          .then(function(response) {
+              // Touch .body here to test the behavior after touching it.
+              response.body;
+              return passThroughCacheIfNeeded(params, event.request, response);
+            })
+          .then(function(response) {
+              // Touch .body here to test the behavior after touching it.
+              response.body;
+              return response;
+            }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
new file mode 100644
index 0000000..b9c28f5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
@@ -0,0 +1 @@
+callback('OK');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js
new file mode 100644
index 0000000..c06f8dd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js
@@ -0,0 +1,62 @@
+importScripts('/common/get-host-info.sub.js');
+
+const text = 'worker loading intercepted by service worker';
+const dedicated_worker_script = `postMessage('${text}');`;
+const shared_worker_script =
+    `onconnect = evt => evt.ports[0].postMessage('${text}');`;
+
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+  source = event.data.port;
+  source.postMessage({id: event.source.id});
+  source.onmessage = resolveDone;
+  event.waitUntil(done);
+});
+
+self.onfetch = event => {
+  const url = event.request.url;
+  const destination = event.request.destination;
+
+  if (source)
+     source.postMessage({clientId:event.clientId, resultingClientId: event.resultingClientId});
+
+  // Request handler for a synthesized response.
+  if (url.indexOf('synthesized') != -1) {
+    let script_headers = new Headers({ "Content-Type": "text/javascript" });
+    if (destination === 'worker')
+      event.respondWith(new Response(dedicated_worker_script, { 'headers': script_headers }));
+    else if (destination === 'sharedworker')
+      event.respondWith(new Response(shared_worker_script, { 'headers': script_headers }));
+    else
+      event.respondWith(new Response('Unexpected request! ' + destination));
+    return;
+  }
+
+  // Request handler for a same-origin response.
+  if (url.indexOf('same-origin') != -1) {
+    event.respondWith(fetch('postmessage-on-load-worker.js'));
+    return;
+  }
+
+  // Request handler for a cross-origin response.
+  if (url.indexOf('cors') != -1) {
+    const filename = 'postmessage-on-load-worker.js';
+    const path = (new URL(filename, self.location)).pathname;
+    let new_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+    let mode;
+    if (url.indexOf('no-cors') != -1) {
+      // Test no-cors mode.
+      mode = 'no-cors';
+    } else {
+      // Test cors mode.
+      new_url += '?pipe=header(Access-Control-Allow-Origin,*)';
+      mode = 'cors';
+    }
+    event.respondWith(fetch(new_url, { mode: mode }));
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html
new file mode 100644
index 0000000..12a1799
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>Hello world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt
new file mode 100644
index 0000000..802992c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt
@@ -0,0 +1 @@
+Hello world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
new file mode 100644
index 0000000..239fa73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
@@ -0,0 +1,63 @@
+<script>
+function with_iframe(url) {
+  return new Promise(resolve => {
+    let frame = document.createElement('iframe');
+    frame.src = url;
+    frame.onload = () => { resolve(frame); };
+    document.body.appendChild(frame);
+  });
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+  return new Promise(resolve => {
+    let frame = document.createElement('iframe');
+    frame.sandbox = sandbox;
+    frame.src = url;
+    frame.onload = () => { resolve(frame); };
+    document.body.appendChild(frame);
+  });
+}
+
+function fetch_from_worker(url) {
+  return new Promise(resolve => {
+    let blob = new Blob([
+      `fetch('${url}', {mode: 'no-cors'})` +
+      "  .then(() => { self.postMessage('OK'); });"]);
+    let worker_url = URL.createObjectURL(blob);
+    let worker = new Worker(worker_url);
+    worker.onmessage = resolve;
+  });
+}
+
+function run_test(type) {
+  const base_path = location.href;
+  switch (type) {
+    case 'fetch':
+      return fetch(`${base_path}&test=fetch`, {mode: 'no-cors'});
+    case 'fetch-from-worker':
+      return fetch_from_worker(`${base_path}&test=fetch-from-worker`);
+    case 'iframe':
+      return with_iframe(`${base_path}&test=iframe`);
+    case 'sandboxed-iframe':
+      return with_sandboxed_iframe(`${base_path}&test=sandboxed-iframe`,
+                                   "allow-scripts");
+    case 'sandboxed-iframe-same-origin':
+      return with_sandboxed_iframe(
+        `${base_path}&test=sandboxed-iframe-same-origin`,
+        "allow-scripts allow-same-origin");
+    default:
+      return Promise.reject(`Unknown type: ${type}`);
+  }
+}
+
+window.onmessage = event => {
+  let id = event.data['id'];
+  run_test(event.data['type'])
+    .then(() => {
+      window.top.postMessage({id: id, result: 'done'}, '*');
+    })
+    .catch(e => {
+      window.top.postMessage({id: id, result: 'error: ' + e.toString()}, '*');
+    });
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
new file mode 100644
index 0000000..409a15b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
@@ -0,0 +1,18 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+  header = [(b'Content-Type', b'text/html')]
+  if b'test' in request.GET:
+    with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u'blank.html'), u'r') as f:
+      body = f.read()
+    return (header, body)
+
+  if b'sandbox' in request.GET:
+    header.append((b'Content-Security-Policy',
+                   b'sandbox %s' % request.GET[b'sandbox']))
+  with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+                         u'sandboxed-iframe-fetch-event-iframe.html'), u'r') as f:
+    body = f.read()
+  return (header, body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
new file mode 100644
index 0000000..4035a8b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
@@ -0,0 +1,20 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+    event.waitUntil(self.clients.matchAll()
+      .then(function(clients) {
+          var client_urls = [];
+          for(var client of clients){
+            client_urls.push(client.url);
+          }
+          client_urls = client_urls.sort();
+          event.data.port.postMessage(
+              {clients: client_urls, requests: requests});
+          requests = [];
+        }));
+  });
+
+self.addEventListener('fetch', function(event) {
+    requests.push(event.request.url);
+    event.respondWith(fetch(event.request));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
new file mode 100644
index 0000000..1d682e4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
@@ -0,0 +1,25 @@
+<script>
+window.onmessage = function(e) {
+  const id = e.data['id'];
+  try {
+    var sw = window.navigator.serviceWorker;
+  } catch (e) {
+    window.top.postMessage({
+        id: id,
+        result: 'navigator.serviceWorker failed: ' + e.name
+      }, '*');
+    return;
+  }
+
+  window.navigator.serviceWorker.getRegistration()
+    .then(function() {
+        window.top.postMessage({id: id, result:'ok'}, '*');
+      })
+    .catch(function(e) {
+        window.top.postMessage({
+            id: id,
+            result: 'getRegistration() failed: ' + e.name
+          }, '*');
+        });
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
new file mode 100644
index 0000000..ae681ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
@@ -0,0 +1 @@
+import * as module from './redirect.py?Redirect=/service-workers/service-worker/resources/scope2/imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
new file mode 100644
index 0000000..e285052
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
@@ -0,0 +1 @@
+import * as module from '../scope2/imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                              os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
new file mode 100644
index 0000000..5f785b5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+    return ([
+        (b'Cache-Control', b'no-cache, must-revalidate'),
+        (b'Pragma', b'no-cache'),
+        (b'Content-Type', b'application/javascript')],
+      b'echo_output = "%s (scope2/)";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js
new file mode 100644
index 0000000..a18e704
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js
@@ -0,0 +1,4 @@
+export const imported = 'A module script.';
+onmessage = msg => {
+    msg.source.postMessage('pong');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt
new file mode 100644
index 0000000..cd87667
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt
@@ -0,0 +1 @@
+a simple text file (scope2/)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                              os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js
new file mode 100644
index 0000000..5ba99f0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js
@@ -0,0 +1,21 @@
+self.addEventListener('fetch', event => {
+    let url = new URL(event.request.url);
+    if (url.pathname.indexOf('sender.html') != -1) {
+        event.respondWith(new Response(
+            "<script>window.parent.postMessage('interception', '*');</script>",
+            { headers: { 'Content-Type': 'text/html'} }
+        ));
+    } else if (url.pathname.indexOf('report') != -1) {
+        self.clients.matchAll().then(clients => {
+            for (client of clients) {
+                client.postMessage(url.searchParams.get('result'));
+            }
+        });
+        event.respondWith(
+            new Response(
+                '<script>window.close()</script>',
+                { headers: { 'Content-Type': 'text/html'} }
+            )
+        );
+    }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html
new file mode 100644
index 0000000..05e5882
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html
@@ -0,0 +1 @@
+<script>window.parent.postMessage('network', '*');</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html
new file mode 100644
index 0000000..071a507
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html
@@ -0,0 +1,15 @@
+<body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../test-helpers.sub.js"></script>
+<script>
+const HTTPS_PREFIX = get_host_info().HTTPS_ORIGIN + base_path();
+
+window.onmessage = event => {
+    window.location = HTTPS_PREFIX + 'report?result=' + event.data;
+};
+
+const frame = document.createElement('iframe');
+frame.src = HTTPS_PREFIX + 'sender.html';
+document.body.appendChild(frame);
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py
new file mode 100644
index 0000000..62c945f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py
@@ -0,0 +1,183 @@
+bodyDefault = '''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+    var import_script_failed = false;
+    try {
+      importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+        base_path() + 'empty.js');
+    } catch(e) {
+      import_script_failed = true;
+    }
+    assert_true(import_script_failed,
+                'Importing the other origins script should fail.');
+  }, 'importScripts test for default-src');
+
+test(function() {
+    assert_throws_js(EvalError,
+                     function() { eval('1 + 1'); },
+                     'eval() should throw EvalError.')
+    assert_throws_js(EvalError,
+                     function() { new Function('1 + 1'); },
+                     'new Function() should throw EvalError.')
+  }, 'eval test for default-src');
+
+async_test(function(t) {
+    fetch(host_info.HTTPS_REMOTE_ORIGIN +
+          base_path() + 'fetch-access-control.py?ACAOrigin=*',
+          {mode: 'cors'})
+      .then(function(response){
+          assert_unreached('fetch should fail.');
+        }, function(){
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Fetch test for default-src');
+
+async_test(function(t) {
+    var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+      base_path() + 'redirect.py?Redirect=';
+    var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+      base_path() + 'fetch-access-control.py?'
+    fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+          {mode: 'cors'})
+      .then(function(response){
+          assert_unreached('Redirected fetch should fail.');
+        }, function(){
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Redirected fetch test for default-src');'''
+
+bodyScript = '''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+    var import_script_failed = false;
+    try {
+      importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+        base_path() + 'empty.js');
+    } catch(e) {
+      import_script_failed = true;
+    }
+    assert_true(import_script_failed,
+                'Importing the other origins script should fail.');
+  }, 'importScripts test for script-src');
+
+test(function() {
+    assert_throws_js(EvalError,
+                     function() { eval('1 + 1'); },
+                     'eval() should throw EvalError.')
+    assert_throws_js(EvalError,
+                     function() { new Function('1 + 1'); },
+                     'new Function() should throw EvalError.')
+  }, 'eval test for script-src');
+
+async_test(function(t) {
+    fetch(host_info.HTTPS_REMOTE_ORIGIN +
+          base_path() + 'fetch-access-control.py?ACAOrigin=*',
+          {mode: 'cors'})
+      .then(function(response){
+          t.done();
+        }, function(){
+          assert_unreached('fetch should not fail.');
+        })
+      .catch(unreached_rejection(t));
+  }, 'Fetch test for script-src');
+
+async_test(function(t) {
+    var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+      base_path() + 'redirect.py?Redirect=';
+    var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+      base_path() + 'fetch-access-control.py?'
+    fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+          {mode: 'cors'})
+      .then(function(response){
+          t.done();
+        }, function(){
+          assert_unreached('Redirected fetch should not fail.');
+        })
+      .catch(unreached_rejection(t));
+  }, 'Redirected fetch test for script-src');'''
+
+bodyConnect = '''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+    var import_script_failed = false;
+    try {
+      importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+        base_path() + 'empty.js');
+    } catch(e) {
+      import_script_failed = true;
+    }
+    assert_false(import_script_failed,
+                 'Importing the other origins script should not fail.');
+  }, 'importScripts test for connect-src');
+
+test(function() {
+    var eval_failed = false;
+    try {
+      eval('1 + 1');
+      new Function('1 + 1');
+    } catch(e) {
+      eval_failed = true;
+    }
+    assert_false(eval_failed,
+                 'connect-src without unsafe-eval should not block eval().');
+  }, 'eval test for connect-src');
+
+async_test(function(t) {
+    fetch(host_info.HTTPS_REMOTE_ORIGIN +
+          base_path() + 'fetch-access-control.py?ACAOrigin=*',
+          {mode: 'cors'})
+      .then(function(response){
+          assert_unreached('fetch should fail.');
+        }, function(){
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Fetch test for connect-src');
+
+async_test(function(t) {
+    var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+      base_path() + 'redirect.py?Redirect=';
+    var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+      base_path() + 'fetch-access-control.py?'
+    fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+          {mode: 'cors'})
+      .then(function(response){
+          assert_unreached('Redirected fetch should fail.');
+        }, function(){
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Redirected fetch test for connect-src');'''
+
+def main(request, response):
+    headers = []
+    headers.append(('Content-Type', 'application/javascript'))
+    directive = request.GET['directive']
+    body = 'ERROR: Unknown directive'
+    if directive == 'default':
+        headers.append(('Content-Security-Policy', "default-src 'self'"))
+        body = bodyDefault
+    elif directive == 'script':
+        headers.append(('Content-Security-Policy', "script-src 'self'"))
+        body = bodyScript
+    elif directive == 'connect':
+        headers.append(('Content-Security-Policy', "connect-src 'self'"))
+        body = bodyConnect
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py
new file mode 100644
index 0000000..d64a9d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py
@@ -0,0 +1,20 @@
+def main(request, response):
+  service_worker_header = request.headers.get(b'service-worker')
+
+  if b'header' in request.GET and service_worker_header != b'script':
+    return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+  if b'no-header' in request.GET and service_worker_header == b'script':
+    return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+  # no-cache itself to ensure the user agent finds a new version for each
+  # update.
+  headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+             (b'Pragma', b'no-cache'),
+             (b'Content-Type', b'application/javascript')]
+  body = b'/* This is a service worker script */\n'
+
+  if b'import' in request.GET:
+    body += b"importScripts('%s');" % request.GET[b'import']
+
+  return 200, headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
new file mode 100644
index 0000000..680e07f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
@@ -0,0 +1 @@
+import('./service-worker-interception-network-worker.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
new file mode 100644
index 0000000..5ff3900
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
@@ -0,0 +1 @@
+postMessage('LOADED_FROM_NETWORK');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
new file mode 100644
index 0000000..6b43a37
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
@@ -0,0 +1,9 @@
+const kURL = '/service-worker-interception-network-worker.js';
+const kScript = 'postMessage("LOADED_FROM_SERVICE_WORKER")';
+const kHeaders = [['content-type', 'text/javascript']];
+
+self.addEventListener('fetch', e => {
+  // Serve a generated response for kURL.
+  if (e.request.url.indexOf(kURL) != -1)
+    e.respondWith(new Response(kScript, { headers: kHeaders }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
new file mode 100644
index 0000000..e570958
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
@@ -0,0 +1 @@
+import './service-worker-interception-network-worker.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga b/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga
new file mode 100644
index 0000000..af59188
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga
Binary files differ
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js
new file mode 100644
index 0000000..f8b5f8c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js
@@ -0,0 +1,5 @@
+self.onfetch = function(event) {
+  if (event.request.url.indexOf('simple') != -1)
+    event.respondWith(
+      new Response(new Blob(['intercepted by service worker'])));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
new file mode 100644
index 0000000..a17a9a3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
@@ -0,0 +1 @@
+Content-Type: application/javascript
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html
new file mode 100644
index 0000000..0c3e3e7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Simple</title>
+Here's a simple html file.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt
new file mode 100644
index 0000000..9e3cb91
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
new file mode 100644
index 0000000..6f7008b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
@@ -0,0 +1,33 @@
+var saw_activate_event = false
+
+self.addEventListener('activate', function() {
+    saw_activate_event = true;
+  });
+
+self.addEventListener('message', function(event) {
+    var port = event.data.port;
+    event.waitUntil(self.skipWaiting()
+      .then(function(result) {
+          if (result !== undefined) {
+            port.postMessage('FAIL: Promise should be resolved with undefined');
+            return;
+          }
+
+          if (!saw_activate_event) {
+            port.postMessage(
+                'FAIL: Promise should be resolved after activate event is dispatched');
+            return;
+          }
+
+          if (self.registration.active.state !== 'activating') {
+            port.postMessage(
+                'FAIL: Promise should be resolved before ServiceWorker#state is set to activated');
+            return;
+          }
+
+          port.postMessage('PASS');
+        })
+      .catch(function(e) {
+          port.postMessage('FAIL: unexpected exception: ' + e);
+        }));
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js
new file mode 100644
index 0000000..3fc1d1e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js
@@ -0,0 +1,21 @@
+importScripts('worker-testharness.js');
+
+promise_test(function() {
+    return skipWaiting()
+      .then(function(result) {
+          assert_equals(result, undefined,
+                        'Promise should be resolved with undefined');
+        })
+      .then(function() {
+          var promises = [];
+          for (var i = 0; i < 8; ++i)
+            promises.push(self.skipWaiting());
+          return Promise.all(promises);
+        })
+      .then(function(results) {
+          results.forEach(function(r) {
+              assert_equals(r, undefined,
+                            'Promises should be resolved with undefined');
+            });
+        });
+  }, 'skipWaiting');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/square.png b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png
new file mode 100644
index 0000000..01c9666
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png
Binary files differ
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers
new file mode 100644
index 0000000..7341132
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers
@@ -0,0 +1,2 @@
+Content-Type: image/png
+Access-Control-Allow-Origin: *
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js
new file mode 100644
index 0000000..fdf1e6c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js
@@ -0,0 +1,54 @@
+async function post_message_to_client(role, message, ports) {
+    (await clients.matchAll()).forEach(client => {
+        if (new URL(client.url).searchParams.get('role') === role) {
+            client.postMessage(message, ports);
+        }
+    });
+}
+
+async function post_message_to_child(message, ports) {
+    await post_message_to_client('child', message, ports);
+}
+
+function ping_message(data) {
+    return { type: 'ping', data };
+}
+
+self.onmessage = event => {
+    const message = ping_message(event.data);
+    post_message_to_child(message);
+    post_message_to_parent(message);
+}
+
+async function post_message_to_parent(message, ports) {
+    await post_message_to_client('parent', message, ports);
+}
+
+function fetch_message(key) {
+    return { type: 'fetch', key };
+}
+
+// Send a message to the parent along with a MessagePort to respond
+// with.
+function report_fetch_request(key) {
+    const channel = new MessageChannel();
+    const reply = new Promise(resolve => {
+        channel.port1.onmessage = resolve;
+    }).then(event => event.data);
+    return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply);
+}
+
+function respond_with_script(script) {
+    return new Response(new Blob(script, { type: 'text/javascript' }));
+}
+
+// Whenever a controlled document requests a URL with a 'key' search
+// parameter we report the request to the parent frame and wait for
+// a response. The content of the response is then used to respond to
+// the fetch request.
+addEventListener('fetch', event => {
+    let key = new URL(event.request.url).searchParams.get('key');
+    if (key) {
+        event.respondWith(report_fetch_request(key).then(respond_with_script));
+    }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
new file mode 100644
index 0000000..f745d7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+    return ([
+        (b'Cache-Control', b'no-cache, must-revalidate'),
+        (b'Pragma', b'no-cache'),
+        (b'Content-Type', b'application/javascript')],
+      b'echo_output = "%s (subdir/)";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt
new file mode 100644
index 0000000..86bcdd7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt
@@ -0,0 +1 @@
+a simple text file (subdir/)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                              os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/success.py b/third_party/web_platform_tests/service-workers/service-worker/resources/success.py
new file mode 100644
index 0000000..a026991
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/success.py
@@ -0,0 +1,8 @@
+def main(request, response):
+    headers = []
+
+    if b"ACAOrigin" in request.GET:
+        for item in request.GET[b"ACAOrigin"].split(b","):
+            headers.append((b"Access-Control-Allow-Origin", item))
+
+    return headers, b"{ \"result\": \"success\" }"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
new file mode 100644
index 0000000..59fb524
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<img src="/images/green.svg">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html
new file mode 100644
index 0000000..9a93d3b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Green svg box reference file</title>
+<p>Pass if you see a green box below.</p>
+<iframe src="svg-target-reftest-001-frame.html">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
new file mode 100644
index 0000000..d6fc820
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="/images/colors.svg#green">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js
new file mode 100644
index 0000000..7430152
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js
@@ -0,0 +1,300 @@
+// Adapter for testharness.js-style tests with Service Workers
+
+/**
+ * @param options an object that represents RegistrationOptions except for scope.
+ * @param options.type a WorkerType.
+ * @param options.updateViaCache a ServiceWorkerUpdateViaCache.
+ * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
+ */
+function service_worker_unregister_and_register(test, url, scope, options) {
+  if (!scope || scope.length == 0)
+    return Promise.reject(new Error('tests must define a scope'));
+
+  if (options && options.scope)
+    return Promise.reject(new Error('scope must not be passed in options'));
+
+  options = Object.assign({ scope: scope }, options);
+  return service_worker_unregister(test, scope)
+    .then(function() {
+        return navigator.serviceWorker.register(url, options);
+      })
+    .catch(unreached_rejection(test,
+                               'unregister and register should not fail'));
+}
+
+// This unregisters the registration that precisely matches scope. Use this
+// when unregistering by scope. If no registration is found, it just resolves.
+function service_worker_unregister(test, scope) {
+  var absoluteScope = (new URL(scope, window.location).href);
+  return navigator.serviceWorker.getRegistration(scope)
+    .then(function(registration) {
+        if (registration && registration.scope === absoluteScope)
+          return registration.unregister();
+      })
+    .catch(unreached_rejection(test, 'unregister should not fail'));
+}
+
+function service_worker_unregister_and_done(test, scope) {
+  return service_worker_unregister(test, scope)
+    .then(test.done.bind(test));
+}
+
+function unreached_fulfillment(test, prefix) {
+  return test.step_func(function(result) {
+      var error_prefix = prefix || 'unexpected fulfillment';
+      assert_unreached(error_prefix + ': ' + result);
+    });
+}
+
+// Rejection-specific helper that provides more details
+function unreached_rejection(test, prefix) {
+  return test.step_func(function(error) {
+      var reason = error.message || error.name || error;
+      var error_prefix = prefix || 'unexpected rejection';
+      assert_unreached(error_prefix + ': ' + reason);
+    });
+}
+
+/**
+ * Adds an iframe to the document and returns a promise that resolves to the
+ * iframe when it finishes loading. The caller is responsible for removing the
+ * iframe later if needed.
+ *
+ * @param {string} url
+ * @returns {HTMLIFrameElement}
+ */
+function with_iframe(url) {
+  return new Promise(function(resolve) {
+      var frame = document.createElement('iframe');
+      frame.className = 'test-iframe';
+      frame.src = url;
+      frame.onload = function() { resolve(frame); };
+      document.body.appendChild(frame);
+    });
+}
+
+function normalizeURL(url) {
+  return new URL(url, self.location).toString().replace(/#.*$/, '');
+}
+
+function wait_for_update(test, registration) {
+  if (!registration || registration.unregister == undefined) {
+    return Promise.reject(new Error(
+      'wait_for_update must be passed a ServiceWorkerRegistration'));
+  }
+
+  return new Promise(test.step_func(function(resolve) {
+      var handler = test.step_func(function() {
+        registration.removeEventListener('updatefound', handler);
+        resolve(registration.installing);
+      });
+      registration.addEventListener('updatefound', handler);
+    }));
+}
+
+// Return true if |state_a| is more advanced than |state_b|.
+function is_state_advanced(state_a, state_b) {
+  if (state_b === 'installing') {
+    switch (state_a) {
+      case 'installed':
+      case 'activating':
+      case 'activated':
+      case 'redundant':
+        return true;
+    }
+  }
+
+  if (state_b === 'installed') {
+    switch (state_a) {
+      case 'activating':
+      case 'activated':
+      case 'redundant':
+        return true;
+    }
+  }
+
+  if (state_b === 'activating') {
+    switch (state_a) {
+      case 'activated':
+      case 'redundant':
+        return true;
+    }
+  }
+
+  if (state_b === 'activated') {
+    switch (state_a) {
+      case 'redundant':
+        return true;
+    }
+  }
+  return false;
+}
+
+function wait_for_state(test, worker, state) {
+  if (!worker || worker.state == undefined) {
+    return Promise.reject(new Error(
+      'wait_for_state needs a ServiceWorker object to be passed.'));
+  }
+  if (worker.state === state)
+    return Promise.resolve(state);
+
+  if (is_state_advanced(worker.state, state)) {
+    return Promise.reject(new Error(
+      `Waiting for ${state} but the worker is already ${worker.state}.`));
+  }
+  return new Promise(test.step_func(function(resolve, reject) {
+      worker.addEventListener('statechange', test.step_func(function() {
+          if (worker.state === state)
+            resolve(state);
+
+          if (is_state_advanced(worker.state, state)) {
+            reject(new Error(
+              `The state of the worker becomes ${worker.state} while waiting` +
+                `for ${state}.`));
+          }
+        }));
+    }));
+}
+
+// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
+// is the service worker script URL. This function:
+// - Instantiates a new test with the description specified in |description|.
+//   The test will succeed if the specified service worker can be successfully
+//   registered and installed.
+// - Creates a new ServiceWorker registration with a scope unique to the current
+//   document URL. Note that this doesn't allow more than one
+//   service_worker_test() to be run from the same document.
+// - Waits for the new worker to begin installing.
+// - Imports tests results from tests running inside the ServiceWorker.
+function service_worker_test(url, description) {
+  // If the document URL is https://example.com/document and the script URL is
+  // https://example.com/script/worker.js, then the scope would be
+  // https://example.com/script/scope/document.
+  var scope = new URL('scope' + window.location.pathname,
+                      new URL(url, window.location)).toString();
+  promise_test(function(test) {
+      return service_worker_unregister_and_register(test, url, scope)
+        .then(function(registration) {
+            add_completion_callback(function() {
+                registration.unregister();
+              });
+            return wait_for_update(test, registration)
+              .then(function(worker) {
+                  return fetch_tests_from_worker(worker);
+                });
+          });
+    }, description);
+}
+
+function base_path() {
+  return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+
+function test_login(test, origin, username, password, cookie) {
+  return new Promise(function(resolve, reject) {
+      with_iframe(
+        origin + base_path() +
+        'resources/fetch-access-control-login.html')
+        .then(test.step_func(function(frame) {
+            var channel = new MessageChannel();
+            channel.port1.onmessage = test.step_func(function() {
+                frame.remove();
+                resolve();
+              });
+            frame.contentWindow.postMessage(
+              {username: username, password: password, cookie: cookie},
+              origin, [channel.port2]);
+          }));
+    });
+}
+
+function test_websocket(test, frame, url) {
+  return new Promise(function(resolve, reject) {
+      var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
+      var openCalled = false;
+      ws.addEventListener('open', test.step_func(function(e) {
+          assert_equals(ws.readyState, 1, "The WebSocket should be open");
+          openCalled = true;
+          ws.close();
+        }), true);
+
+      ws.addEventListener('close', test.step_func(function(e) {
+          assert_true(openCalled, "The WebSocket should be closed after being opened");
+          resolve();
+        }), true);
+
+      ws.addEventListener('error', reject);
+    });
+}
+
+function login_https(test) {
+  var host_info = get_host_info();
+  return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
+                    'username1s', 'password1s', 'cookie1')
+    .then(function() {
+        return test_login(test, host_info.HTTPS_ORIGIN,
+                          'username2s', 'password2s', 'cookie2');
+      });
+}
+
+function websocket(test, frame) {
+  return test_websocket(test, frame, get_websocket_url());
+}
+
+function get_websocket_url() {
+  return 'wss://{{host}}:{{ports[wss][0]}}/echo';
+}
+
+// The navigator.serviceWorker.register() method guarantees that the newly
+// installing worker is available as registration.installing when its promise
+// resolves. However some tests test installation using a <link> element where
+// it is possible for the installing worker to have already become the waiting
+// or active worker. So this method is used to get the newest worker when these
+// tests need access to the ServiceWorker itself.
+function get_newest_worker(registration) {
+  if (registration.installing)
+    return registration.installing;
+  if (registration.waiting)
+    return registration.waiting;
+  if (registration.active)
+    return registration.active;
+}
+
+function register_using_link(script, options) {
+  var scope = options.scope;
+  var link = document.createElement('link');
+  link.setAttribute('rel', 'serviceworker');
+  link.setAttribute('href', script);
+  link.setAttribute('scope', scope);
+  document.getElementsByTagName('head')[0].appendChild(link);
+  return new Promise(function(resolve, reject) {
+        link.onload = resolve;
+        link.onerror = reject;
+      })
+    .then(() => navigator.serviceWorker.getRegistration(scope));
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+  return new Promise(function(resolve) {
+      var frame = document.createElement('iframe');
+      frame.sandbox = sandbox;
+      frame.src = url;
+      frame.onload = function() { resolve(frame); };
+      document.body.appendChild(frame);
+    });
+}
+
+// Registers, waits for activation, then unregisters on a sample scope.
+//
+// This can be used to wait for a period of time needed to register,
+// activate, and then unregister a service worker.  When checking that
+// certain behavior does *NOT* happen, this is preferable to using an
+// arbitrary delay.
+async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
+  const script = '/service-workers/service-worker/resources/empty-worker.js';
+  const scope = 'resources/there/is/no/there/there?' + Date.now();
+  let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
+  await wait_for_state(t, registration.installing, 'activated');
+  await registration.unregister();
+}
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+  e.source.postMessage(headers);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py
new file mode 100644
index 0000000..8188bab
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py
@@ -0,0 +1,19 @@
+import json
+import os
+import uuid
+import sys
+
+def main(request, response):
+  path = os.path.join(os.path.dirname(__file__),
+                      u"test-request-headers-worker.js")
+  body = open(path, u"rb").read()
+
+  data = {key:request.headers[key] for key,value in request.headers.items()}
+  body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+  body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+  headers = []
+  headers.append((b"ETag", b"etag"))
+  headers.append((b"Content-Type", b'text/javascript'))
+
+  return headers, body
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+  e.source.postMessage(headers);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py
new file mode 100644
index 0000000..8449841
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py
@@ -0,0 +1,22 @@
+import json
+import os
+import uuid
+import sys
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+  path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+                      u"test-request-mode-worker.js")
+  body = open(path, u"rb").read()
+
+  data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+  body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+  body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+  headers = []
+  headers.append((b"ETag", b"etag"))
+  headers.append((b"Content-Type", b'text/javascript'))
+
+  return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js b/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js
new file mode 100644
index 0000000..b1a5b96
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js
@@ -0,0 +1,136 @@
+/*
+ * testharness-helpers contains various useful extensions to testharness.js to
+ * allow them to be used across multiple tests before they have been
+ * upstreamed. This file is intended to be usable from both document and worker
+ * environments, so code should for example not rely on the DOM.
+ */
+
+// Asserts that two objects |actual| and |expected| are weakly equal under the
+// following definition:
+//
+// |a| and |b| are weakly equal if any of the following are true:
+//   1. If |a| is not an 'object', and |a| === |b|.
+//   2. If |a| is an 'object', and all of the following are true:
+//     2.1 |a.p| is weakly equal to |b.p| for all own properties |p| of |a|.
+//     2.2 Every own property of |b| is an own property of |a|.
+//
+// This is a replacement for the the version of assert_object_equals() in
+// testharness.js. The latter doesn't handle own properties correctly. I.e. if
+// |a.p| is not an own property, it still requires that |b.p| be an own
+// property.
+//
+// Note that |actual| must not contain cyclic references.
+self.assert_object_equals = function(actual, expected, description) {
+  var object_stack = [];
+
+  function _is_equal(actual, expected, prefix) {
+    if (typeof actual !== 'object') {
+      assert_equals(actual, expected, prefix);
+      return;
+    }
+    assert_equals(typeof expected, 'object', prefix);
+    assert_equals(object_stack.indexOf(actual), -1,
+                  prefix + ' must not contain cyclic references.');
+
+    object_stack.push(actual);
+
+    Object.getOwnPropertyNames(expected).forEach(function(property) {
+        assert_own_property(actual, property, prefix);
+        _is_equal(actual[property], expected[property],
+                  prefix + '.' + property);
+      });
+    Object.getOwnPropertyNames(actual).forEach(function(property) {
+        assert_own_property(expected, property, prefix);
+      });
+
+    object_stack.pop();
+  }
+
+  function _brand(object) {
+    return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+  }
+
+  _is_equal(actual, expected,
+            (description ? description + ': ' : '') + _brand(expected));
+};
+
+// Equivalent to assert_in_array, but uses a weaker equivalence relation
+// (assert_object_equals) than '==='.
+function assert_object_in_array(actual, expected_array, description) {
+  assert_true(expected_array.some(function(element) {
+      try {
+        assert_object_equals(actual, element);
+        return true;
+      } catch (e) {
+        return false;
+      }
+    }), description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals. The order is not significant.
+//
+// |expected| is assumed to not contain any duplicates as determined by
+// assert_object_equals().
+function assert_array_equivalent(actual, expected, description) {
+  assert_true(Array.isArray(actual), description);
+  assert_equals(actual.length, expected.length, description);
+  expected.forEach(function(expected_element) {
+      // assert_in_array treats the first argument as being 'actual', and the
+      // second as being 'expected array'. We are switching them around because
+      // we want to be resilient against the |actual| array containing
+      // duplicates.
+      assert_object_in_array(expected_element, actual, description);
+    });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals(). The corresponding elements
+// must occupy corresponding indices in their respective arrays.
+function assert_array_objects_equals(actual, expected, description) {
+  assert_true(Array.isArray(actual), description);
+  assert_equals(actual.length, expected.length, description);
+  actual.forEach(function(value, index) {
+      assert_object_equals(value, expected[index],
+                           description + ' : object[' + index + ']');
+    });
+}
+
+// Asserts that |object| that is an instance of some interface has the attribute
+// |attribute_name| following the conditions specified by WebIDL, but it's
+// acceptable that the attribute |attribute_name| is an own property of the
+// object because we're in the middle of moving the attribute to a prototype
+// chain.  Once we complete the transition to prototype chains,
+// assert_will_be_idl_attribute must be replaced with assert_idl_attribute
+// defined in testharness.js.
+//
+// FIXME: Remove assert_will_be_idl_attribute once we complete the transition
+// of moving the DOM attributes to prototype chains.  (http://crbug.com/43394)
+function assert_will_be_idl_attribute(object, attribute_name, description) {
+  assert_equals(typeof object, "object", description);
+
+  assert_true("hasOwnProperty" in object, description);
+
+  // Do not test if |attribute_name| is not an own property because
+  // |attribute_name| is in the middle of the transition to a prototype
+  // chain.  (http://crbug.com/43394)
+
+  assert_true(attribute_name in object, description);
+}
+
+// Stringifies a DOM object.  This function stringifies not only own properties
+// but also DOM attributes which are on a prototype chain.  Note that
+// JSON.stringify only stringifies own properties.
+function stringifyDOMObject(object)
+{
+    function deepCopy(src) {
+        if (typeof src != "object")
+            return src;
+        var dst = Array.isArray(src) ? [] : {};
+        for (var property in src) {
+            dst[property] = deepCopy(src[property]);
+        }
+        return dst;
+    }
+    return JSON.stringify(deepCopy(object));
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py b/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py
new file mode 100644
index 0000000..6423f7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py
@@ -0,0 +1,14 @@
+import time
+
+def main(request, response):
+    delay = float(request.GET.first(b"ms", 500)) / 1E3
+    count = int(request.GET.first(b"count", 50))
+    # Read request body
+    request.body
+    time.sleep(delay)
+    response.headers.set(b"Content-type", b"text/plain")
+    response.write_status_headers()
+    time.sleep(delay)
+    for i in range(count):
+        response.writer.write_content(b"TEST_TRICKLE\n")
+        time.sleep(delay)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js
new file mode 100644
index 0000000..1779e23
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js
@@ -0,0 +1,10 @@
+let type = '';
+try {
+  importScripts('empty.js');
+  type = 'classic';
+} catch (e) {
+  type = 'module';
+}
+onmessage = e => {
+  e.source.postMessage(type);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html
new file mode 100644
index 0000000..18a95ee
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+    return new Promise(function(resolve, reject) {
+        var request = new XMLHttpRequest();
+        request.addEventListener('load', function(event) {
+            if (request.status == 200)
+                resolve(request.response);
+            else
+                reject(Error(request.statusText));
+        });
+        request.open('GET', url);
+        request.send();
+    });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
new file mode 100644
index 0000000..91a30de
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
@@ -0,0 +1,19 @@
+'use strict';
+
+// Returns a promise for a network response that contains the Clear-Site-Data:
+// "storage" header.
+function clear_site_data() {
+  return fetch('resources/blank.html?pipe=header(Clear-Site-Data,"storage")');
+}
+
+async function assert_no_registrations_exist() {
+  const registrations = await navigator.serviceWorker.getRegistrations();
+  assert_equals(registrations.length, 0);
+}
+
+async function add_controlled_iframe(test, url) {
+  const frame = await with_iframe(url);
+  test.add_cleanup(() => { frame.remove(); });
+  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+  return frame;
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
new file mode 100644
index 0000000..f5d0367
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+  const params = new URLSearchParams(self.location.search);
+  const scope = self.origin + params.get('scopepath');
+  const reg = await navigator.serviceWorker.getRegistration(scope);
+  if (reg) {
+    await reg.unregister();
+  }
+  if (window.opener) {
+    window.opener.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+  } else {
+    window.top.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+  }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py
new file mode 100644
index 0000000..64914a9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py
@@ -0,0 +1,24 @@
+import time
+
+script = u'''
+// Time stamp: %s
+// (This ensures the source text is *not* a byte-for-byte match with any
+// previously-fetched version of this script.)
+
+// This no-op fetch handler is necessary to bypass explicitly the no fetch
+// handler optimization by which this service worker script can be skipped.
+addEventListener('fetch', event => {
+    return;
+  });
+
+addEventListener('install', event => {
+    event.waitUntil(self.skipWaiting());
+  });
+
+addEventListener('activate', event => {
+    event.waitUntil(self.clients.claim());
+  });'''
+
+
+def main(request, response):
+  return [(b'Content-Type', b'application/javascript')], script % time.time()
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js
new file mode 100644
index 0000000..f1997bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const installEventFired = new Promise(resolve => {
+  self.fireInstallEvent = resolve;
+});
+
+const installFinished = new Promise(resolve => {
+  self.finishInstall = resolve;
+});
+
+addEventListener('install', event => {
+  fireInstallEvent();
+  event.waitUntil(installFinished);
+});
+
+addEventListener('message', event => {
+  let resolveWaitUntil;
+  event.waitUntil(new Promise(resolve => { resolveWaitUntil = resolve; }));
+
+  // Use a dedicated MessageChannel for every request so senders can wait for
+  // individual requests to finish, and concurrent requests (to different
+  // workers) don't cause race conditions.
+  const port = event.data;
+  port.onmessage = (event) => {
+    switch (event.data) {
+      case 'awaitInstallEvent':
+        installEventFired.then(() => {
+            port.postMessage('installEventFired');
+        }).finally(resolveWaitUntil);
+        break;
+
+      case 'finishInstall':
+        installFinished.then(() => {
+            port.postMessage('installFinished');
+        }).finally(resolveWaitUntil);
+        finishInstall();
+        break;
+
+      case 'callUpdate': {
+        const channel = new MessageChannel();
+        registration.update().then(() => {
+            channel.port2.postMessage({
+                success: true,
+            });
+        }).catch((exception) => {
+            channel.port2.postMessage({
+                success: false,
+                exception: exception.name,
+            });
+        }).finally(resolveWaitUntil);
+        port.postMessage(channel.port1, [channel.port1]);
+        break;
+      }
+
+      default:
+        port.postMessage('Unexpected command ' + event.data);
+        resolveWaitUntil();
+        break;
+    }
+  };
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py
new file mode 100644
index 0000000..3e15926
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py
@@ -0,0 +1,11 @@
+import random
+
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Cache-Control', b'max-age=0')]
+    # Plug in random.random() to the worker so update() finds a new worker every time.
+    body = u'''
+// %s
+importScripts('update-during-installation-worker.js');
+    '''.strip() % (random.random())
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py
new file mode 100644
index 0000000..02cbb42
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py
@@ -0,0 +1,18 @@
+import random
+import time
+
+def main(request, response):
+    # no-cache itself to ensure the user agent finds a new version for each update.
+    headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+               (b'Pragma', b'no-cache')]
+
+    content_type = b''
+    extra_body = u''
+
+    content_type = b'application/javascript'
+    headers.append((b'Content-Type', content_type))
+
+    extra_body = u"self.onfetch = (event) => { event.respondWith(fetch(event.request)); };"
+
+    # Return a different script for each access.
+    return headers, u'/* %s %s */ %s' % (time.time(), random.random(), extra_body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
new file mode 100644
index 0000000..7cc5a65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
@@ -0,0 +1,14 @@
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Cache-Control', b'max-age=86400'),
+               (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+    body = u'''
+        const importTime = {time:8f};
+    '''.format(time=time.time())
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py
new file mode 100644
index 0000000..4f87906
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py
@@ -0,0 +1,30 @@
+import time
+import json
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Cache-Control', b'max-age=86400'),
+               (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+    test = request.GET[b'test']
+
+    body = u'''
+        const mainTime = {time:8f};
+        const testName = {test};
+        importScripts('update-max-aged-worker-imported-script.py');
+
+        addEventListener('message', event => {{
+            event.source.postMessage({{
+                mainTime,
+                importTime,
+                test: {test}
+            }});
+        }});
+    '''.format(
+        time=time.time(),
+        test=json.dumps(isomorphic_decode(test))
+    )
+
+    return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
new file mode 100644
index 0000000..2d95387
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
@@ -0,0 +1,9 @@
+def main(request, response):
+    key = request.GET['key']
+    already_requested = request.server.stash.take(key)
+
+    if already_requested is None:
+        request.server.stash.put(key, True)
+        return [('Content-Type', 'application/javascript')], '// initial script'
+
+    response.status = (404, 'Not found: should not have been able to import this script twice!')
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
new file mode 100644
index 0000000..8be62ec
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
@@ -0,0 +1,13 @@
+def main(request, response):
+    key = request.GET['key']
+    already_requested = request.server.stash.take(key)
+
+    header = [('Content-Type', 'application/javascript')]
+    initial_script = 'importScripts("./update-missing-import-scripts-imported-worker.py?key={0}")'.format(key)
+    updated_script = '// removed importScripts()'
+
+    if already_requested is None:
+        request.server.stash.put(key, True)
+        return header, initial_script
+
+    return header, updated_script
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py
new file mode 100644
index 0000000..34eff02
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py
@@ -0,0 +1,14 @@
+import random
+import time
+
+def main(request, response):
+    # no-cache itself to ensure the user agent finds a new version for each update.
+    headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+               (b'Pragma', b'no-cache')]
+
+    # Set a normal mimetype.
+    content_type = b'application/javascript'
+
+    headers.append((b'Content-Type', content_type))
+    # Return a different script for each access.
+    return headers, u'// %s %s' % (time.time(), random.random())
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py
new file mode 100644
index 0000000..9ac7ce7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py
@@ -0,0 +1,25 @@
+def main(request, response):
+    # Set mode to 'init' for initial fetch.
+    mode = b'init'
+    if b'update-recovery-mode' in request.cookies:
+        mode = request.cookies[b'update-recovery-mode'].value
+
+    # no-cache itself to ensure the user agent finds a new version for each update.
+    headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+               (b'Pragma', b'no-cache')]
+
+    extra_body = b''
+
+    if mode == b'init':
+        # Install a bad service worker that will break the controlled
+        # document navigation.
+        response.set_cookie(b'update-recovery-mode', b'bad')
+        extra_body = b"addEventListener('fetch', function(e) { e.respondWith(Promise.reject()); });"
+    elif mode == b'bad':
+        # When the update tries to pull the script again, update to
+        # a worker service worker that does not break document
+        # navigation.  Serve the same script from then on.
+        response.delete_cookie(b'update-recovery-mode')
+
+    headers.append((b'Content-Type', b'application/javascript'))
+    return headers, b'%s' % (extra_body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py
new file mode 100644
index 0000000..3cabc0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py
@@ -0,0 +1,33 @@
+def classic_script():
+    return b"""
+      importScripts('./imported-classic-script.js');
+      self.onmessage = e => {
+        e.source.postMessage(imported);
+      };
+      """
+
+def module_script():
+    return b"""
+      import * as module from './imported-module-script.js';
+      self.onmessage = e => {
+        e.source.postMessage(module.imported);
+      };
+      """
+
+# Returns the classic script for a first request and
+# returns the module script for second and subsequent requests.
+def main(request, response):
+    headers = [(b'Content-Type', b'application/javascript'),
+               (b'Pragma', b'no-store'),
+               (b'Cache-Control', b'no-store')]
+
+    classic_first = request.GET[b'classic_first']
+    key = request.GET[b'key']
+    requested_once = request.server.stash.take(key)
+    if requested_once is None:
+        request.server.stash.put(key, True)
+        body = classic_script() if classic_first == b'1' else module_script()
+    else:
+        body = module_script() if classic_first == b'1' else classic_script()
+
+    return 200, headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
new file mode 100644
index 0000000..d43f6b2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
@@ -0,0 +1 @@
+// Hello world!
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
new file mode 100644
index 0000000..30c8783
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
@@ -0,0 +1,2 @@
+// Hello world!
+// **with extra body**
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py
new file mode 100644
index 0000000..ac0850f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py
@@ -0,0 +1,33 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+def serve_js_from_file(request, response, filename):
+  body = b''
+  path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), filename)
+  with open(path, 'rb') as f:
+    body = f.read()
+  return (
+    [
+      (b'Cache-Control', b'no-cache, must-revalidate'),
+      (b'Pragma', b'no-cache'),
+      (b'Content-Type', b'application/javascript')
+    ], body)
+
+def main(request, response):
+  key = request.GET[b"Key"]
+
+  visited_count = request.server.stash.take(key)
+  if visited_count is None:
+    visited_count = 0
+
+  # Keep how many times the test requested this resource.
+  visited_count += 1
+  request.server.stash.put(key, visited_count)
+
+  # Serve a file based on how many times it's requested.
+  if visited_count == 1:
+    return serve_js_from_file(request, response, request.GET[b"First"])
+  if visited_count == 2:
+    return serve_js_from_file(request, response, request.GET[b"Second"])
+  raise u"Unknown state"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py
new file mode 100644
index 0000000..5638a88
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py
@@ -0,0 +1,62 @@
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def redirect_response(request, response, visited_count):
+  # |visited_count| is used as a unique id to differentiate responses
+  # every time.
+  location = b'empty.js'
+  if b'Redirect' in request.GET:
+      location = isomorphic_encode(unquote(isomorphic_decode(request.GET[b'Redirect'])))
+  return (301,
+  [
+    (b'Cache-Control', b'no-cache, must-revalidate'),
+    (b'Pragma', b'no-cache'),
+    (b'Content-Type', b'application/javascript'),
+    (b'Location', location),
+  ],
+  u'/* %s */' % str(visited_count))
+
+def not_found_response():
+  return 404, [(b'Content-Type', b'text/plain')], u"Page not found"
+
+def ok_response(request, response, visited_count,
+                extra_body=u'', mime_type=b'application/javascript'):
+  # |visited_count| is used as a unique id to differentiate responses
+  # every time.
+  return (
+    [
+      (b'Cache-Control', b'no-cache, must-revalidate'),
+      (b'Pragma', b'no-cache'),
+      (b'Content-Type', mime_type)
+    ],
+    u'/* %s */ %s' % (str(visited_count), extra_body))
+
+def main(request, response):
+  key = request.GET[b"Key"]
+  mode = request.GET[b"Mode"]
+
+  visited_count = request.server.stash.take(key)
+  if visited_count is None:
+    visited_count = 0
+
+  # Keep how many times the test requested this resource.
+  visited_count += 1
+  request.server.stash.put(key, visited_count)
+
+  # Return a response based on |mode| only when it's the second time (== update).
+  if visited_count == 2:
+    if mode == b'normal':
+      return ok_response(request, response, visited_count)
+    if mode == b'bad_mime_type':
+      return ok_response(request, response, visited_count, mime_type=b'text/html')
+    if mode == b'not_found':
+      return not_found_response()
+    if mode == b'redirect':
+          return redirect_response(request, response, visited_count)
+    if mode == b'syntax_error':
+      return ok_response(request, response, visited_count, extra_body=u'badsyntax(isbad;')
+    if mode == b'throw_install':
+      return ok_response(request, response, visited_count, extra_body=u"addEventListener('install', function(e) { throw new Error('boom'); });")
+
+  return ok_response(request, response, visited_count)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
new file mode 100644
index 0000000..9d4c982
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
@@ -0,0 +1,8 @@
+<body>
+<script>
+function load_image(url) {
+  var img = document.createElement('img');
+  img.src = url;
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py
new file mode 100644
index 0000000..2070509
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py
@@ -0,0 +1,32 @@
+# This serves a different response to each request, to test service worker
+# updates. If |filename| is provided, it writes that file into the body.
+#
+# Usage:
+#   navigator.serviceWorker.register('update_shell.py?filename=worker.js')
+#
+# This registers worker.js as a service worker, and every update check
+# will return a new response.
+import os
+import random
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+  # Set no-cache to ensure the user agent finds a new version for each update.
+  headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+             (b'Pragma', b'no-cache'),
+             (b'Content-Type', b'application/javascript')]
+
+  # Return a different script for each access.
+  timestamp = u'// %s %s' % (time.time(), random.random())
+  body = isomorphic_encode(timestamp) + b'\n'
+
+  # Inject the file into the response.
+  if b'filename' in request.GET:
+    path = os.path.join(os.path.dirname(isomorphic_encode(__file__)),
+                        request.GET[b'filename'])
+    with open(path, 'rb') as f:
+      body += f.read()
+
+  return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html
new file mode 100644
index 0000000..c3ac803
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page Title</title>
+<video>
+  <track>
+</video>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
new file mode 100644
index 0000000..af85a73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
@@ -0,0 +1,12 @@
+var waitUntilResolve;
+self.addEventListener('install', function(event) {
+    event.waitUntil(new Promise(function(resolve) {
+        waitUntilResolve = resolve;
+      }));
+  });
+
+self.addEventListener('message', function(event) {
+    if (event.data === 'STOP_WAITING') {
+      waitUntilResolve();
+    }
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js
new file mode 100644
index 0000000..bb2dc81
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js
@@ -0,0 +1,35 @@
+let port;
+let received = false;
+
+function reportFailure(details) {
+  port.postMessage('FAIL: ' + details);
+}
+
+onmessage = event => {
+  port = event.source;
+
+  const ws = new WebSocket('wss://{{host}}:{{ports[wss][0]}}/echo');
+  ws.onopen = () => {
+    ws.send('Hello');
+  };
+  ws.onmessage = msg => {
+    if (msg.data !== 'Hello') {
+      reportFailure('Unexpected reply: ' + msg.data);
+      return;
+    }
+
+    received = true;
+    ws.close();
+  };
+  ws.onclose = (event) => {
+    if (!received) {
+      reportFailure('Closed before receiving reply: ' + event.code);
+      return;
+    }
+
+    port.postMessage('PASS');
+  };
+  ws.onerror = () => {
+    reportFailure('Got an error event');
+  };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js
new file mode 100644
index 0000000..fc6abd2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js
@@ -0,0 +1,7 @@
+self.urls = [];
+self.addEventListener('fetch', function(event) {
+    self.urls.push(event.request.url);
+  });
+self.addEventListener('message', function(event) {
+    event.data.port.postMessage({urls: self.urls});
+  });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html b/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html
new file mode 100644
index 0000000..32d0744
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+  self.onmessage = evt => {
+    if (self.opener)
+      self.opener.postMessage(evt.data, '*');
+    else
+      self.top.postMessage(evt.data, '*');
+  }
+  const params = new URLSearchParams(self.location.search);
+  const w = window.open(params.get('target'));
+  self.addEventListener('unload', evt => w.close());
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
new file mode 100644
index 0000000..383f666
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
@@ -0,0 +1,75 @@
+importScripts('/resources/testharness.js');
+
+function matchQuery(queryString) {
+  return self.location.search.substr(1) === queryString;
+}
+
+async function navigateTest(t, e) {
+  const port = e.data.port;
+  const url = e.data.url;
+  const expected = e.data.expected;
+
+  let p = clients.matchAll({ includeUncontrolled : true })
+    .then(function(clients) {
+      for (const client of clients) {
+        if (client.url === e.data.clientUrl) {
+          assert_equals(client.frameType, e.data.frameType);
+          return client.navigate(url);
+        }
+      }
+      throw 'Could not locate window client.';
+    }).then(function(newClient) {
+      // If we didn't reject, we better get resolved with the right thing.
+      if (newClient === null) {
+        assert_equals(newClient, expected);
+      } else {
+        assert_equals(newClient.url, expected);
+      }
+    });
+
+  if (typeof self[expected] === "function") {
+    // It's a JS error type name.  We are expecting our promise to be rejected
+    // with that error.
+    p = promise_rejects_js(t, self[expected], p);
+  }
+
+  // Let our caller know we are done.
+  return p.finally(() => port.postMessage(null));
+}
+
+function getTestClient() {
+  return clients.matchAll({ includeUncontrolled: true })
+    .then(function(clients) {
+      for (const client of clients) {
+        if (client.url.includes('windowclient-navigate.https.html')) {
+          return client;
+        }
+      }
+
+      throw new Error('Service worker was unable to locate test client.');
+    });
+}
+
+function waitForMessage(client) {
+  const channel = new MessageChannel();
+  client.postMessage({ port: channel.port2 }, [channel.port2]);
+
+  return new Promise(function(resolve) {
+    channel.port1.onmessage = resolve;
+  });
+}
+
+// The worker must remain in the "installing" state for the duration of some
+// sub-tests. In order to achieve this coordination without relying on global
+// state, the worker must create a message channel with the client from within
+// the "install" event handler.
+if (matchQuery('installing')) {
+  self.addEventListener('install', function(e) {
+    e.waitUntil(getTestClient().then(waitForMessage));
+  });
+}
+
+self.addEventListener('message', function(e) {
+  e.waitUntil(promise_test(t => navigateTest(t, e),
+                           e.data.description + " worker side"));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js
new file mode 100644
index 0000000..f592629
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js
@@ -0,0 +1,25 @@
+addEventListener('fetch', evt => {
+  if (evt.request.url.includes('worker-echo-client-id.js')) {
+    evt.respondWith(new Response(
+      'fetch("fetch-echo-client-id").then(r => r.text()).then(t => self.postMessage(t));',
+      { headers: { 'Content-Type': 'application/javascript' }}));
+    return;
+  }
+
+  if (evt.request.url.includes('fetch-echo-client-id')) {
+    evt.respondWith(new Response(evt.clientId));
+    return;
+  }
+
+  if (evt.request.url.includes('frame.html')) {
+    evt.respondWith(new Response(''));
+    return;
+  }
+});
+
+addEventListener('message', evt => {
+  if (evt.data === 'echo-client-id') {
+    evt.ports[0].postMessage(evt.source.id);
+    return;
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
new file mode 100644
index 0000000..a81bb3d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+self.addEventListener('fetch', event => {
+  const host_info = get_host_info();
+  // The sneaky Service Worker changes the same-origin 'square' request for a cross-origin image.
+  if (event.request.url.indexOf('square') != -1) {
+    const searchParams = new URLSearchParams(location.search);
+    const mode = searchParams.get("mode") || "cors";
+    event.respondWith(fetch(`${host_info['HTTPS_REMOTE_ORIGIN']}${base_path()}square.png`, { mode }));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
new file mode 100644
index 0000000..d36b0b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
@@ -0,0 +1,53 @@
+let name;
+if (self.registration.scope.indexOf('scope1') != -1)
+  name = 'sw1';
+if (self.registration.scope.indexOf('scope2') != -1)
+  name = 'sw2';
+
+
+self.addEventListener('fetch', evt => {
+  // There are three types of requests this service worker handles.
+
+  // (1) The first request for the worker, which will redirect elsewhere.
+  // "redirect.py" means to test network redirect, so let network handle it.
+  if (evt.request.url.indexOf('redirect.py') != -1) {
+    return;
+  }
+  // "sw-redirect" means to test service worker redirect, so respond with a
+  // redirect.
+  if (evt.request.url.indexOf('sw-redirect') != -1) {
+    const url = new URL(evt.request.url);
+    const redirect_to = url.searchParams.get('Redirect');
+    evt.respondWith(Response.redirect(redirect_to));
+    return;
+  }
+
+  // (2) After redirect, the request is for a "webworker.py" URL.
+  // Add a search parameter to indicate this service worker handled the
+  // final request for the worker.
+  if (evt.request.url.indexOf('webworker.py') != -1) {
+    const greeting = encodeURIComponent(`${name} saw the request for the worker script`);
+    // Serve from `./subdir/`, not `./`,
+    // to conform that the base URL used in the worker is
+    // the response URL (`./subdir/`), not the current request URL (`./`).
+    evt.respondWith(fetch(`subdir/worker_interception_redirect_webworker.py?greeting=${greeting}`));
+    return;
+  }
+
+  const path = (new URL(evt.request.url)).pathname;
+
+  // (3) The worker does an importScripts() to import-scripts-echo.py. Indicate
+  // that this service worker handled the request.
+  if (evt.request.url.indexOf('import-scripts-echo.py') != -1) {
+    const msg = encodeURIComponent(`${name} saw importScripts from the worker: ${path}`);
+    evt.respondWith(fetch(`import-scripts-echo.py?msg=${msg}`));
+    return;
+  }
+
+  // (4) The worker does a fetch() to simple.txt. Indicate that this service
+  // worker handled the request.
+  if (evt.request.url.indexOf('simple.txt') != -1) {
+    evt.respondWith(new Response(`${name} saw the fetch from the worker: ${path}`));
+    return;
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
new file mode 100644
index 0000000..b7e6d81
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
@@ -0,0 +1,56 @@
+// This is the (shared or dedicated) worker file for the
+// worker-interception-redirect test. It should be served by the corresponding
+// .py file instead of being served directly.
+//
+// This file is served from both resources/*webworker.py,
+// resources/scope2/*webworker.py and resources/subdir/*webworker.py.
+// Relative paths are used in `fetch()` and `importScripts()` to confirm that
+// the correct base URLs are used.
+
+// This greeting text is meant to be injected by the Python script that serves
+// this file, to indicate how the script was served (from network or from
+// service worker).
+//
+// We can't just use a sub pipe and name this file .sub.js since we want
+// to serve the file from multiple URLs (see above).
+let greeting = '%GREETING_TEXT%';
+if (!greeting)
+  greeting = 'the worker script was served from network';
+
+// Call importScripts() which fills |echo_output| with a string indicating
+// whether a service worker intercepted the importScripts() request.
+let echo_output;
+const import_scripts_msg = encodeURIComponent(
+    'importScripts: served from network');
+let import_scripts_greeting = 'not set';
+try {
+  importScripts(`import-scripts-echo.py?msg=${import_scripts_msg}`);
+  import_scripts_greeting = echo_output;
+} catch(e) {
+  import_scripts_greeting = 'importScripts failed';
+}
+
+async function runTest(port) {
+  port.postMessage(greeting);
+
+  port.postMessage(import_scripts_greeting);
+
+  const response = await fetch('simple.txt');
+  const text = await response.text();
+  port.postMessage('fetch(): ' + text);
+
+  port.postMessage(self.location.href);
+}
+
+if ('DedicatedWorkerGlobalScope' in self &&
+    self instanceof DedicatedWorkerGlobalScope) {
+  runTest(self);
+} else if (
+    'SharedWorkerGlobalScope' in self &&
+    self instanceof SharedWorkerGlobalScope) {
+  self.onconnect = function(e) {
+    const port = e.ports[0];
+    port.start();
+    runTest(port);
+  };
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js
new file mode 100644
index 0000000..ebc0db6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js
@@ -0,0 +1,16 @@
+importScripts('/common/get-host-info.sub.js');
+
+const response_text = 'This load was successfully intercepted.';
+const response_script =
+    `const message = 'This load was successfully intercepted.';`;
+
+self.onfetch = event => {
+  const url = event.request.url;
+  if (url.indexOf('synthesized-response.txt') != -1) {
+    event.respondWith(new Response(response_text));
+  } else if (url.indexOf('synthesized-response.js') != -1) {
+    event.respondWith(new Response(
+        response_script,
+        {headers: {'Content-Type': 'application/javascript'}}));
+  }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js
new file mode 100644
index 0000000..73e97be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js
@@ -0,0 +1,49 @@
+/*
+ * worker-test-harness should be considered a temporary polyfill around
+ * testharness.js for supporting Service Worker based tests. It should not be
+ * necessary once the test harness is able to drive worker based tests natively.
+ * See https://github.com/w3c/testharness.js/pull/82 for status of effort to
+ * update upstream testharness.js. Once the upstreaming is complete, tests that
+ * reference worker-test-harness should be updated to directly import
+ * testharness.js.
+ */
+
+importScripts('/resources/testharness.js');
+
+(function() {
+  var next_cache_index = 1;
+
+  // Returns a promise that resolves to a newly created Cache object. The
+  // returned Cache will be destroyed when |test| completes.
+  function create_temporary_cache(test) {
+    var uniquifier = String(++next_cache_index);
+    var cache_name = self.location.pathname + '/' + uniquifier;
+
+    test.add_cleanup(function() {
+        return self.caches.delete(cache_name);
+      });
+
+    return self.caches.delete(cache_name)
+      .then(function() {
+          return self.caches.open(cache_name);
+        });
+  }
+
+  self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+//    cache_test(function(cache) {
+//      // Do something with |cache|, which is a Cache object.
+//    }, "Some Cache test");
+function cache_test(test_function, description) {
+  promise_test(function(test) {
+      return create_temporary_cache(test)
+        .then(test_function);
+    }, description);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..4ed5bee
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
@@ -0,0 +1,20 @@
+# This serves the worker JavaScript file. It takes a |greeting| request
+# parameter to inject into the JavaScript to indicate how the request
+# reached the server.
+import os
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+  path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+                      u"worker-interception-redirect-webworker.js")
+  body = open(path, u"rb").read()
+  if b"greeting" in request.GET:
+    body = body.replace(b"%GREETING_TEXT%", request.GET[b"greeting"])
+  else:
+    body = body.replace(b"%GREETING_TEXT%", b"")
+
+  headers = []
+  headers.append((b"Content-Type", b"text/javascript"))
+
+  return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js
new file mode 100644
index 0000000..604deec
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js
@@ -0,0 +1,22 @@
+// Service worker for the xhr-content-length test.
+
+self.addEventListener("fetch", event => {
+  const url = new URL(event.request.url);
+  const type = url.searchParams.get("type");
+
+  if (type === "no-content-length") {
+    event.respondWith(new Response("Hello!"));
+  }
+
+  if (type === "larger-content-length") {
+    event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"]] }));
+  }
+
+  if (type === "double-content-length") {
+    event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"], ["Content-Length", "10000"]] }));
+  }
+
+  if (type === "bogus-content-length") {
+    event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "test"]] }));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html
new file mode 100644
index 0000000..4c57bbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for xhr tests</title>
+<script>
+async function xhr(url, options) {
+  return new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest();
+    const opts = options ? options : {};
+    xhr.onload = () => {
+      resolve(xhr);
+    };
+    xhr.onerror = () => {
+      reject('xhr failed');
+    };
+
+    xhr.open('GET', url);
+    if (opts.responseType) {
+      xhr.responseType = opts.responseType;
+    }
+    xhr.send();
+  });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js
new file mode 100644
index 0000000..906ad50
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js
@@ -0,0 +1,32 @@
+// Service worker for the xhr-response-url test.
+
+self.addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+  const respondWith = url.searchParams.get('respondWith');
+  if (!respondWith)
+    return;
+
+  if (respondWith == 'fetch') {
+    const target = url.searchParams.get('url');
+    event.respondWith(fetch(target));
+    return;
+  }
+
+  if (respondWith == 'string') {
+    const headers = {'content-type': 'text/plain'};
+    event.respondWith(new Response('hello', {headers}));
+    return;
+  }
+
+  if (respondWith == 'document') {
+    const doc = `
+        <!DOCTYPE html>
+        <html>
+        <title>hi</title>
+        <body>hello</body>
+        </html>`;
+    const headers = {'content-type': 'text/html'};
+    event.respondWith(new Response(doc, {headers}));
+    return;
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
new file mode 100644
index 0000000..065a07a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="resources/request-url-path/import-relative.xsl"?>
+<stylesheet-test>
+This tests a stylesheet which has a xsl:import with a relative URL.
+</stylesheet-test>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js
new file mode 100644
index 0000000..50e2b18
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', event => {
+  const url = new URL(event.request.url);
+
+  // For the import-relative.xsl file, respond in a way that changes the
+  // response URL. This is expected to change the base URL and allow the import
+  // from the file to succeed.
+  const path = 'request-url-path/import-relative.xsl';
+  if (url.pathname.indexOf(path) != -1) {
+    // Respond with a different URL, deleting "request-url-path/".
+    event.respondWith(fetch('import-relative.xsl'));
+  }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl b/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl
new file mode 100644
index 0000000..2cd7f2f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:template match="/">
+    <html>
+      <body>
+           <p>PASS</p>
+      </body>
+    </html>
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
new file mode 100644
index 0000000..f6713d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker responds with .body accessed response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+promise_test(t => {
+    const SCOPE = 'resources/respond-with-body-accessed-response-iframe.html';
+    const SCRIPT = 'resources/respond-with-body-accessed-response-worker.js';
+
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(reg => {
+          promise_test(t => {
+              if (frame)
+                frame.remove();
+              return reg.unregister();
+            }, 'restore global state');
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(() => { return with_iframe(SCOPE); })
+      .then(f => { frame = f; });
+  }, 'initialize global state');
+
+const TEST_CASES = [
+  "type=basic",
+  "type=opaque",
+  "type=default",
+  "type=basic&clone=1",
+  "type=opaque&clone=1",
+  "type=default&clone=1",
+  "type=basic&clone=2",
+  "type=opaque&clone=2",
+  "type=default&clone=2",
+  "type=basic&passThroughCache=true",
+  "type=opaque&passThroughCache=true",
+  "type=default&passThroughCache=true",
+  "type=basic&clone=1&passThroughCache=true",
+  "type=opaque&clone=1&passThroughCache=true",
+  "type=default&clone=1&passThroughCache=true",
+  "type=basic&clone=2&passThroughCache=true",
+  "type=opaque&clone=2&passThroughCache=true",
+  "type=default&clone=2&passThroughCache=true",
+];
+
+TEST_CASES.forEach(param => {
+    promise_test(t => {
+        const url = 'TestRequest?' + param;
+        return frame.contentWindow.getJSONP(url)
+          .then(result => { assert_equals(result, 'OK'); });
+      }, 'test: ' + param);
+  });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html b/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html
new file mode 100644
index 0000000..1d9b60d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Same-site cookie behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const COOKIE_VALUE = 'COOKIE_VALUE';
+
+function make_nested_url(nested_origins, target_url) {
+  for (let i = nested_origins.length - 1; i >= 0; --i) {
+    target_url = new URL(
+      `./resources/nested-parent.html?target=${encodeURIComponent(target_url)}`,
+      nested_origins[i] + self.location.pathname);
+  }
+  return target_url;
+}
+
+const scopepath = '/cookies/resources/postToParent.py?with-sw';
+
+async function unregister_service_worker(origin, nested_origins=[]) {
+  let target_url = origin +
+      '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+      '?scopepath=' + encodeURIComponent(scopepath);
+  target_url = make_nested_url(nested_origins, target_url);
+  const w = window.open(target_url);
+  try {
+    await wait_for_message('SW-UNREGISTERED');
+  } finally {
+    w.close();
+  }
+}
+
+async function register_service_worker(origin, nested_origins=[]) {
+  let target_url = origin +
+      '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+      '?scopepath=' + encodeURIComponent(scopepath);
+  target_url = make_nested_url(nested_origins, target_url);
+  const w = window.open(target_url);
+  try {
+    await wait_for_message('SW-REGISTERED');
+  } finally {
+    w.close();
+  }
+}
+
+async function run_test(t, origin, navaction, swaction, expected,
+                        redirect_origins=[], nested_origins=[]) {
+  if (swaction === 'navpreload') {
+    assert_true('navigationPreload' in ServiceWorkerRegistration.prototype,
+                'navigation preload must be supported');
+  }
+  const sw_param = swaction === 'no-sw' ? 'no-sw' : 'with-sw';
+  let action_param = '';
+  if (swaction === 'fallback') {
+    action_param = '&ignore';
+  } else if (swaction !== 'no-sw') {
+    action_param = '&' + swaction;
+  }
+  const navpreload_param = swaction === 'navpreload' ? '&navpreload' : '';
+  const change_request_param = swaction === 'change-request' ? '&change-request' : '';
+  const target_string = origin + `/cookies/resources/postToParent.py?` +
+                                 `${sw_param}${action_param}`
+  let target_url = new URL(target_string);
+
+  for (let i = redirect_origins.length - 1; i >= 0; --i) {
+    const redirect_url = new URL(
+        `./resources/redirect.py?Status=307&Redirect=${encodeURIComponent(target_url)}`,
+        redirect_origins[i] + self.location.pathname);
+    target_url = redirect_url;
+  }
+
+  if (navaction === 'window.open') {
+    target_url = new URL(
+        `./resources/window-opener.html?target=${encodeURIComponent(target_url)}`,
+        self.origin + self.location.pathname);
+  } else if (navaction === 'form post') {
+    target_url = new URL(
+        `./resources/form-poster.html?target=${encodeURIComponent(target_url)}`,
+        self.origin + self.location.pathname);
+  } else if (navaction === 'set location') {
+    target_url = new URL(
+        `./resources/location-setter.html?target=${encodeURIComponent(target_url)}`,
+        self.origin + self.location.pathname);
+  }
+
+  const w = window.open(make_nested_url(nested_origins, target_url));
+  t.add_cleanup(() => w.close());
+
+  const result = await wait_for_message('COOKIES');
+  verifySameSiteCookieState(expected, COOKIE_VALUE, result.data);
+}
+
+promise_test(async t => {
+  await resetSameSiteCookies(self.origin, COOKIE_VALUE);
+  await register_service_worker(self.origin);
+
+  await resetSameSiteCookies(SECURE_SUBDOMAIN_ORIGIN, COOKIE_VALUE);
+  await register_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+
+  await resetSameSiteCookies(SECURE_CROSS_SITE_ORIGIN, COOKIE_VALUE);
+  await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+
+  await register_service_worker(self.origin,
+      [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Setup service workers');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'no-sw',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, window.open with no service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'fallback',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, window.open with fallback');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'passthrough',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, window.open with passthrough');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, window.open with change-request');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'navpreload',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, window.open with navpreload');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'no-sw',
+                  SameSiteStatus.STRICT);
+}, 'same-site, window.open with no service worker');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'fallback',
+                  SameSiteStatus.STRICT);
+}, 'same-site, window.open with fallback');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'passthrough',
+                  SameSiteStatus.STRICT);
+}, 'same-site, window.open with passthrough');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'same-site, window.open with change-request');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'navpreload',
+                  SameSiteStatus.STRICT);
+}, 'same-site, window.open with navpreload');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'no-sw',
+                  SameSiteStatus.LAX);
+}, 'cross-site, window.open with no service worker');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'fallback',
+                  SameSiteStatus.LAX);
+}, 'cross-site, window.open with fallback');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'passthrough',
+                  SameSiteStatus.LAX);
+}, 'cross-site, window.open with passthrough');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'cross-site, window.open with change-request');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'navpreload',
+                  SameSiteStatus.LAX);
+}, 'cross-site, window.open with navpreload');
+
+//
+// window.open redirect tests
+//
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'no-sw',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with no service worker and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'fallback',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with fallback and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'passthrough',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with passthrough and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with change-request and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'navpreload',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with navpreload and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'no-sw',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with no service worker and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'fallback',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with fallback and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'passthrough',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with passthrough and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with change-request and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'navpreload',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with navpreload and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'no-sw',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with no service worker, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'fallback',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with fallback, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'passthrough',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with passthrough, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with change-request, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'navpreload',
+                  SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with navpreload, cross-site redirect, and ' +
+   'same-origin redirect');
+
+//
+// Double-nested frame calling open.window() tests
+//
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'no-sw',
+                  SameSiteStatus.STRICT, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+   'no service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'fallback',
+                  SameSiteStatus.STRICT, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+   'fallback service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'passthrough',
+                  SameSiteStatus.STRICT, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+   'passthrough service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'change-request',
+                  SameSiteStatus.STRICT, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+   'change-request service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'window.open', 'navpreload',
+                  SameSiteStatus.STRICT, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+   'navpreload service worker');
+
+//
+// Double-nested frame setting location tests
+//
+promise_test(t => {
+  return run_test(t, self.origin, 'set location', 'no-sw',
+                  SameSiteStatus.CROSS_SITE, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+   'no service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'set location', 'fallback',
+                  SameSiteStatus.CROSS_SITE, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+   'fallback service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'set location', 'passthrough',
+                  SameSiteStatus.CROSS_SITE, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+   'passthrough service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'set location', 'change-request',
+                  SameSiteStatus.CROSS_SITE, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+   'change-request service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'set location', 'navpreload',
+                  SameSiteStatus.CROSS_SITE, [],
+                  [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+   'navpreload service worker');
+
+//
+// Form POST tests
+//
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'no-sw', SameSiteStatus.STRICT);
+}, 'same-origin, form post with no service worker');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'fallback',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, form post with fallback');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'passthrough',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, form post with passthrough');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'same-origin, form post with change-request');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'no-sw',
+                  SameSiteStatus.STRICT);
+}, 'same-site, form post with no service worker');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'fallback',
+                  SameSiteStatus.STRICT);
+}, 'same-site, form post with fallback');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'passthrough',
+                  SameSiteStatus.STRICT);
+}, 'same-site, form post with passthrough');
+
+promise_test(t => {
+  return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'same-site, form post with change-request');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'no-sw',
+                  SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with no service worker');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'fallback',
+                  SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with fallback');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'passthrough',
+                  SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with passthrough');
+
+promise_test(t => {
+  return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'change-request',
+                  SameSiteStatus.STRICT);
+}, 'cross-site, form post with change-request');
+
+//
+// Form POST redirect tests
+//
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'no-sw',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with no service worker and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'fallback',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with fallback and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'passthrough',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with passthrough and same-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with change-request and same-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'no-sw',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with no service worker and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'fallback',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with fallback and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'passthrough',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with passthrough and cross-site redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with change-request and cross-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'no-sw',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+                                              self.origin]);
+}, 'same-origin, form post with no service worker, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'fallback',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+                                              self.origin]);
+}, 'same-origin, form post with fallback, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'passthrough',
+                  SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+                                              self.origin]);
+}, 'same-origin, form post with passthrough, cross-site redirect, and ' +
+   'same-origin redirect');
+
+promise_test(t => {
+  return run_test(t, self.origin, 'form post', 'change-request',
+                  SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN,
+                                          self.origin]);
+}, 'same-origin, form post with change-request, cross-site redirect, and ' +
+   'same-origin redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(async t => {
+  await unregister_service_worker(self.origin);
+  await unregister_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+  await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+  await unregister_service_worker(self.origin,
+      [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
new file mode 100644
index 0000000..ba34e79
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
@@ -0,0 +1,536 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent for sandboxed iframe.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function doTest(frame, type) {
+  return new Promise(function(resolve) {
+    var id = ++lastCallbackId;
+    callbacks[id] = resolve;
+    frame.contentWindow.postMessage({id: id, type: type}, '*');
+  });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+  return new Promise(resolve => {
+    let channel = new MessageChannel();
+    channel.port1.onmessage = msg => {
+      resolve(msg.data);
+    };
+    worker.postMessage({port: channel.port2}, [channel.port2]);
+  });
+}
+
+window.onmessage = function (e) {
+  message = e.data;
+  var id = message['id'];
+  var callback = callbacks[id];
+  delete callbacks[id];
+  callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// Service worker controlling |SCOPE|.
+let worker;
+// A normal iframe.
+// This should be controlled by a service worker.
+let normal_frame;
+// An iframe created by <iframe sandbox='allow-scripts'>.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame;
+// An iframe created by <iframe sandbox='allow-scripts allow-same-origin'>.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+  return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+    .then(function(registration) {
+      add_completion_callback(() => registration.unregister());
+      worker = registration.installing;
+      return wait_for_state(t, registration.installing, 'activated');
+    });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+  return with_iframe(SCOPE + '?iframe')
+    .then(f => {
+      normal_frame = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1);
+      assert_equals(requests[0], expected_base_url + '?iframe');
+      assert_true(data.clients.includes(expected_base_url + '?iframe'));
+    });
+}, 'Prepare a normal iframe.');
+
+promise_test(t => {
+  return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe', 'allow-scripts')
+    .then(f => {
+      sandboxed_frame = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0);
+      assert_false(data.clients.includes(expected_base_url +
+                                         '?sandboxed-iframe'));
+    });
+}, 'Prepare an iframe sandboxed by <iframe sandbox="allow-scripts">.');
+
+promise_test(t => {
+  return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe-same-origin',
+                               'allow-scripts allow-same-origin')
+    .then(f => {
+      sandboxed_same_origin_frame = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1);
+      assert_equals(requests[0],
+                    expected_base_url + '?sandboxed-iframe-same-origin');
+      assert_true(data.clients.includes(
+        expected_base_url + '?sandboxed-iframe-same-origin'));
+    })
+}, 'Prepare an iframe sandboxed by ' +
+   '<iframe sandbox="allow-scripts allow-same-origin">.');
+
+promise_test(t => {
+  const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+                          'sandboxed-frame-by-header';
+  return with_iframe(iframe_full_url)
+    .then(f => {
+      sandboxed_frame_by_header = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'Service worker should provide the response');
+      assert_equals(requests[0], iframe_full_url);
+      assert_false(data.clients.includes(iframe_full_url),
+                   'Service worker should NOT control the sandboxed page');
+    });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+  const iframe_full_url =
+    expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+    'sandboxed-iframe-same-origin-by-header';
+  return with_iframe(iframe_full_url)
+    .then(f => {
+      sandboxed_same_origin_frame_by_header = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1);
+      assert_equals(requests[0], iframe_full_url);
+      assert_true(data.clients.includes(iframe_full_url));
+    })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+   'allow-same-origin.');
+
+promise_test(t => {
+  let frame = normal_frame;
+  return doTest(frame, 'fetch')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The fetch request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=fetch');
+    });
+}, 'Fetch request from a normal iframe');
+
+promise_test(t => {
+  let frame = normal_frame;
+  return doTest(frame, 'fetch-from-worker')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The fetch request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+    });
+}, 'Fetch request from a worker in a normal iframe');
+
+promise_test(t => {
+  let frame = normal_frame;
+  return doTest(frame, 'iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=iframe');
+      assert_true(data.clients.includes(frame.src + '&test=iframe'));
+
+    });
+}, 'Request for an iframe in the normal iframe');
+
+promise_test(t => {
+  let frame = normal_frame;
+  return doTest(frame, 'sandboxed-iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the normal ' +
+   'iframe');
+
+promise_test(t => {
+  let frame = normal_frame;
+  return doTest(frame, 'sandboxed-iframe-same-origin')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0],
+                    frame.src + '&test=sandboxed-iframe-same-origin');
+      assert_true(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe-same-origin'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+   'allow-same-origin flag in the normal iframe');
+
+promise_test(t => {
+  let frame = sandboxed_frame;
+  return doTest(frame, 'fetch')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The fetch request should NOT be handled by SW.');
+    });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+   'flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame;
+  return doTest(frame, 'fetch-from-worker')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The fetch request should NOT be handled by SW.');
+    });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame;
+  return doTest(frame, 'iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(frame.src + '&test=iframe'));
+    });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame;
+  return doTest(frame, 'sandboxed-iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+   'sandboxed by an attribute with allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame;
+  return doTest(frame, 'sandboxed-iframe-same-origin')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe-same-origin'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+   'allow-same-origin flag in the iframe sandboxed by an attribute with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame;
+  return doTest(frame, 'fetch')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The fetch request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=fetch');
+    });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+   'and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame;
+  return doTest(frame, 'fetch-from-worker')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The fetch request should be handled by SW.');
+      assert_equals(requests[0],
+                    frame.src + '&test=fetch-from-worker');
+    });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+   'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame;
+  return doTest(frame, 'iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=iframe');
+      assert_true(data.clients.includes(frame.src + '&test=iframe'));
+    });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+   'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame;
+  return doTest(frame, 'sandboxed-iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+   'sandboxed by attribute with allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame;
+  return doTest(frame, 'sandboxed-iframe-same-origin')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0],
+                    frame.src + '&test=sandboxed-iframe-same-origin');
+      assert_true(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe-same-origin'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+   'allow-same-origin flag in the iframe sandboxed by attribute with ' +
+   'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame_by_header;
+  return doTest(frame, 'fetch')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+    });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame_by_header;
+  return doTest(frame, 'iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(frame.src + '&test=iframe'));
+    });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame_by_header;
+  return doTest(frame, 'sandboxed-iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+   'sandboxed by CSP HTTP header with allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_frame_by_header;
+  return doTest(frame, 'sandboxed-iframe-same-origin')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe-same-origin'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+   'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame_by_header;
+  return doTest(frame, 'fetch')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=fetch');
+    });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame_by_header;
+  return doTest(frame, 'iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=iframe');
+      assert_true(data.clients.includes(frame.src + '&test=iframe'));
+    });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame_by_header;
+  return doTest(frame, 'sandboxed-iframe')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+      assert_false(
+        data.clients.includes(frame.src + '&test=sandboxed-iframe'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the ' +
+   'iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+   'allow-same-origin flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame_by_header;
+  return doTest(frame, 'sandboxed-iframe-same-origin')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0],
+                    frame.src + '&test=sandboxed-iframe-same-origin');
+      assert_true(data.clients.includes(
+        frame.src + '&test=sandboxed-iframe-same-origin'));
+    });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+   'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+   'allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
new file mode 100644
index 0000000..70be6ef
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<title>Accessing navigator.serviceWorker in sandboxed iframe.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function postMessageAndWaitResult(frame) {
+  return new Promise(function(resolve, reject) {
+    var id = ++lastCallbackId;
+    callbacks[id] = resolve;
+    frame.contentWindow.postMessage({id:id}, '*');
+    const timeout = 1000;
+    step_timeout(() => reject("no msg back after " + timeout + "ms"), timeout);
+  });
+}
+
+window.onmessage = function(e) {
+  message = e.data;
+  var id = message['id'];
+  var callback = callbacks[id];
+  delete callbacks[id];
+  callback(message.result);
+};
+
+promise_test(function(t) {
+    var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+    var frame;
+    return with_iframe(url)
+      .then(function(f) {
+          frame = f;
+          add_result_callback(() => { frame.remove(); });
+          return postMessageAndWaitResult(f);
+        })
+      .then(function(result) {
+          assert_equals(result, 'ok');
+        });
+  }, 'Accessing navigator.serviceWorker in normal iframe should not throw.');
+
+promise_test(function(t) {
+    var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+    var frame;
+    return with_sandboxed_iframe(url, 'allow-scripts')
+      .then(function(f) {
+          frame = f;
+          add_result_callback(() => { frame.remove(); });
+          return postMessageAndWaitResult(f);
+        })
+      .then(function(result) {
+          assert_equals(
+              result,
+              'navigator.serviceWorker failed: SecurityError');
+        });
+  }, 'Accessing navigator.serviceWorker in sandboxed iframe should throw.');
+
+promise_test(function(t) {
+    var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+    var frame;
+    return with_sandboxed_iframe(url, 'allow-scripts allow-same-origin')
+      .then(function(f) {
+          frame = f;
+          add_result_callback(() => { frame.remove(); });
+          return postMessageAndWaitResult(f);
+        })
+      .then(function(result) {
+          assert_equals(result, 'ok');
+        });
+  },
+  'Accessing navigator.serviceWorker in sandboxed iframe with ' +
+  'allow-same-origin flag should not throw.');
+
+promise_test(function(t) {
+    var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+    var frame;
+    return new Promise(function(resolve) {
+          frame = document.createElement('iframe');
+          add_result_callback(() => { frame.remove(); });
+          frame.sandbox = '';
+          frame.src = url;
+          frame.onload = resolve;
+          document.body.appendChild(frame);
+          // Switch the sandbox attribute while loading the iframe.
+          frame.sandbox = 'allow-scripts allow-same-origin';
+        })
+      .then(function() {
+          return postMessageAndWaitResult(frame)
+        })
+      .then(function(result) {
+          // The HTML spec seems to say that changing the sandbox attribute
+          // after the iframe is inserted into its parent document does not
+          // affect the sandboxing. If that's true, the frame should still
+          // act as if it still doesn't have
+          // 'allow-scripts allow-same-origin' set and throw a SecurityError.
+          //
+          // 1) From Section 4.8.5 "The iframe element":
+          // "When an iframe element is inserted into a document that has a
+          // browsing context, the user agent must create a new browsing
+          // context..."
+          // 2) "Create a new browsing context" expands to Section 7.1
+          // "Browsing contexts", which includes creating a Document and
+          // "Implement the sandboxing for document."
+          // 3) "Implement the sandboxing" expands to Section 7.6 "Sandboxing",
+          // which includes "populate document's active sandboxing flag set".
+          //
+          // It's not clear whether navigation subsequently creates a new
+          // Document, but I'm assuming it wouldn't.
+          // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox
+          assert_true(
+              false,
+              'should NOT get message back from a sandboxed frame where scripts are not allowed to execute');
+        })
+      .catch(msg => {
+        assert_true(msg.startsWith('no msg back'), 'expecting error message "no msg back"');
+      });
+  }, 'Switching iframe sandbox attribute while loading the iframe');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html b/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html
new file mode 100644
index 0000000..666a5d3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Ensure service worker is bypassed in insecure contexts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// This test checks that an HTTPS iframe embedded in an HTTP document is not
+// loaded via a service worker, since it's not a secure context. To that end, we
+// first register a service worker, wait for its activation, and create an
+// iframe that is controlled by said service worker. We use the iframe as a
+// way to receive messages from the service worker.
+// The bulk of the test begins by opening an HTTP window with the noopener
+// option, installing a message event handler, and embedding an HTTPS iframe. If
+// the browser behaves correctly then the iframe will be loaded from the network
+// and will contain a script that posts a message to the parent window,
+// informing it that it was loaded from the network. If, however, the iframe is
+// intercepted, the service worker will return a page with a script that posts a
+// message to the parent window, informing it that it was intercepted.
+// Upon getting either result, the window will report the result to the service
+// worker by navigating to a reporting URL. The service worker will then inform
+// all clients about the result, including the controlled iframe from the
+// beginning of the test. The message event handler will verify that the result
+// is as expected, concluding the test.
+promise_test(t => {
+    const SCRIPT = "resources/secure-context-service-worker.js";
+    const SCOPE = "resources/";
+    const HTTP_IFRAME_URL = get_host_info().HTTP_ORIGIN + base_path() + SCOPE + "secure-context/window.html";
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+        .then(registration => {
+            t.add_cleanup(() => {
+                return registration.unregister();
+            });
+            return wait_for_state(t, registration.installing, 'activated');
+        })
+        .then(() => {
+            return with_iframe(SCOPE + "blank.html");
+        })
+        .then(iframe => {
+            t.add_cleanup(() => {
+                iframe.remove();
+            });
+            return new Promise(resolve => {
+                iframe.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+                    assert_equals(event.data, 'network');
+                    resolve();
+                });
+                window.open(HTTP_IFRAME_URL, 'MyWindow', 'noopener');
+            });
+        });
+})
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html
new file mode 100644
index 0000000..226f4a4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP connect directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+    'resources/service-worker-csp-worker.py?directive=connect',
+    'CSP test for connect-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html
new file mode 100644
index 0000000..1d4e762
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP default directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+    'resources/service-worker-csp-worker.py?directive=default',
+    'CSP test for default-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html
new file mode 100644
index 0000000..14c2eb7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP script directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+    'resources/service-worker-csp-worker.py?directive=script',
+    'CSP test for script-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html
new file mode 100644
index 0000000..fb902cd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  const script = 'resources/service-worker-header.py'
+    + '?header&import=service-worker-header.py?no-header';
+  const scope = 'resources/service-worker-header';
+  const expected_url = normalizeURL(script);
+  const registration =
+    await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(() => registration.unregister());
+  assert_true(registration instanceof ServiceWorkerRegistration);
+
+  await wait_for_state(t, registration.installing, 'activated');
+  await registration.update();
+}, 'A request to fetch service worker main script should have Service-Worker '
+  + 'header and imported scripts should not have one');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html b/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
new file mode 100644
index 0000000..fac8f20
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: ServiceWorkerMessageEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html';
+    var url = 'resources/postmessage-to-client-worker.js';
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          var w = frame.contentWindow;
+          var worker = w.navigator.serviceWorker.controller;
+          assert_equals(
+              self.ServiceWorkerMessageEvent, undefined,
+              'ServiceWorkerMessageEvent should not be defined.');
+          return new Promise(function(resolve) {
+              w.navigator.serviceWorker.onmessage = t.step_func(function(e) {
+                  assert_true(
+                      e instanceof w.MessageEvent,
+                      'message events should use MessageEvent interface.');
+                  assert_true(e.source instanceof w.ServiceWorker);
+                  assert_equals(e.type, 'message');
+                  assert_equals(e.source, worker,
+                                'source should equal to the controller.');
+                  assert_equals(e.ports.length, 0);
+                  resolve();
+                });
+              worker.postMessage('PING');
+            });
+        });
+  }, 'Test MessageEvent supplants ServiceWorkerMessageEvent.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html b/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
new file mode 100644
index 0000000..6004985
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>ServiceWorker object: scriptURL property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function url_test(name, url) {
+  const scope = 'resources/scope/' + name;
+  const expectedURL = normalizeURL(url);
+
+  promise_test(async t => {
+    const registration =
+        await service_worker_unregister_and_register(t, url, scope);
+    const worker = registration.installing;
+    assert_equals(worker.scriptURL, expectedURL, 'scriptURL');
+    await registration.unregister();
+  }, 'Verify the scriptURL property: ' + name);
+}
+
+url_test('relative', 'resources/empty-worker.js');
+url_test('with-fragment', 'resources/empty-worker.js#ref');
+url_test('with-query', 'resources/empty-worker.js?ref');
+url_test('absolute', normalizeURL('./resources/empty-worker.js'));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html
new file mode 100644
index 0000000..b604f65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting installed worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?skip-waiting-installed';
+    var url1 = 'resources/empty.js';
+    var url2 = 'resources/skip-waiting-installed-worker.js';
+    var frame, frame_sw, service_worker, registration, onmessage, oncontrollerchanged;
+    var saw_message = new Promise(function(resolve) {
+        onmessage = function(e) {
+            resolve(e.data);
+          };
+        })
+      .then(function(message) {
+          assert_equals(
+            message, 'PASS',
+            'skipWaiting promise should be resolved with undefined');
+        });
+    var saw_controllerchanged = new Promise(function(resolve) {
+        oncontrollerchanged = function() {
+            assert_equals(
+                frame_sw.controller.scriptURL, normalizeURL(url2),
+                'Controller scriptURL should change to the second one');
+            assert_equals(registration.active.scriptURL, normalizeURL(url2),
+                          'Worker which calls skipWaiting should be active by controllerchange');
+            resolve();
+        };
+      });
+    return service_worker_unregister_and_register(t, url1, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          frame_sw = f.contentWindow.navigator.serviceWorker;
+          assert_equals(
+              frame_sw.controller.scriptURL, normalizeURL(url1),
+              'Document controller scriptURL should equal to the first one');
+          frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+          return navigator.serviceWorker.register(url2, {scope: scope});
+        })
+      .then(function(r) {
+          registration = r;
+          service_worker = r.installing;
+          return wait_for_state(t, service_worker, 'installed');
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+          channel.port1.onmessage = t.step_func(onmessage);
+          service_worker.postMessage({port: channel.port2}, [channel.port2]);
+          return Promise.all([saw_message, saw_controllerchanged]);
+        })
+      .then(function() {
+          frame.remove();
+        });
+  }, 'Test skipWaiting when a installed worker is waiting');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html
new file mode 100644
index 0000000..412ee2a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?skip-waiting-using-registration';
+    var url1 = 'resources/empty.js';
+    var url2 = 'resources/skip-waiting-worker.js';
+    var frame, frame_sw, sw_registration, oncontrollerchanged;
+    var saw_controllerchanged = new Promise(function(resolve) {
+        oncontrollerchanged = function(e) {
+            resolve(e);
+          };
+        })
+      .then(function(e) {
+          assert_equals(e.type, 'controllerchange',
+                        'Event name should be "controllerchange"');
+          assert_true(
+              e.target instanceof frame.contentWindow.ServiceWorkerContainer,
+              'Event target should be a ServiceWorkerContainer');
+          assert_equals(e.target.controller.state, 'activating',
+                        'Controller state should be activating');
+          assert_equals(
+              frame_sw.controller.scriptURL, normalizeURL(url2),
+              'Controller scriptURL should change to the second one');
+        });
+
+    return service_worker_unregister_and_register(t, url1, scope)
+      .then(function(registration) {
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          frame = f;
+          frame_sw = f.contentWindow.navigator.serviceWorker;
+          assert_equals(
+              frame_sw.controller.scriptURL, normalizeURL(url1),
+              'Document controller scriptURL should equal to the first one');
+          frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+          return navigator.serviceWorker.register(url2, {scope: scope});
+        })
+      .then(function(registration) {
+          sw_registration = registration;
+          t.add_cleanup(function() {
+              return registration.unregister();
+            });
+          return saw_controllerchanged;
+        })
+      .then(function() {
+          assert_not_equals(sw_registration.active, null,
+                            'Registration active worker should not be null');
+          return fetch_tests_from_worker(sw_registration.active);
+        });
+  }, 'Test skipWaiting while a client is using the registration');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html
new file mode 100644
index 0000000..62060a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+    'resources/skip-waiting-worker.js',
+    'Test single skipWaiting() when no client attached');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
new file mode 100644
index 0000000..ced64e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?skip-waiting-without-using-registration';
+    var url = 'resources/skip-waiting-worker.js';
+    var frame_sw, sw_registration;
+
+    return service_worker_unregister(t, scope)
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          frame_sw = f.contentWindow.navigator.serviceWorker;
+          assert_equals(frame_sw.controller, null,
+                        'Document controller should be null');
+          return navigator.serviceWorker.register(url, {scope: scope});
+        })
+      .then(function(registration) {
+          sw_registration = registration;
+          t.add_cleanup(function() {
+              return registration.unregister();
+            });
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          assert_equals(frame_sw.controller, null,
+                        'Document controller should still be null');
+          assert_not_equals(sw_registration.active, null,
+                            'Registration active worker should not be null');
+          return fetch_tests_from_worker(sw_registration.active);
+        });
+  }, 'Test skipWaiting while a client is not being controlled');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html
new file mode 100644
index 0000000..f8392fc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+    var scope = 'resources/blank.html?skip-waiting';
+    var url1 = 'resources/empty.js';
+    var url2 = 'resources/empty-worker.js';
+    var url3 = 'resources/skip-waiting-worker.js';
+    var sw_registration, activated_worker, waiting_worker;
+    return service_worker_unregister_and_register(t, url1, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          sw_registration = registration;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          t.add_cleanup(function() {
+              f.remove();
+            });
+          return navigator.serviceWorker.register(url2, {scope: scope});
+        })
+      .then(function(registration) {
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          activated_worker = sw_registration.active;
+          waiting_worker = sw_registration.waiting;
+          assert_equals(activated_worker.scriptURL, normalizeURL(url1),
+                        'Worker with url1 should be activated');
+          assert_equals(waiting_worker.scriptURL, normalizeURL(url2),
+                        'Worker with url2 should be waiting');
+          return navigator.serviceWorker.register(url3, {scope: scope});
+        })
+      .then(function(registration) {
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          assert_equals(activated_worker.state, 'redundant',
+                        'Worker with url1 should be redundant');
+          assert_equals(waiting_worker.state, 'redundant',
+                        'Worker with url2 should be redundant');
+          assert_equals(sw_registration.active.scriptURL, normalizeURL(url3),
+                        'Worker with url3 should be activated');
+        });
+  }, 'Test skipWaiting with both active and waiting workers');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/state.https.html b/third_party/web_platform_tests/service-workers/service-worker/state.https.html
new file mode 100644
index 0000000..7358e58
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/state.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function (t) {
+    var currentState = 'test-is-starting';
+    var scope = 'resources/state/';
+
+    return service_worker_unregister_and_register(
+        t, 'resources/empty-worker.js', scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          var sw = registration.installing;
+
+          assert_equals(sw.state, 'installing',
+                        'the service worker should be in "installing" state.');
+          checkStateTransition(sw.state);
+          return onStateChange(sw);
+        });
+
+    function checkStateTransition(newState) {
+        switch (currentState) {
+        case 'test-is-starting':
+            break; // anything goes
+        case 'installing':
+            assert_in_array(newState, ['installed', 'redundant']);
+            break;
+        case 'installed':
+            assert_in_array(newState, ['activating', 'redundant']);
+            break;
+        case 'activating':
+            assert_in_array(newState, ['activated', 'redundant']);
+            break;
+        case 'activated':
+            assert_equals(newState, 'redundant');
+            break;
+        case 'redundant':
+            assert_unreached('a ServiceWorker should not transition out of ' +
+                             'the "redundant" state');
+            break;
+        default:
+            assert_unreached('should not transition into unknown state "' +
+                             newState + '"');
+            break;
+        }
+        currentState = newState;
+    }
+
+    function onStateChange(expectedTarget) {
+      return new Promise(function(resolve) {
+            expectedTarget.addEventListener('statechange', resolve);
+          }).then(function(event) {
+            assert_true(event.target instanceof ServiceWorker,
+                        'the target of the statechange event should be a ' +
+                        'ServiceWorker.');
+            assert_equals(event.target, expectedTarget,
+                          'the target of the statechange event should be ' +
+                          'the installing ServiceWorker');
+            assert_equals(event.type, 'statechange',
+                          'the type of the event should be "statechange".');
+
+            checkStateTransition(event.target.state);
+
+            if (event.target.state != 'activated')
+                return onStateChange(expectedTarget);
+          });
+    }
+}, 'Service Worker state property and "statechange" event');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html b/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html
new file mode 100644
index 0000000..3710ee6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Service worker interception does not break SVG fragment targets</title>
+<meta name="assert" content="SVG with link fragment should render correctly when intercepted by a service worker.">
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="match" href="resources/svg-target-reftest-001.html">
+<p>Pass if you see a green box below.</p>
+<script>
+// We want to use utility functions designed for testharness.js where
+// there is a test object.  We don't have a test object in reftests
+// so fake one for now.
+const fake_test = { step_func: f => f };
+
+async function runTest() {
+  const script = './resources/pass-through-worker.js';
+  const scope = './resources/svg-target-reftest-frame.html';
+  let reg = await navigator.serviceWorker.register(script, { scope });
+  await wait_for_state(fake_test, reg.installing, 'activated');
+  let f = await with_iframe(scope);
+  document.documentElement.classList.remove('reftest-wait');
+  await reg.unregister();
+  // Note, we cannot remove the frame explicitly because we can't
+  // tell when the reftest completes.
+}
+runTest();
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html b/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html
new file mode 100644
index 0000000..0e9f63a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<title>ServiceWorker: worker objects have synced state</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests that ServiceWorker objects representing the same Service Worker
+// entity have the same state. JS-level equality is now required according to
+// the spec.
+'use strict';
+
+function nextChange(worker) {
+  return new Promise(function(resolve, reject) {
+      worker.addEventListener('statechange', function handler(event) {
+          try {
+            worker.removeEventListener('statechange', handler);
+            resolve(event.currentTarget.state);
+          } catch (err) {
+            reject(err);
+          }
+        });
+    });
+}
+
+promise_test(function(t) {
+    var scope = 'resources/synced-state';
+    var script = 'resources/empty-worker.js';
+    var registration, worker;
+
+    return service_worker_unregister_and_register(t, script, scope)
+      .then(function(r) {
+          registration = r;
+          worker = registration.installing;
+
+          t.add_cleanup(function() {
+              return r.unregister();
+            });
+
+          return nextChange(worker);
+        })
+      .then(function(state) {
+          assert_equals(state, 'installed',
+                        'original SW should be installed');
+          assert_equals(registration.installing, null,
+                        'in installed, .installing should be null');
+          assert_equals(registration.waiting, worker,
+                        'in installed, .waiting should be equal to the ' +
+                          'original worker');
+          assert_equals(registration.waiting.state, 'installed',
+                        'in installed, .waiting should be installed');
+          assert_equals(registration.active, null,
+                        'in installed, .active should be null');
+
+          return nextChange(worker);
+        })
+      .then(function(state) {
+          assert_equals(state, 'activating',
+                        'original SW should be activating');
+          assert_equals(registration.installing, null,
+                        'in activating, .installing should be null');
+          assert_equals(registration.waiting, null,
+                        'in activating, .waiting should be null');
+          assert_equals(registration.active, worker,
+                        'in activating, .active should be equal to the ' +
+                          'original worker');
+          assert_equals(
+              registration.active.state, 'activating',
+              'in activating, .active should be activating');
+
+          return nextChange(worker);
+        })
+      .then(function(state) {
+          assert_equals(state, 'activated',
+                        'original SW should be activated');
+          assert_equals(registration.installing, null,
+                        'in activated, .installing should be null');
+          assert_equals(registration.waiting, null,
+                        'in activated, .waiting should be null');
+          assert_equals(registration.active, worker,
+                        'in activated, .active should be equal to the ' +
+                          'original worker');
+          assert_equals(registration.active.state, 'activated',
+                        'in activated .active should be activated');
+        })
+      .then(function() {
+          return navigator.serviceWorker.getRegistration(scope);
+        })
+      .then(function(r) {
+          assert_equals(r, registration, 'getRegistration should return the ' +
+                                         'same object');
+        });
+  }, 'worker objects for the same entity have the same state');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html b/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html
new file mode 100644
index 0000000..e22ca8f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function fetch_url(url) {
+  return new Promise(function(resolve, reject) {
+      var request = new XMLHttpRequest();
+      request.addEventListener('load', function(event) {
+          if (request.status == 200)
+            resolve(request.response);
+          else
+            reject(Error(request.statusText));
+        });
+      request.open('GET', url);
+      request.send();
+    });
+}
+var worker = 'resources/fail-on-fetch-worker.js';
+
+promise_test(function(t) {
+    var scope = 'resources/scope/uncontrolled-page/';
+    return service_worker_unregister_and_register(t, worker, scope)
+      .then(function(reg) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, reg.installing, 'activated');
+        })
+      .then(function() {
+          return fetch_url('resources/simple.txt');
+        })
+      .then(function(text) {
+          assert_equals(text, 'a simple text file\n');
+        });
+  }, 'Fetch events should not go through uncontrolled page.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html
new file mode 100644
index 0000000..3bf4cff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/simple-intercept-worker.js';
+
+async_test(function(t) {
+    var scope =
+        'resources/unregister-controller-page.html?load-before-unregister';
+    var frame_window;
+    var controller;
+    var registration;
+    var frame;
+
+    service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          frame_window = frame.contentWindow;
+          controller = frame_window.navigator.serviceWorker.controller;
+          assert_true(controller instanceof frame_window.ServiceWorker,
+                      'document should load with a controller');
+          return registration.unregister();
+        })
+      .then(function() {
+          assert_equals(frame_window.navigator.serviceWorker.controller,
+                        controller,
+                        'unregistration should not modify controller');
+          return frame_window.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'intercepted by service worker',
+                        'controller should intercept requests');
+          frame.remove();
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Unregister does not affect existing controller');
+
+async_test(function(t) {
+    var scope =
+        'resources/unregister-controller-page.html?load-after-unregister';
+    var registration;
+    var frame;
+
+    service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return registration.unregister();
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(f) {
+          frame = f;
+          var frame_window = frame.contentWindow;
+          assert_equals(frame_window.navigator.serviceWorker.controller, null,
+                        'document should not have a controller');
+          return frame_window.fetch_url('simple.txt');
+        })
+      .then(function(response) {
+          assert_equals(response, 'a simple text file\n',
+                        'requests should not be intercepted');
+          frame.remove();
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Unregister prevents control of subsequent navigations');
+
+async_test(function(t) {
+    var scope =
+        'resources/scope/no-new-controllee-even-if-registration-is-still-used';
+    var registration;
+
+    service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          registration = r;
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          return registration.unregister();
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+                        null,
+                        'document should not have a controller');
+          frame.remove();
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Unregister prevents new controllee even if registration is still in use');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
new file mode 100644
index 0000000..79cdaf0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installing' or 'parsed'.  Clear-Site-Data must delete the registration,
+// abort the installation and then clear the registration by setting the
+// worker's state to 'redundant'.
+
+promise_test(async test => {
+  // This test keeps the the service worker in the 'parsed' state by using a
+  // script with an infinite loop.
+  const script_url = 'resources/onparse-infiniteloop-worker.js';
+  const scope_url =
+    'resources/scope-for-unregister-immediately-with-parsed-worker';
+
+  await service_worker_unregister(test, /*scope=*/script_url);
+
+  // Clear-Site-Data must cause register() to fail.
+  const register_promise = promise_rejects_dom(test, 'AbortError',
+    navigator.serviceWorker.register(script_url, { scope: scope_url}));;
+
+  await Promise.all([clear_site_data(), register_promise]);
+
+  await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must abort service worker registration.');
+
+promise_test(async test => {
+  // This test keeps the the service worker in the 'installing' state by using a
+  // script with an install event waitUntil() promise that never resolves.
+  const script_url = 'resources/oninstall-waituntil-forever.js';
+  const scope_url =
+    'resources/scope-for-unregister-immediately-with-installing-worker';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+  const service_worker = registration.installing;
+
+  // Clear-Site-Data must cause install to fail.
+  await Promise.all([
+    clear_site_data(),
+    wait_for_state(test, service_worker, 'redundant')]);
+
+  await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+     + 'in the "installing" state.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
new file mode 100644
index 0000000..6ba87a7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker that has pending
+// extendable events.  Clear-Site-Data must delete the registration,
+// abort all pending extendable events and then clear the registration by
+// setting the worker's state to 'redundant'
+
+promise_test(async test => {
+  // Use a service worker script that can produce fetch events with pending
+  // respondWith() promises that never resolve.
+  const script_url = 'resources/onfetch-waituntil-forever.js';
+  const scope_url =
+    'resources/blank.html?unregister-immediately-with-fetch-event';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+
+  await wait_for_state(test, registration.installing, 'activated');
+
+  const frame = await add_controlled_iframe(test, scope_url);
+
+  // Clear-Site-Data must cause the pending fetch promise to reject.
+  const fetch_promise = promise_rejects_js(
+    test, TypeError, frame.contentWindow.fetch('waituntil-forever'));
+
+  const event_watcher = new EventWatcher(
+    test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+  await Promise.all([
+    clear_site_data(),
+    fetch_promise,
+    event_watcher.wait_for('controllerchange'),
+    wait_for_state(test, registration.active, 'redundant'),]);
+
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+  await assert_no_registrations_exist();
+}, 'Clear-Site-Data must fail pending subresource fetch events.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html
new file mode 100644
index 0000000..54be40a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installed', 'waiting', 'activating' or 'activated'.  Immediately
+// unregistering runs the "Clear Registration" algorithm without waiting for the
+// active worker's controlled clients to unload.
+
+promise_test(async test => {
+  // This test keeps the the service worker in the 'activating' state by using a
+  // script with an activate event waitUntil() promise that never resolves.
+  const script_url = 'resources/onactivate-waituntil-forever.js';
+  const scope_url =
+    'resources/scope-for-unregister-immediately-with-waiting-worker';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+  const service_worker = registration.installing;
+
+  await wait_for_state(test, service_worker, 'activating');
+
+  // Clear-Site-Data must cause activation to fail.
+  await Promise.all([
+    clear_site_data(),
+    wait_for_state(test, service_worker, 'redundant')]);
+
+  await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+     + 'in the "activating" state.');
+
+promise_test(async test => {
+  // Create an registration with two service workers: one activated and one
+  // installed.
+  const script_url = 'resources/update_shell.py';
+  const scope_url =
+    'resources/scope-for-unregister-immediately-with-with-update';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+  const first_service_worker = registration.installing;
+
+  await wait_for_state(test, first_service_worker, 'activated');
+  registration.update();
+
+  const event_watcher = new EventWatcher(test, registration, 'updatefound');
+  await event_watcher.wait_for('updatefound');
+
+  const second_service_worker = registration.installing;
+  await wait_for_state(test, second_service_worker, 'installed');
+
+  // Clear-Site-Data must clear both workers from the registration.
+  await Promise.all([
+    clear_site_data(),
+    wait_for_state(test, first_service_worker, 'redundant'),
+    wait_for_state(test, second_service_worker, 'redundant')]);
+
+  await assert_no_registrations_exist();
+}, 'Clear-Site-Data must unregister an activated registration with '
+    + 'an update waiting.');
+
+promise_test(async test => {
+  const script_url = 'resources/empty.js';
+  const scope_url =
+    'resources/blank.html?unregister-immediately-with-controlled-client';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+  const service_worker = registration.installing;
+
+  await wait_for_state(test, service_worker, 'activated');
+  const frame = await add_controlled_iframe(test, scope_url);
+  const frame_registration =
+    await frame.contentWindow.navigator.serviceWorker.ready;
+
+  const event_watcher = new EventWatcher(
+    test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+  // Clear-Site-Data must remove the iframe's controller.
+  await Promise.all([
+    clear_site_data(),
+    event_watcher.wait_for('controllerchange'),
+    wait_for_state(test, service_worker, 'redundant')]);
+
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+  await assert_no_registrations_exist();
+
+  // The ready promise must continue to resolve with the unregistered
+  // registration.
+  assert_equals(frame_registration,
+    await frame.contentWindow.navigator.serviceWorker.ready);
+}, 'Clear-Site-Data must unregister an activated registration with controlled '
+   + 'clients.');
+
+promise_test(async test => {
+  const script_url = 'resources/empty.js';
+  const scope_url =
+    'resources/blank.html?unregister-immediately-while-waiting-to-clear';
+
+  const registration = await service_worker_unregister_and_register(
+    test, script_url, scope_url);
+  const service_worker = registration.installing;
+
+  await wait_for_state(test, service_worker, 'activated');
+  const frame = await add_controlled_iframe(test, scope_url);
+
+  const event_watcher = new EventWatcher(
+    test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+  // Unregister waits to clear the registration until no controlled clients
+  // exist.
+  await registration.unregister();
+
+  // Clear-Site-Data must clear the unregistered registration immediately.
+  await Promise.all([
+    clear_site_data(),
+    event_watcher.wait_for('controllerchange'),
+    wait_for_state(test, service_worker, 'redundant')]);
+
+  assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+  await assert_no_registrations_exist();
+}, 'Clear-Site-Data must clear an unregistered registration waiting for '
+   + ' controlled clients to unload.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html
new file mode 100644
index 0000000..d046423
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/unregister-then-register-new-script-that-exists';
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  const newWorkerURL = worker_url + '?new';
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const iframe = await with_iframe(scope);
+  t.add_cleanup(() => iframe.remove());
+
+  await registration.unregister();
+
+  const newRegistration = await navigator.serviceWorker.register(newWorkerURL, { scope });
+  t.add_cleanup(() => newRegistration.unregister());
+
+  assert_equals(
+    registration.installing,
+    null,
+    'before activated registration.installing'
+  );
+  assert_equals(
+    registration.waiting,
+    null,
+    'before activated registration.waiting'
+  );
+  assert_equals(
+    registration.active.scriptURL,
+    normalizeURL(worker_url),
+    'before activated registration.active'
+  );
+  assert_equals(
+    newRegistration.installing.scriptURL,
+    normalizeURL(newWorkerURL),
+    'before activated newRegistration.installing'
+  );
+  assert_equals(
+    newRegistration.waiting,
+    null,
+    'before activated newRegistration.waiting'
+  );
+  assert_equals(
+    newRegistration.active,
+    null,
+    'before activated newRegistration.active'
+  );
+  iframe.remove();
+
+  await wait_for_state(t, newRegistration.installing, 'activated');
+
+  assert_equals(
+    newRegistration.installing,
+    null,
+    'after activated newRegistration.installing'
+  );
+  assert_equals(
+    newRegistration.waiting,
+    null,
+    'after activated newRegistration.waiting'
+  );
+  assert_equals(
+    newRegistration.active.scriptURL,
+    normalizeURL(newWorkerURL),
+    'after activated newRegistration.active'
+  );
+
+  const newIframe = await with_iframe(scope);
+  t.add_cleanup(() => newIframe.remove());
+
+  assert_equals(
+    newIframe.contentWindow.navigator.serviceWorker.controller.scriptURL,
+    normalizeURL(newWorkerURL),
+    'the new worker should control a new document'
+  );
+}, 'Registering a new script URL while an unregistered registration is in use');
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/unregister-then-register-new-script-that-404s';
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const iframe = await with_iframe(scope);
+  t.add_cleanup(() => iframe.remove());
+
+  await registration.unregister();
+
+  await promise_rejects_js(
+    t, TypeError,
+    navigator.serviceWorker.register('this-will-404', { scope })
+  );
+
+  assert_equals(registration.installing, null, 'registration.installing');
+  assert_equals(registration.waiting, null, 'registration.waiting');
+  assert_equals(registration.active.scriptURL, normalizeURL(worker_url), 'registration.active');
+
+  const newIframe = await with_iframe(scope);
+  t.add_cleanup(() => newIframe.remove());
+
+  assert_equals(newIframe.contentWindow.navigator.serviceWorker.controller, null, 'Document should not be controlled');
+}, 'Registering a new script URL that 404s does not resurrect unregistered registration');
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/unregister-then-register-reject-install-worker';
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const iframe = await with_iframe(scope);
+  t.add_cleanup(() => iframe.remove());
+
+  await registration.unregister();
+
+  const newRegistration = await navigator.serviceWorker.register(
+    'resources/reject-install-worker.js', { scope }
+  );
+  t.add_cleanup(() => newRegistration.unregister());
+
+  await wait_for_state(t, newRegistration.installing, 'redundant');
+
+  assert_equals(registration.installing, null, 'registration.installing');
+  assert_equals(registration.waiting, null, 'registration.waiting');
+  assert_equals(registration.active.scriptURL, normalizeURL(worker_url),
+                'registration.active');
+  assert_not_equals(registration, newRegistration, 'New registration is different');
+}, 'Registering a new script URL that fails to install does not resurrect unregistered registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html
new file mode 100644
index 0000000..b61608c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+    const scope = 'resources/scope/re-register-resolves-to-new-value';
+    const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+    t.add_cleanup(() => registration.unregister());
+
+    await wait_for_state(t, registration.installing, 'activated');
+    await registration.unregister();
+    const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+    t.add_cleanup(() => newRegistration.unregister());
+
+    assert_not_equals(
+      registration, newRegistration,
+      'register should resolve to a new value'
+    );
+  }, 'Unregister then register resolves to a new value');
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/re-register-while-old-registration-in-use';
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activated');
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+
+  await registration.unregister();
+  const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+  t.add_cleanup(() => newRegistration.unregister());
+
+  assert_not_equals(
+    registration, newRegistration,
+    'Unregister and register should always create a new registration'
+  );
+}, 'Unregister then register does not resolve to the original value even if the registration is in use.');
+
+promise_test(function(t) {
+    var scope = 'resources/scope/re-register-does-not-affect-existing-controllee';
+    var iframe;
+    var registration;
+    var controller;
+
+    return service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+            return service_worker_unregister(t, scope);
+          });
+
+          registration = r;
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          iframe = frame;
+          controller = iframe.contentWindow.navigator.serviceWorker.controller;
+          return registration.unregister();
+        })
+      .then(function() {
+          return navigator.serviceWorker.register(worker_url, { scope: scope });
+        })
+      .then(function(newRegistration) {
+          assert_equals(registration.installing, null,
+                        'installing version is null');
+          assert_equals(registration.waiting, null, 'waiting version is null');
+          assert_equals(
+              iframe.contentWindow.navigator.serviceWorker.controller,
+              controller,
+              'the worker from the first registration is the controller');
+          iframe.remove();
+        });
+  }, 'Unregister then register does not affect existing controllee');
+
+promise_test(async function(t) {
+  const scope = 'resources/scope/resurrection';
+  const altWorkerURL = worker_url + '?alt';
+  const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+  t.add_cleanup(() => registration.unregister());
+
+  await wait_for_state(t, registration.installing, 'activating');
+  const iframe = await with_iframe(scope);
+  t.add_cleanup(() => iframe.remove());
+
+  await registration.unregister();
+  const newRegistration = await navigator.serviceWorker.register(altWorkerURL, { scope });
+  t.add_cleanup(() => newRegistration.unregister());
+
+  assert_equals(newRegistration.active, null, 'Registration is new');
+
+  await wait_for_state(t, newRegistration.installing, 'activating');
+
+  const newIframe = await with_iframe(scope);
+  t.add_cleanup(() => newIframe.remove());
+
+  const iframeController = iframe.contentWindow.navigator.serviceWorker.controller;
+  const newIframeController = newIframe.contentWindow.navigator.serviceWorker.controller;
+
+  assert_not_equals(iframeController, newIframeController, 'iframes have different controllers');
+}, 'Unregister then register does not resurrect the registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html
new file mode 100644
index 0000000..492aecb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+    var scope = 'resources/scope/unregister-twice';
+    var registration;
+    navigator.serviceWorker.register('resources/empty-worker.js',
+                                     {scope: scope})
+      .then(function(r) {
+          registration = r;
+          return registration.unregister();
+        })
+      .then(function() {
+          return registration.unregister();
+        })
+      .then(function(value) {
+          assert_equals(value, false,
+                        'unregistering twice should resolve with false');
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Unregister twice');
+
+async_test(function(t) {
+    var scope = 'resources/scope/successful-unregister/';
+    navigator.serviceWorker.register('resources/empty-worker.js',
+                                     {scope: scope})
+      .then(function(registration) {
+          return registration.unregister();
+        })
+      .then(function(value) {
+          assert_equals(value, true,
+                        'unregistration should resolve with true');
+          t.done();
+        })
+      .catch(unreached_rejection(t));
+  }, 'Register then unregister');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
new file mode 100644
index 0000000..ff51f7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Service Worker: Update should be triggered after a navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function cleanup(frame, registration) {
+  if (frame)
+    frame.remove();
+  if (registration)
+   await registration.unregister();
+}
+
+promise_test(async t => {
+  const script = 'resources/update_shell.py?filename=empty.js';
+  const scope = 'resources/scope/update';
+  let registration;
+  let frame;
+
+  async function run() {
+    registration = await service_worker_unregister_and_register(
+        t, script, scope);
+    await wait_for_state(t, registration.installing, 'activated');
+
+    // Navigation should trigger update.
+    frame = await with_iframe(scope);
+    await wait_for_update(t, registration);
+  }
+
+  try {
+    await run();
+  } finally {
+    await cleanup(frame, registration);
+  }
+}, 'Update should be triggered after a navigation (no fetch event worker).');
+
+promise_test(async t => {
+  const script = 'resources/update_shell.py?filename=simple-intercept-worker.js';
+  const scope = 'resources/scope/update';
+  let registration;
+  let frame;
+
+  async function run() {
+    registration = await service_worker_unregister_and_register(
+        t, script, scope);
+    await wait_for_state(t, registration.installing, 'activated');
+
+    // Navigation should trigger update (network fallback).
+    frame = await with_iframe(scope + '?ignore');
+    await wait_for_update(t, registration);
+
+    // Navigation should trigger update (respondWith called).
+    frame.src = scope + '?string';
+    await wait_for_update(t, registration);
+  }
+
+  try {
+    await run();
+  } finally {
+    await cleanup(frame, registration);
+  }
+}, 'Update should be triggered after a navigation (fetch event worker).');
+
+promise_test(async t => {
+  const script = 'resources/update_shell.py?filename=empty.js';
+  const scope = 'resources/';
+  let registration;
+  let frame;
+
+  async function run() {
+    registration = await service_worker_unregister_and_register(
+        t, script, scope);
+    await wait_for_state(t, registration.installing, 'activated');
+
+    // Navigation should trigger update. Don't use with_iframe as it waits for
+    // the onload event.
+    frame = document.createElement('iframe');
+    frame.src = 'resources/malformed-http-response.asis';
+    document.body.appendChild(frame);
+    await wait_for_update(t, registration);
+  }
+
+  try {
+    await run();
+  } finally {
+    await cleanup(frame, registration);
+  }
+}, 'Update should be triggered after a navigation (network error).');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html
new file mode 100644
index 0000000..6e821fe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update should be triggered after redirects during navigation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+  // This test does a navigation that goes through a redirect chain. Each
+  // request in the chain has a service worker. Each service worker has no
+  // fetch event handler. The redirects are performed by redirect.py.
+  const script = 'resources/update-nocookie-worker.py';
+  const scope1 = 'resources/redirect.py?scope1';
+  const scope2 = 'resources/redirect.py?scope2';
+  const scope3 = 'resources/empty.html';
+  let registration1;
+  let registration2;
+  let registration3;
+  let frame;
+
+  async function cleanup() {
+    if (frame)
+      frame.remove();
+    if (registration1)
+      return registration1.unregister();
+    if (registration2)
+      return registration2.unregister();
+    if (registration3)
+      return registration3.unregister();
+  }
+
+  async function make_active_registration(scope) {
+    const registration =
+        await service_worker_unregister_and_register(t, script, scope);
+    await wait_for_state(t, registration.installing, 'activated');
+    return registration;
+  }
+
+  async function run() {
+    // Make the registrations.
+    registration1 = await make_active_registration(scope1);
+    registration2 = await make_active_registration(scope2);
+    registration3 = await make_active_registration(scope3);
+
+    // Make the promises that resolve on update.
+    const saw_update1 = wait_for_update(t, registration1);
+    const saw_update2 = wait_for_update(t, registration2);
+    const saw_update3 = wait_for_update(t, registration3);
+
+    // Create a URL for the redirect chain: scope1 -> scope2 -> scope3.
+    // Build the URL in reverse order.
+    let url = `${base_path()}${scope3}`;
+    url = `${base_path()}${scope2}&Redirect=${encodeURIComponent(url)}`
+    url = `${base_path()}${scope1}&Redirect=${encodeURIComponent(url)}`
+
+    // Navigate to the URL.
+    frame = await with_iframe(url);
+
+    // Each registration should update.
+    await saw_update1;
+    await saw_update2;
+    await saw_update3;
+  }
+
+  try {
+    await run();
+  } finally {
+    await cleanup();
+  }
+}, 'service workers are updated on redirects during navigation');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html
new file mode 100644
index 0000000..e7a8aa4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- This test requires browser to treat all registrations are older than 24 hours.
+     Preference 'dom.serviceWorkers.testUpdateOverOneDay' should be enabled during
+     the execution of the test -->
+<title>Service Worker: Functional events should trigger update if last update time is over 24 hours</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+    var script = 'resources/update-nocookie-worker.py';
+    var scope = 'resources/update/update-after-oneday.https.html';
+    var expected_url = normalizeURL(script);
+    var registration;
+    var frame;
+
+    return service_worker_unregister_and_register(t, expected_url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(scope); })
+      .then(function(f) {
+          frame = f;
+          return wait_for_update(t, registration);
+        })
+      .then(function() {
+          assert_equals(registration.installing.scriptURL, expected_url,
+                        'new installing should be set after update resolves.');
+          assert_equals(registration.waiting, null,
+                        'waiting should still be null after update resolves.');
+          assert_equals(registration.active.scriptURL, expected_url,
+                        'active should still exist after update found.');
+          return wait_for_state(t, registration.installing, 'installed');
+        })
+      .then(function() {
+          // Trigger a non-navigation fetch event
+          frame.contentWindow.load_image(normalizeURL('resources/update/sample'));
+          return wait_for_update(t, registration);
+       })
+       .then(function() {
+          frame.remove();
+       })
+  }, 'Update should be triggered after a functional event when last update time is over 24 hours');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
new file mode 100644
index 0000000..121a737
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains cors cases only.
+
+/*
+ * @param string main
+ *   Decide the content of the main script, where 'default' is for constant
+ *   content while 'time' is for time-variant content.
+ * @param string imported
+ *   Decide the content of the imported script, where 'default' is for constant
+ *   content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+                  {main: 'default', imported: 'time'   },
+                  {main: 'time',    imported: 'default'},
+                  {main: 'time',    imported: 'time'   }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+  promise_test(async (t) => {
+    // Specify a cross origin path to load imported scripts from a cross origin.
+    const path = host_info.HTTPS_REMOTE_ORIGIN +
+                 '/service-workers/service-worker/resources/';
+    const script = 'resources/bytecheck-worker.py' +
+                   '?main=' + main +
+                   '&imported=' + imported +
+                   '&path=' + path +
+                   '&type=classic';
+    const scope = 'resources/blank.html';
+
+    // Register a service worker.
+    const swr = await service_worker_unregister_and_register(t, script, scope);
+    t.add_cleanup(() => swr.unregister());
+    const sw = await wait_for_update(t, swr);
+    await wait_for_state(t, sw, 'activated');
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+
+    // Update the service worker registration.
+    await swr.update();
+
+    // If there should be a new service worker.
+    if (main === 'time' || imported === 'time') {
+      return wait_for_update(t, swr);
+    }
+    // Otherwise, make sure there is no newly created service worker.
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+  }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+  promise_test(async (t) => {
+    // Specify a cross origin path to load imported scripts from a cross origin.
+    const path = host_info.HTTPS_REMOTE_ORIGIN +
+                 '/service-workers/service-worker/resources/';
+    const script = 'resources/bytecheck-worker.py' +
+                   '?main=' + main +
+                   '&imported=' + imported +
+                   '&path=' + path +
+                   '&type=module';
+    const scope = 'resources/blank.html';
+
+    // Register a service worker.
+    const swr = await service_worker_unregister_and_register(t, script, scope, {type: 'module'});
+    t.add_cleanup(() => swr.unregister());
+    const sw = await wait_for_update(t, swr);
+    await wait_for_state(t, sw, 'activated');
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+
+    // Update the service worker registration.
+    await swr.update();
+
+    // If there should be a new service worker.
+    if (main === 'time' || imported === 'time') {
+      return wait_for_update(t, swr);
+    }
+    // Otherwise, make sure there is no newly created service worker.
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+  }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html
new file mode 100644
index 0000000..3e5a28b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains non-cors cases only.
+
+/*
+ * @param string main
+ *   Decide the content of the main script, where 'default' is for constant
+ *   content while 'time' is for time-variant content.
+ * @param string imported
+ *   Decide the content of the imported script, where 'default' is for constant
+ *   content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+                  {main: 'default', imported: 'time'   },
+                  {main: 'time',    imported: 'default'},
+                  {main: 'time',    imported: 'time'   }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+  promise_test(async (t) => {
+    // Empty path results in the same origin imported scripts.
+    const path = '';
+    const script = 'resources/bytecheck-worker.py' +
+                   '?main=' + main +
+                   '&imported=' + imported +
+                   '&path=' + path +
+                   '&type=classic';
+    const scope = 'resources/blank.html';
+
+    // Register a service worker.
+    const swr = await service_worker_unregister_and_register(t, script, scope);
+    t.add_cleanup(() => swr.unregister());
+    const sw = await wait_for_update(t, swr);
+    await wait_for_state(t, sw, 'activated');
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+
+    // Update the service worker registration.
+    await swr.update();
+
+    // If there should be a new service worker.
+    if (main === 'time' || imported === 'time') {
+      return wait_for_update(t, swr);
+    }
+    // Otherwise, make sure there is no newly created service worker.
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+  }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+  promise_test(async (t) => {
+    // Empty path results in the same origin imported scripts.
+    const path = './';
+    const script = 'resources/bytecheck-worker.py' +
+                   '?main=' + main +
+                   '&imported=' + imported +
+                   '&path=' + path +
+                   '&type=module';
+    const scope = 'resources/blank.html';
+
+    // Register a module service worker.
+    const swr = await service_worker_unregister_and_register(t, script, scope,
+                                                            {type: 'module'});
+
+    t.add_cleanup(() => swr.unregister());
+    const sw = await wait_for_update(t, swr);
+    await wait_for_state(t, sw, 'activated');
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+
+    // Update the service worker registration.
+    await swr.update();
+
+    // If there should be a new service worker.
+    if (main === 'time' || imported === 'time') {
+      return wait_for_update(t, swr);
+    }
+    // Otherwise, make sure there is no newly created service worker.
+    assert_array_equals([swr.active, swr.waiting, swr.installing],
+                        [sw, null, null]);
+  }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html
new file mode 100644
index 0000000..a2df529
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts ignored error</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This file contains tests to check if imported scripts appropriately updated.
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_update_worker_from_file(
+    t, initial_worker, updated_worker) {
+  const key = token();
+  const worker_url = `resources/update-worker-from-file.py?` +
+      `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+  const expected_url = normalizeURL(worker_url);
+
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, SCOPE);
+  await wait_for_state(t, registration.installing, 'activated');
+  assert_equals(registration.installing, null,
+                'prepare_ready: installing');
+  assert_equals(registration.waiting, null,
+                'prepare_ready: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'prepare_ready: active');
+  return [registration, expected_url];
+}
+
+// Create a service worker using the script under resources/.
+async function prepare_ready_normal_worker(t, filename, additional_params='') {
+  const key = token();
+  const worker_url = `resources/${filename}?Key=${key}&${additional_params}`;
+  const expected_url = normalizeURL(worker_url);
+
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, SCOPE);
+  await wait_for_state(t, registration.installing, 'activated');
+  assert_equals(registration.installing, null,
+                'prepare_ready: installing');
+  assert_equals(registration.waiting, null,
+                'prepare_ready: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'prepare_ready: active');
+  return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+  assert_equals(registration.installing.scriptURL, expected_url,
+                'assert_installing_and_active: installing');
+  assert_equals(registration.waiting, null,
+                'assert_installing_and_active: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+  assert_equals(registration.installing, null,
+                'assert_waiting_and_active: installing');
+  assert_equals(registration.waiting.scriptURL, expected_url,
+                'assert_waiting_and_active: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+  assert_equals(registration.installing, null,
+                'assert_active_only: installing');
+  assert_equals(registration.waiting, null,
+                'assert_active_only: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_active_only: active');
+}
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_update_worker_from_file(
+          t, 'empty.js', 'import-scripts-404.js');
+  t.add_cleanup(() => registration.unregister());
+
+  await promise_rejects_js(t, TypeError, registration.update());
+  assert_active_only(registration, expected_url);
+}, 'update() should fail when a new worker imports an unavailable script.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_update_worker_from_file(
+          t, 'import-scripts-404-after-update.js', 'empty.js');
+  t.add_cleanup(() => registration.unregister());
+
+  await Promise.all([registration.update(), wait_for_update(t, registration)]);
+  assert_installing_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.installing, 'installed');
+  assert_waiting_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.waiting, 'activated');
+  assert_active_only(registration, expected_url);
+}, 'update() should succeed when the old imported script no longer exist but ' +
+   "the new worker doesn't import it.");
+
+promise_test(async t => {
+  const [registration, expected_url] = await prepare_ready_normal_worker(
+    t, 'import-scripts-404-after-update.js');
+  t.add_cleanup(() => registration.unregister());
+
+  await registration.update();
+  assert_active_only(registration, expected_url);
+}, 'update() should treat 404 on imported scripts as no change.');
+
+promise_test(async t => {
+  const [registration, expected_url] = await prepare_ready_normal_worker(
+    t, 'import-scripts-404-after-update-plus-update-worker.js',
+       `AdditionalKey=${token()}`);
+  t.add_cleanup(() => registration.unregister());
+
+  await promise_rejects_js(t, TypeError, registration.update());
+  assert_active_only(registration, expected_url);
+}, 'update() should find an update in an imported script but update() should ' +
+   'result in failure due to missing the other imported script.');
+
+promise_test(async t => {
+  const [registration, expected_url] = await prepare_ready_normal_worker(
+    t, 'import-scripts-cross-origin-worker.sub.js');
+  t.add_cleanup(() => registration.unregister());
+  await registration.update();
+  assert_installing_and_active(registration, expected_url);
+}, 'update() should work with cross-origin importScripts.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html
new file mode 100644
index 0000000..66e8bfa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: update with missing importScripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+/**
+ * Test ServiceWorkerRegistration.update() when importScripts in a service worker
+ * script is no longer available (but was initially).
+ */
+let registration = null;
+
+promise_test(async (test) => {
+  const script = `resources/update-missing-import-scripts-main-worker.py?key=${token()}`;
+  const scope = 'resources/update-missing-import-scripts';
+
+  registration = await service_worker_unregister_and_register(test, script, scope);
+
+  add_completion_callback(() => { registration.unregister(); });
+
+  await wait_for_state(test, registration.installing, 'activated');
+}, 'Initialize global state');
+
+promise_test(test => {
+  return new Promise(resolve => {
+    registration.addEventListener('updatefound', resolve);
+    registration.update();
+  });
+}, 'Update service worker with new script that\'s missing importScripts()');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html
new file mode 100644
index 0000000..b3875d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test that mode is set to same-origin for a main module</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a main module service worker script fetch during an update check.
+// The fetch should have the mode set to 'same-origin'.
+//
+// The test works by registering a main module service worker. It then does an
+// update. The test server responds with an updated worker script that remembers
+// the http request. The updated worker reports back this request to the test
+// page.
+promise_test(async (t) => {
+  const script = "resources/test-request-mode-worker.py";
+  const scope = "resources/";
+
+  // Register the service worker.
+  await service_worker_unregister(t, scope);
+  const registration = await navigator.serviceWorker.register(
+      script, {scope, type: 'module'});
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Do an update.
+  await registration.update();
+
+  // Ask the new worker what the request was.
+  const newWorker = registration.installing;
+  const sawMessage = new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+  });
+  newWorker.postMessage('getHeaders');
+  const result = await sawMessage;
+
+  // Test the result.
+  assert_equals(result['sec-fetch-mode'], 'same-origin');
+  assert_equals(result['origin'], undefined);
+
+}, 'headers of a main module script');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html
new file mode 100644
index 0000000..6ebad4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that cache is being bypassed/validated in no-cache mode on update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a service worker script fetch during an update check which
+// bypasses/validates the browser cache. The fetch should have the
+// 'if-none-match' request header.
+//
+// This tests the Update step:
+//  "Set request’s cache mode to "no-cache" if any of the following are true..."
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+//
+// The test works by registering a service worker with |updateViaCache|
+// set to "none". It then does an update. The test server responds with
+// an updated worker script that remembers the http request headers.
+// The updated worker reports back these headers to the test page.
+promise_test(async (t) => {
+  const script = "resources/test-request-headers-worker.py";
+  const scope = "resources/";
+
+  // Register the service worker.
+  await service_worker_unregister(t, scope);
+  const registration = await navigator.serviceWorker.register(
+      script, {scope, updateViaCache: 'none'});
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Do an update.
+  await registration.update();
+
+  // Ask the new worker what the request headers were.
+  const newWorker = registration.installing;
+  const sawMessage = new Promise((resolve) => {
+    navigator.serviceWorker.onmessage = (event) => {
+      resolve(event.data);
+    };
+  });
+  newWorker.postMessage('getHeaders');
+  const result = await sawMessage;
+
+  // Test the result.
+  assert_equals(result['service-worker'], 'script');
+  assert_equals(result['if-none-match'], 'etag');
+}, 'headers in no-cache mode');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html
new file mode 100644
index 0000000..0a54aa9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function send_message_to_worker_and_wait_for_response(worker, message) {
+  return new Promise(resolve => {
+    // Use a dedicated channel for every request to avoid race conditions on
+    // concurrent requests.
+    const channel = new MessageChannel();
+    worker.postMessage(channel.port1, [channel.port1]);
+
+    let messageReceived = false;
+    channel.port2.onmessage = event => {
+      assert_false(messageReceived, 'Already received response for ' + message);
+      messageReceived = true;
+      resolve(event.data);
+    };
+    channel.port2.postMessage(message);
+  });
+}
+
+async function ensure_install_event_fired(worker) {
+  const response = await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+  assert_equals('installEventFired', response);
+  assert_equals('installing', worker.state, 'Expected worker to be installing.');
+}
+
+async function finish_install(worker) {
+  await ensure_install_event_fired(worker);
+  const response = await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+  assert_equals('installFinished', response);
+}
+
+async function activate_service_worker(t, worker) {
+  await finish_install(worker);
+  // By waiting for both states at the same time, the test fails
+  // quickly if the installation fails, avoiding a timeout.
+  await Promise.race([wait_for_state(t, worker, 'activated'),
+                      wait_for_state(t, worker, 'redundant')]);
+  assert_equals('activated', worker.state, 'Service worker should be activated.');
+}
+
+async function update_within_service_worker(worker) {
+  // This function returns a Promise that resolves when update()
+  // has been called but is not necessarily finished yet.
+  // Call finish() on the returned object to wait for update() settle.
+  const port = await send_message_to_worker_and_wait_for_response(worker, 'callUpdate');
+  let messageReceived = false;
+  return {
+    finish: () => {
+      return new Promise(resolve => {
+        port.onmessage = event => {
+          assert_false(messageReceived, 'Update already finished.');
+          messageReceived = true;
+          resolve(event.data);
+        };
+      });
+    },
+  };
+}
+
+async function update_from_client_and_await_installing_version(test, registration) {
+  const updatefound = wait_for_update(test, registration);
+  registration.update();
+  await updatefound;
+  return registration.installing;
+}
+
+async function spin_up_service_worker(test) {
+  const script = 'resources/update-during-installation-worker.py';
+  const scope = 'resources/blank.html';
+
+  const registration = await service_worker_unregister_and_register(test, script, scope);
+  test.add_cleanup(async () => {
+    if (registration.installing) {
+      // If there is an installing worker, we need to finish installing it.
+      // Otherwise, the tests fails with an timeout because unregister() blocks
+      // until the install-event-handler finishes.
+      const worker = registration.installing;
+      await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+      await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+    }
+    return registration.unregister();
+  });
+
+  return registration;
+}
+
+promise_test(async t => {
+  const registration = await spin_up_service_worker(t);
+  const worker = registration.installing;
+  await ensure_install_event_fired(worker);
+
+  const result = registration.update();
+  await activate_service_worker(t, worker);
+  return result;
+}, 'ServiceWorkerRegistration.update() from client succeeds while installing service worker.');
+
+promise_test(async t => {
+  const registration = await spin_up_service_worker(t);
+  const worker = registration.installing;
+  await ensure_install_event_fired(worker);
+
+  // Add event listener to fail the test if update() succeeds.
+  const updatefound = t.step_func(async () => {
+    registration.removeEventListener('updatefound', updatefound);
+    // Activate new worker so non-compliant browsers don't fail with timeout.
+    await activate_service_worker(t, registration.installing);
+    assert_unreached("update() should have failed");
+  });
+  registration.addEventListener('updatefound', updatefound);
+
+  const update = await update_within_service_worker(worker);
+  // Activate worker to ensure update() finishes and the test doesn't timeout
+  // in non-compliant browsers.
+  await activate_service_worker(t, worker);
+
+  const response = await update.finish();
+  assert_false(response.success, 'update() should have failed.');
+  assert_equals('InvalidStateError', response.exception, 'update() should have thrown InvalidStateError.');
+}, 'ServiceWorkerRegistration.update() from installing service worker throws.');
+
+promise_test(async t => {
+  const registration = await spin_up_service_worker(t);
+  const worker1 = registration.installing;
+  await activate_service_worker(t, worker1);
+
+  const worker2 = await update_from_client_and_await_installing_version(t, registration);
+  await ensure_install_event_fired(worker2);
+
+  const update = await update_within_service_worker(worker1);
+  // Activate the new version so that update() finishes and the test doesn't timeout.
+  await activate_service_worker(t, worker2);
+  const response = await update.finish();
+  assert_true(response.success, 'update() from active service worker should have succeeded.');
+}, 'ServiceWorkerRegistration.update() from active service worker succeeds while installing service worker.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html
new file mode 100644
index 0000000..5273420
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Update on navigation</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/test-helpers.sub.js'></script>
+<script>
+promise_test(async (t) => {
+    var script = 'resources/update-fetch-worker.py';
+    var scope = 'resources/trickle.py?ms=1000&count=1';
+
+    const registration = await service_worker_unregister_and_register(t, script, scope);
+    t.add_cleanup(() => registration.unregister());
+
+    if (registration.installing)
+        await wait_for_state(t, registration.installing, 'activated');
+
+    const frame = await with_iframe(scope);
+    t.add_cleanup(() => frame.remove());
+}, 'The active service worker in charge of a navigation load should not be terminated as part of updating the registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html
new file mode 100644
index 0000000..17608d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>Service Worker: recovery by navigation update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+    var scope = 'resources/simple.txt';
+    var worker_url = 'resources/update-recovery-worker.py';
+    var expected_url = normalizeURL(worker_url);
+    var registration;
+
+    function with_bad_iframe(url) {
+      return new Promise(function(resolve, reject) {
+        var frame = document.createElement('iframe');
+
+        // There is no cross-browser event to listen for to detect an
+        // iframe that fails to load due to a bad interception.  Unfortunately
+        // we have to use a timeout.
+        var timeout = setTimeout(function() {
+          frame.remove();
+          resolve();
+        }, 5000);
+
+        // If we do get a load event, though, we know something went wrong.
+        frame.addEventListener('load', function() {
+          clearTimeout(timeout);
+          frame.remove();
+          reject('expected bad iframe should not fire a load event!');
+        });
+
+        frame.src = url;
+        document.body.appendChild(frame);
+      });
+    }
+
+    function with_update(t) {
+      return new Promise(function(resolve, reject) {
+        registration.addEventListener('updatefound', function onUpdate() {
+          registration.removeEventListener('updatefound', onUpdate);
+          wait_for_state(t, registration.installing, 'activated').then(function() {
+            resolve();
+          });
+        });
+      });
+    }
+
+    return service_worker_unregister_and_register(t, worker_url, scope)
+      .then(function(r) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          registration = r;
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() {
+          return Promise.all([
+            with_update(t),
+            with_bad_iframe(scope)
+          ]);
+        })
+      .then(function() {
+          return with_iframe(scope);
+        })
+      .then(function(frame) {
+          assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+                        expected_url);
+          frame.remove();
+        });
+  }, 'Recover from a bad service worker by updating after a failed navigation.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html
new file mode 100644
index 0000000..269e61b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html
@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update the registration with a different script type.</title>
+<!-- common.js is for guid() -->
+<script src="/common/security-features/resources/common.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// The following two tests check that a registration is updated correctly
+// with different script type. At first Service Worker is registered as
+// classic script type, then it is re-registered as module script type,
+// and vice versa. A main script is also updated at the same time.
+promise_test(async t => {
+  const key = guid();
+  const script = `resources/update-registration-with-type.py?classic_first=1&key=${key}`;
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with classic script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  });
+  const firstWorker = firstRegistration.installing;
+  await wait_for_state(t, firstWorker, 'activated');
+  firstWorker.postMessage(' ');
+  let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+  assert_equals(msgEvent.data, 'A classic script.');
+
+  // Re-register with module script type.
+  const secondRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  const secondWorker = secondRegistration.installing;
+  secondWorker.postMessage(' ');
+  msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+  assert_equals(msgEvent.data, 'A module script.');
+
+  assert_not_equals(firstWorker, secondWorker);
+  assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module).');
+
+promise_test(async t => {
+  const key = guid();
+  const script = `resources/update-registration-with-type.py?classic_first=0&key=${key}`;
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with module script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  const firstWorker = firstRegistration.installing;
+  await wait_for_state(t, firstWorker, 'activated');
+  firstWorker.postMessage(' ');
+  let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+  assert_equals(msgEvent.data, 'A module script.');
+
+  // Re-register with classic script type.
+  const secondRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  });
+  const secondWorker = secondRegistration.installing;
+  secondWorker.postMessage(' ');
+  msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+  assert_equals(msgEvent.data, 'A classic script.');
+
+  assert_not_equals(firstWorker, secondWorker);
+  assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic).');
+
+// The following two tests change the script type while keeping
+// the script identical.
+promise_test(async t => {
+  const script = 'resources/empty-worker.js';
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with classic script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  });
+  const firstWorker = firstRegistration.installing;
+  await wait_for_state(t, firstWorker, 'activated');
+
+  // Re-register with module script type.
+  const secondRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  const secondWorker = secondRegistration.installing;
+
+  assert_not_equals(firstWorker, secondWorker);
+  assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module) '
+    + 'and with a same main script.');
+
+promise_test(async t => {
+  const script = 'resources/empty-worker.js';
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with module script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  const firstWorker = firstRegistration.installing;
+  await wait_for_state(t, firstWorker, 'activated');
+
+  // Re-register with classic script type.
+  const secondRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  });
+  const secondWorker = secondRegistration.installing;
+
+  assert_not_equals(firstWorker, secondWorker);
+  assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic) '
+    + 'and with a same main script.');
+
+// This test checks that a registration is not updated with the same script
+// type and the same main script.
+promise_test(async t => {
+  const script = 'resources/empty-worker.js';
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with module script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  await wait_for_state(t, firstRegistration.installing, 'activated');
+
+  // Re-register with module script type.
+  const secondRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  assert_equals(secondRegistration.installing, null);
+
+  assert_equals(firstRegistration, secondRegistration);
+}, 'Does not update the registration with the same script type and '
+    + 'the same main script.');
+
+// In the case (classic => module), a worker script contains importScripts()
+// that is disallowed on module scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+  const script = 'resources/classic-worker.js';
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with classic script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  });
+  assert_not_equals(firstRegistration.installing, null);
+  await wait_for_state(t, firstRegistration.installing, 'activated');
+
+  // Re-register with module script type and expect TypeError.
+  return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (classic => module) '
+    + 'and with a same main script. Expect evaluation failed.');
+
+// In the case (module => classic), a worker script contains static-import
+// that is disallowed on classic scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+  const script = 'resources/module-worker.js';
+  const scope = 'resources/update-registration-with-type';
+  await service_worker_unregister(t, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+
+  // Register with module script type.
+  const firstRegistration = await navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'module'
+  });
+  assert_not_equals(firstRegistration.installing, null);
+  await wait_for_state(t, firstRegistration.installing, 'activated');
+
+  // Re-register with classic script type and expect TypeError.
+  return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+    scope: scope,
+    type: 'classic'
+  }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (module => classic) '
+    + 'and with a same main script. Expect evaluation failed.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html
new file mode 100644
index 0000000..d8ed94f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(async function(t) {
+  const script = './resources/empty.js';
+  const scope = './resources/empty.html?update-result';
+
+  let reg = await navigator.serviceWorker.register(script, { scope });
+  t.add_cleanup(async _ => await reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  let result = await reg.update();
+  assert_true(result instanceof ServiceWorkerRegistration,
+              'update() should resolve a ServiceWorkerRegistration');
+}, 'ServiceWorkerRegistration.update() should resolve a registration object');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update.https.html b/third_party/web_platform_tests/service-workers/service-worker/update.https.html
new file mode 100644
index 0000000..f9fded3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update.https.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration update()</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker.py). The response to update() will be
+// different based on the mode.
+async function prepare_ready_registration_with_mode(t, mode) {
+  const key = token();
+  const worker_url = `resources/update-worker.py?Key=${key}&Mode=${mode}`;
+  const expected_url = normalizeURL(worker_url);
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, SCOPE);
+  await wait_for_state(t, registration.installing, 'activated');
+  assert_equals(registration.installing, null,
+                'prepare_ready: installing');
+  assert_equals(registration.waiting, null,
+                'prepare_ready: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'prepare_ready: active');
+  return [registration, expected_url];
+}
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_registration_with_file(
+    t, initial_worker, updated_worker) {
+  const key = token();
+  const worker_url = `resources/update-worker-from-file.py?` +
+      `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+  const expected_url = normalizeURL(worker_url);
+
+  const registration = await service_worker_unregister_and_register(
+      t, worker_url, SCOPE);
+  await wait_for_state(t, registration.installing, 'activated');
+  assert_equals(registration.installing, null,
+                'prepare_ready: installing');
+  assert_equals(registration.waiting, null,
+                'prepare_ready: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'prepare_ready: active');
+  return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+  assert_equals(registration.installing.scriptURL, expected_url,
+                'assert_installing_and_active: installing');
+  assert_equals(registration.waiting, null,
+                'assert_installing_and_active: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+  assert_equals(registration.installing, null,
+                'assert_waiting_and_active: installing');
+  assert_equals(registration.waiting.scriptURL, expected_url,
+                'assert_waiting_and_active: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+  assert_equals(registration.installing, null,
+                'assert_active_only: installing');
+  assert_equals(registration.waiting, null,
+                'assert_active_only: waiting');
+  assert_equals(registration.active.scriptURL, expected_url,
+                'assert_active_only: active');
+}
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'normal');
+  t.add_cleanup(() => registration.unregister());
+
+  await Promise.all([registration.update(), wait_for_update(t, registration)]);
+  assert_installing_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.installing, 'installed');
+  assert_waiting_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.waiting, 'activated');
+  assert_active_only(registration, expected_url);
+}, 'update() should succeed when new script is available.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'bad_mime_type');
+  t.add_cleanup(() => registration.unregister());
+
+  await promise_rejects_dom(t, 'SecurityError', registration.update());
+  assert_active_only(registration, expected_url);
+}, 'update() should fail when mime type is invalid.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'redirect');
+  t.add_cleanup(() => registration.unregister());
+
+  await promise_rejects_js(t, TypeError, registration.update());
+  assert_active_only(registration, expected_url);
+}, 'update() should fail when a response for the main script is redirect.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'syntax_error');
+  t.add_cleanup(() => registration.unregister());
+
+  await promise_rejects_js(t, TypeError, registration.update());
+  assert_active_only(registration, expected_url);
+}, 'update() should fail when a new script contains a syntax error.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'throw_install');
+  t.add_cleanup(() => registration.unregister());
+
+  await Promise.all([registration.update(), wait_for_update(t, registration)]);
+  assert_installing_and_active(registration, expected_url);
+}, 'update() should resolve when the install event throws.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_mode(t, 'normal');
+  t.add_cleanup(() => registration.unregister());
+
+  // We need to hold a client alive so that unregister() below doesn't remove
+  // the registration before update() has had a chance to look at the pending
+  // uninstall flag.
+  const frame = await with_iframe(SCOPE);
+  t.add_cleanup(() => frame.remove());
+
+  await promise_rejects_js(
+      t, TypeError,
+      Promise.all([registration.unregister(), registration.update()]));
+}, 'update() should fail when the pending uninstall flag is set.');
+
+promise_test(async t => {
+  const [registration, expected_url] =
+      await prepare_ready_registration_with_file(
+        t,
+        'update-smaller-body-before-update-worker.js',
+        'update-smaller-body-after-update-worker.js');
+  t.add_cleanup(() => registration.unregister());
+
+  await Promise.all([registration.update(), wait_for_update(t, registration)]);
+  assert_installing_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.installing, 'installed');
+  assert_waiting_and_active(registration, expected_url);
+
+  await wait_for_state(t, registration.waiting, 'activated');
+  assert_active_only(registration, expected_url);
+}, 'update() should succeed when the script shrinks.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html b/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html
new file mode 100644
index 0000000..499e581
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+promise_test(async t => {
+
+  t.add_cleanup(async() => {
+    if (frame)
+      frame.remove();
+    if (registration)
+      await registration.unregister();
+  });
+
+  await service_worker_unregister(t, SCOPE);
+  const frame = await with_iframe(SCOPE);
+  const registration =
+      await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+  await wait_for_state(t, registration.installing, 'installed');
+  const controller = frame.contentWindow.navigator.serviceWorker.controller;
+  assert_equals(controller, null, 'controller');
+  assert_equals(registration.active, null, 'registration.active');
+  assert_equals(registration.waiting.state, 'installed',
+                'registration.waiting');
+  assert_equals(registration.installing, null, 'registration.installing');
+}, 'waiting is set after installation');
+
+// Tests that the ServiceWorker objects returned from waiting attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+  const registration1 =
+      await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+  const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+  assert_equals(registration1.waiting, registration2.waiting,
+                'ServiceWorkerRegistration.waiting should return the same ' +
+                'object');
+  await registration1.unregister();
+}, 'The ServiceWorker objects returned from waiting attribute getter that ' +
+   'represent the same service worker are the same objects');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html
new file mode 100644
index 0000000..cda9d6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSockets can be created in a Service Worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+    const SCRIPT = 'resources/websocket-worker.js?pipe=sub';
+    const SCOPE = 'resources/blank.html';
+    let registration;
+    return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+      .then(r => {
+          add_completion_callback(() => { r.unregister(); });
+          registration = r;
+          return wait_for_state(t, r.installing, 'activated');
+        })
+      .then(() => {
+          return new Promise(resolve => {
+              navigator.serviceWorker.onmessage = t.step_func(msg => {
+                  assert_equals(msg.data, 'PASS');
+                  resolve();
+              });
+              registration.active.postMessage({});
+          });
+        });
+  }, 'Verify WebSockets can be created in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html b/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html
new file mode 100644
index 0000000..cbfed45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSocket handshake channel is not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+promise_test(function(t) {
+    var path = new URL(".", window.location).pathname
+    var url = 'resources/websocket.js';
+    var scope = 'resources/blank.html?websocket';
+    var host_info = get_host_info();
+    var frameURL = host_info['HTTPS_ORIGIN'] + path + scope;
+    var frame;
+
+    return service_worker_unregister_and_register(t, url, scope)
+      .then(function(registration) {
+          t.add_cleanup(function() {
+              return service_worker_unregister(t, scope);
+            });
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(frameURL); })
+      .then(function(f) {
+          frame = f;
+          return websocket(t, frame);
+        })
+      .then(function() {
+          var channel = new MessageChannel();
+          return new Promise(function(resolve) {
+            channel.port1.onmessage = resolve;
+            frame.contentWindow.navigator.serviceWorker.controller.postMessage({port: channel.port2}, [channel.port2]);
+          });
+        })
+      .then(function(e) {
+          for (var url in e.data.urls) {
+            assert_equals(url.indexOf(get_websocket_url()), -1,
+                          "Observed an unexpected FetchEvent for the WebSocket handshake");
+          }
+          frame.remove();
+        });
+  }, 'Verify WebSocket handshake channel does not get intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html
new file mode 100644
index 0000000..9394ff7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-origin webvtt returned by service worker is detected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+// This file tests responses for WebVTT text track from a service worker. It
+// creates an iframe with a <track> element, controlled by a service worker.
+// Each test tries to load a text track, the service worker intercepts the
+// requests and responds with opaque or non-opaque responses. As the
+// crossorigin attribute is not set, request's mode is always "same-origin",
+// and as specified in https://fetch.spec.whatwg.org/#http-fetch,
+// a response from a service worker whose type is neither "basic" nor
+// "default" is rejected.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+// Add '?ignore' so the service worker falls back for the navigation.
+const kScope = 'resources/vtt-frame.html?ignore';
+let frame;
+
+function load_track(url) {
+  const track = frame.contentDocument.querySelector('track');
+  const result = new Promise((resolve, reject) => {
+      track.onload = (e => {
+          resolve('load event');
+        });
+      track.onerror = (e => {
+          resolve('error event');
+        });
+    });
+
+  track.src = url;
+  // Setting mode to hidden seems needed, or else the text track requests don't
+  // occur.
+  track.track.mode = 'hidden';
+  return result;
+}
+
+promise_test(t => {
+    return service_worker_unregister_and_register(t, kScript, kScope)
+      .then(registration => {
+          promise_test(() => {
+              frame.remove();
+              return registration.unregister();
+            }, 'restore global state');
+
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(() => {
+          return with_iframe(kScope);
+        })
+      .then(f => {
+          frame = f;
+        })
+  }, 'initialize global state');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a same-origin URL.
+    url += '?url=' + host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'load event');
+        });
+  }, 'same-origin text track should load');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a cross-origin URL.
+    url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'cross-origin text track with no-cors request should not load');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a cross-origin URL that
+    // doesn't support CORS.
+    url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN +
+        '/media/foo-no-cors.vtt';
+    // Add '&mode' to tell the service worker to do a CORS request.
+    url += '&mode=cors';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'cross-origin text track with rejected cors request should not load');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a cross-origin URL.
+    url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+    // Add '&mode' to tell the service worker to do a CORS request.
+    url += '&mode=cors';
+    // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+    // that CORS will succeed if the service approves it.
+    url += '&credentials=same-origin';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'cross-origin text track with approved cors request should not load');
+
+// Redirect tests.
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+    redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+    // ... to a same-origin URL.
+    redirect_target = host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+    url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'load event');
+        });
+  }, 'same-origin text track that redirects same-origin should load');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+    redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+    // ... to a cross-origin URL.
+    redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+    url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'same-origin text track that redirects cross-origin should not load');
+
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+    redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+    // ... to a cross-origin URL.
+    redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo-no-cors.vtt';
+    url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+    // Add '&mode' to tell the service worker to do a CORS request.
+    url += '&mode=cors';
+    // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+    // that CORS will succeed if the server approves it.
+    url += '&credentials=same-origin';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'same-origin text track that redirects to a cross-origin text track with rejected cors should not load');
+
+promise_test(t => {
+    let url = '/media/foo.vtt';
+    // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+    redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+    // ... to a cross-origin URL.
+    redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+    url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+    // Add '&mode' to tell the service worker to do a CORS request.
+    url += '&mode=cors';
+    // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+    // that CORS will succeed if the server approves it.
+    url += '&credentials=same-origin';
+    return load_track(url)
+      .then(result => {
+          assert_equals(result, 'error event');
+        });
+  }, 'same-origin text track that redirects to a cross-origin text track with approved cors should not load');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html b/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html
new file mode 100644
index 0000000..ad60f78
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<title>Service Worker: WindowClient.navigate() tests</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const SCOPE = 'resources/blank.html';
+const SCRIPT_URL = 'resources/windowclient-navigate-worker.js';
+const CROSS_ORIGIN_URL =
+  get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/blank.html';
+
+navigateTest({
+  description: 'normal',
+  destUrl: 'blank.html?navigate',
+  expected: normalizeURL(SCOPE) + '?navigate',
+});
+
+navigateTest({
+  description: 'blank url',
+  destUrl: '',
+  expected: normalizeURL(SCRIPT_URL)
+});
+
+navigateTest({
+  description: 'in scope but not controlled test on installing worker',
+  destUrl: 'blank.html?navigate',
+  expected: 'TypeError',
+  waitState: 'installing',
+});
+
+navigateTest({
+  description: 'in scope but not controlled test on active worker',
+  destUrl: 'blank.html?navigate',
+  expected: 'TypeError',
+  controlled: false,
+});
+
+navigateTest({
+  description: 'out of scope',
+  srcUrl: '/common/blank.html',
+  destUrl: 'blank.html?navigate',
+  expected: 'TypeError',
+});
+
+navigateTest({
+  description: 'cross orgin url',
+  destUrl: CROSS_ORIGIN_URL,
+  expected: null
+});
+
+navigateTest({
+  description: 'invalid url (http://[example.com])',
+  destUrl: 'http://[example].com',
+  expected: 'TypeError'
+});
+
+navigateTest({
+  description: 'invalid url (view-source://example.com)',
+  destUrl: 'view-source://example.com',
+  expected: 'TypeError'
+});
+
+navigateTest({
+  description: 'invalid url (file:///)',
+  destUrl: 'file:///',
+  expected: 'TypeError'
+});
+
+navigateTest({
+  description: 'invalid url (about:blank)',
+  destUrl: 'about:blank',
+  expected: 'TypeError'
+});
+
+navigateTest({
+  description: 'navigate on a top-level window client',
+  destUrl: 'blank.html?navigate',
+  srcUrl: 'resources/loaded.html',
+  scope: 'resources/loaded.html',
+  expected: normalizeURL(SCOPE) + '?navigate',
+  frameType: 'top-level'
+});
+
+async function createFrame(t, parameters) {
+  if (parameters.frameType === 'top-level') {
+    // Wait for window.open is completed.
+    await new Promise(resolve => {
+      const win = window.open(parameters.srcUrl);
+      t.add_cleanup(() => win.close());
+      window.addEventListener('message', (e) => {
+        if (e.data.type === 'LOADED') {
+          resolve();
+        }
+      });
+    });
+  }
+
+  if (parameters.frameType === 'nested') {
+    const frame = await with_iframe(parameters.srcUrl);
+    t.add_cleanup(() => frame.remove());
+  }
+}
+
+function navigateTest(overrideParameters) {
+  // default parameters
+  const parameters = {
+    description: null,
+    srcUrl: SCOPE,
+    destUrl: null,
+    expected: null,
+    waitState: 'activated',
+    scope: SCOPE,
+    controlled: true,
+    // `frameType` can be 'nested' for an iframe WindowClient or 'top-level' for
+    // a main frame WindowClient.
+    frameType: 'nested'
+  };
+
+  for (const key in overrideParameters)
+    parameters[key] = overrideParameters[key];
+
+  promise_test(async function(t) {
+    let pausedLifecyclePort;
+    let scriptUrl = SCRIPT_URL;
+
+    // For in-scope-but-not-controlled test on installing worker,
+    // if the waitState is "installing", then append the query to scriptUrl.
+    if (parameters.waitState === 'installing') {
+      scriptUrl += '?' + parameters.waitState;
+
+      navigator.serviceWorker.addEventListener('message', (event) => {
+        if (event.data.port) {
+          pausedLifecyclePort = event.data.port;
+        }
+      });
+    }
+
+    t.add_cleanup(() => {
+      // Some tests require that the worker remain in a given lifecycle phase.
+      // "Clean up" logic for these tests requires signaling the worker to
+      // release the hold; this allows the worker to be properly discarded
+      // prior to the execution of additional tests.
+      if (pausedLifecyclePort) {
+        // The value of the posted message is inconsequential. A string is
+        // specified here solely to aid in test debugging.
+        pausedLifecyclePort.postMessage('continue lifecycle');
+      }
+    });
+
+    // Create a frame that is not controlled by a service worker.
+    if (!parameters.controlled) {
+      await createFrame(t, parameters);
+    }
+
+    const registration = await service_worker_unregister_and_register(
+        t, scriptUrl, parameters.scope);
+    const serviceWorker = registration.installing;
+    await wait_for_state(t, serviceWorker, parameters.waitState);
+    t.add_cleanup(() => registration.unregister());
+
+    // Create a frame after a service worker is registered so that the frmae is
+    // controlled by an active service worker.
+    if (parameters.controlled) {
+      await createFrame(t, parameters);
+    }
+
+    const response = await new Promise(resolve => {
+      const channel = new MessageChannel();
+      channel.port1.onmessage = t.step_func(resolve);
+      serviceWorker.postMessage({
+        port: channel.port2,
+        url: parameters.destUrl,
+        clientUrl: new URL(parameters.srcUrl, location).toString(),
+        frameType: parameters.frameType,
+        expected: parameters.expected,
+        description: parameters.description,
+      }, [channel.port2]);
+    });
+
+    assert_equals(response.data, null);
+    await fetch_tests_from_worker(serviceWorker);
+  }, parameters.description);
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html
new file mode 100644
index 0000000..4e4d316
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Workers should have their own unique client Id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Get the iframe client ID by calling postMessage() on its controlling
+// worker.  This will cause the service worker to post back the
+// MessageEvent.source.id value.
+function getFrameClientId(frame) {
+  return new Promise(resolve => {
+    let mc = new MessageChannel();
+    frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+      'echo-client-id', [mc.port2]);
+    mc.port1.onmessage = evt => {
+      resolve(evt.data);
+    };
+  });
+}
+
+// Get the worker client ID by creating a worker that performs an intercepted
+// fetch().  The synthetic fetch() response will contain the FetchEvent.clientId
+// value.  This is then posted back to here.
+function getWorkerClientId(frame) {
+  return new Promise(resolve => {
+    let w = new frame.contentWindow.Worker('worker-echo-client-id.js');
+    w.onmessage = evt => {
+      resolve(evt.data);
+    };
+  });
+}
+
+promise_test(async function(t) {
+  const script = './resources/worker-client-id-worker.js';
+  const scope = './resources/worker-client-id';
+  const frame = scope + '/frame.html';
+
+  let reg = await navigator.serviceWorker.register(script, { scope });
+  t.add_cleanup(async _ => await reg.unregister());
+  await wait_for_state(t, reg.installing, 'activated');
+
+  let f = await with_iframe(frame);
+  t.add_cleanup(_ => f.remove());
+
+  let frameClientId = await getFrameClientId(f);
+  assert_not_equals(frameClientId, null, 'frame client id should exist');
+
+  let workerClientId = await getWorkerClientId(f);
+  assert_not_equals(workerClientId, null, 'worker client id should exist');
+
+  assert_not_equals(frameClientId, workerClientId,
+                    'frame and worker client ids should be different');
+}, 'Verify workers have a unique client id separate from their owning documents window');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
new file mode 100644
index 0000000..c8480bf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent issued from workers in an iframe sandboxed via CSP HTTP response header.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+let lastCallbackId = 0;
+let callbacks = {};
+function doTest(frame, type) {
+  return new Promise(function(resolve) {
+    var id = ++lastCallbackId;
+    callbacks[id] = resolve;
+    frame.contentWindow.postMessage({id: id, type: type}, '*');
+  });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+  return new Promise(resolve => {
+    let channel = new MessageChannel();
+    channel.port1.onmessage = msg => {
+      resolve(msg.data);
+    };
+    worker.postMessage({port: channel.port2}, [channel.port2]);
+  });
+}
+
+window.onmessage = function (e) {
+  message = e.data;
+  let id = message['id'];
+  let callback = callbacks[id];
+  delete callbacks[id];
+  callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// A service worker controlling |SCOPE|.
+let worker;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+  return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+    .then(function(registration) {
+      add_completion_callback(() => registration.unregister());
+      worker = registration.installing;
+      return wait_for_state(t, registration.installing, 'activated');
+    });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+  const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+                          'sandboxed-frame-by-header';
+  return with_iframe(iframe_full_url)
+    .then(f => {
+      sandboxed_frame_by_header = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'Service worker should provide the response');
+      assert_equals(requests[0], iframe_full_url);
+      assert_false(data.clients.includes(iframe_full_url),
+                   'Service worker should NOT control the sandboxed page');
+    });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+  const iframe_full_url =
+    expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+    'sandboxed-iframe-same-origin-by-header';
+  return with_iframe(iframe_full_url)
+    .then(f => {
+      sandboxed_same_origin_frame_by_header = f;
+      add_completion_callback(() => f.remove());
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1);
+      assert_equals(requests[0], iframe_full_url);
+      assert_true(data.clients.includes(iframe_full_url));
+    })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+   'allow-same-origin.');
+
+promise_test(t => {
+  let frame = sandboxed_frame_by_header;
+  return doTest(frame, 'fetch-from-worker')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      assert_equals(data.requests.length, 0,
+                    'The request should NOT be handled by SW.');
+    });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+   'allow-scripts flag');
+
+promise_test(t => {
+  let frame = sandboxed_same_origin_frame_by_header;
+  return doTest(frame, 'fetch-from-worker')
+    .then(result => {
+      assert_equals(result, 'done');
+      return getResultsFromWorker(worker);
+    })
+    .then(data => {
+      let requests = data.requests;
+      assert_equals(requests.length, 1,
+                    'The request should be handled by SW.');
+      assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+    });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+   'with allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html
new file mode 100644
index 0000000..8d566b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: controlling Worker/SharedWorker</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This tests service worker interception for worker clients, when the request
+// for the worker script goes through redirects. For example, a request can go
+// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
+// scope of a different service worker, if any.
+// The two key questions are:
+// 1. Upon a redirect from A -> B, should a service worker for scope B
+//    intercept the request?
+// 2. After the final response, which service worker controls the resulting
+//    client?
+//
+// The standard prescribes the following:
+// 1. The service worker for scope B intercepts the redirect. *However*, once a
+//    request falls back to network (i.e., a service worker did not call
+//    respondWith()) and a redirect is then received from network, no service
+//    worker should intercept that redirect or any subsequent redirects.
+// 2. The final service worker that got a fetch event (or would have, in the
+//    case of a non-fetch-event worker) becomes the controller of the client.
+//
+// The standard may change later, see:
+// https://github.com/w3c/ServiceWorker/issues/1289
+//
+// The basic test setup is:
+// 1. Page registers service workers for scope1 and scope2.
+// 2. Page requests a worker from scope1.
+// 3. The request is redirected to scope2 or out-of-scope.
+// 4. The worker posts message to the page describing where the final response
+//   was served from (service worker or network).
+// 5. The worker does an importScripts() and fetch(), and posts back the
+//   responses, which describe where the responses where served from.
+
+// Globals for easier cleanup.
+const scope1 = 'resources/scope1';
+const scope2 = 'resources/scope2';
+let frame;
+
+function get_message_from_worker(port) {
+  return new Promise(resolve => {
+      port.onmessage = evt => {
+        resolve(evt.data);
+      }
+    });
+}
+
+async function cleanup() {
+  if (frame)
+    frame.remove();
+
+  const reg1 = await navigator.serviceWorker.getRegistration(scope1);
+  if (reg1)
+    await reg1.unregister();
+  const reg2 = await navigator.serviceWorker.getRegistration(scope2);
+  if (reg2)
+    await reg2.unregister();
+}
+
+// Builds the worker script URL, which encodes information about where
+// to redirect to. The URL falls in sw1's scope.
+//
+// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
+// respondWith() a redirect. Otherwise, it falls back to network and the server
+// responds with a redirect.
+// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
+// redirect ends up in sw2's scope2. Otherwise it's out of scope.
+function build_worker_url(redirector, redirect_location) {
+  let redirect_path;
+  // Set path to redirect.py, a file on the server that serves
+  // a redirect. When sw1 sees this URL, it falls back to network.
+  if (redirector == 'network')
+    redirector_path = 'redirect.py';
+  // Set path to 'sw-redirect', to tell the service worker
+  // to respond with redirect.
+  else if (redirector == 'serviceworker')
+    redirector_path = 'sw-redirect';
+
+  let redirect_to = base_path() + 'resources/';
+  // Append "scope2/" to redirect_to, so the redirect falls in scope2.
+  // Otherwise no change is needed, as the parent "resources/" directory is
+  // used, and is out-of-scope.
+  if (redirect_location == 'scope2')
+    redirect_to += 'scope2/';
+  // Append the name of the file which serves the worker script.
+  redirect_to += 'worker_interception_redirect_webworker.py';
+
+  return `scope1/${redirector_path}?Redirect=${redirect_to}`
+}
+
+promise_test(async t => {
+  await cleanup();
+  const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
+  const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
+  await wait_for_state(t, registration1.installing, 'activated');
+  const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
+  await wait_for_state(t, registration2.installing, 'activated');
+
+  promise_test(t => {
+    return cleanup();
+  }, 'cleanup global state');
+}, 'initialize global state');
+
+async function worker_redirect_test(worker_request_url,
+                              worker_expected_url,
+                              expected_main_resource_message,
+                              expected_import_scripts_message,
+                              expected_fetch_message,
+                              description) {
+  for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
+    for (const type of ['classic', 'module']) {
+      promise_test(async t => {
+        // Create a frame to load the worker from. This way we can remove the
+        // frame to destroy the worker client when the test is done.
+        frame = await with_iframe('resources/blank.html');
+        t.add_cleanup(() => { frame.remove(); });
+
+        // Start the worker.
+        let w;
+        let port;
+        if (workerType === 'DedicatedWorker') {
+          w = new frame.contentWindow.Worker(worker_request_url, {type});
+          port = w;
+        } else {
+          w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
+          port = w.port;
+          w.port.start();
+        }
+        w.onerror = t.unreached_func('Worker error');
+
+        // Expect a message from the worker indicating which service worker
+        // provided the response for the worker script request, if any.
+        const data = await get_message_from_worker(port);
+
+        // The worker does an importScripts(). Expect a message from the worker
+        // indicating which service worker provided the response for the
+        // importScripts(), if any.
+        const import_scripts_message = await get_message_from_worker(port);
+        test(() => {
+          if (type === 'classic') {
+            assert_equals(import_scripts_message,
+                          expected_import_scripts_message);
+          } else {
+            assert_equals(import_scripts_message, 'importScripts failed');
+          }
+        }, `${description} (${type} ${workerType}, importScripts())`);
+
+        // The worker does a fetch(). Expect a message from the worker
+        // indicating which service worker provided the response for the
+        // fetch(), if any.
+        const fetch_message = await get_message_from_worker(port);
+        test(() => {
+          assert_equals(fetch_message, expected_fetch_message);
+        }, `${description} (${type} ${workerType}, fetch())`);
+
+        // Expect a message from the worker indicating |self.location|.
+        const worker_actual_url = await get_message_from_worker(port);
+        test(() => {
+          assert_equals(
+            worker_actual_url,
+            (new URL(worker_expected_url, location.href)).toString(),
+            'location.href');
+        }, `${description} (${type} ${workerType}, location.href)`);
+
+        assert_equals(data, expected_main_resource_message);
+
+      }, `${description} (${type} ${workerType})`);
+    }
+  }
+}
+
+// request to sw1 scope gets network redirect to sw2 scope
+worker_redirect_test(
+    build_worker_url('network', 'scope2'),
+    'resources/scope2/worker_interception_redirect_webworker.py',
+    'the worker script was served from network',
+    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
+    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
+    'Case #1: network scope1->scope2');
+
+// request to sw1 scope gets network redirect to out-of-scope
+worker_redirect_test(
+    build_worker_url('network', 'out-scope'),
+    'resources/worker_interception_redirect_webworker.py',
+    'the worker script was served from network',
+    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+    'Case #2: network scope1->out-scope');
+
+// request to sw1 scope gets service-worker redirect to sw2 scope
+worker_redirect_test(
+    build_worker_url('serviceworker', 'scope2'),
+    'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
+    'sw2 saw the request for the worker script',
+    'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
+    'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
+    'Case #3: sw scope1->scope2');
+
+// request to sw1 scope gets service-worker redirect to out-of-scope
+worker_redirect_test(
+    build_worker_url('serviceworker', 'out-scope'),
+    'resources/worker_interception_redirect_webworker.py',
+    'the worker script was served from network',
+    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+    'Case #4: sw scope1->out-scope');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html
new file mode 100644
index 0000000..27983d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<title>Service Worker: intercepting Worker script loads</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ========== Worker main resource interception tests ==========
+
+async function setup_service_worker(t, service_worker_url, scope) {
+  const r = await service_worker_unregister_and_register(
+      t, service_worker_url, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+  await wait_for_state(t, r.installing, 'activated');
+  return r.active;
+}
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  const serviceWorker = await setup_service_worker(t, service_worker_url, scope);
+
+  const channels = new MessageChannel();
+  serviceWorker.postMessage({port: channels.port1}, [channels.port1]);
+
+  const clientId = await new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data.id));
+
+  const resultPromise =  new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data));
+
+  const w = new Worker(worker_url);
+  const data = await new Promise((resolve, reject) => {
+    w.onmessage = e => resolve(e.data);
+    w.onerror = e => reject(e.message);
+  });
+  assert_equals(data, 'worker loading intercepted by service worker');
+
+  const results = await resultPromise;
+  assert_equals(results.clientId, clientId);
+  assert_true(!!results.resultingClientId.length);
+
+  channels.port2.postMessage("done");
+}, `Verify a dedicated worker script request gets correct client Ids`);
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new Worker(worker_url);
+  const data = await new Promise((resolve, reject) => {
+    w.onmessage = e => resolve(e.data);
+    w.onerror = e => reject(e.message);
+  });
+  assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a dedicated worker script request issued from a uncontrolled ` +
+   `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+  const frame_url = 'resources/create-out-of-scope-worker.html';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = frame_url;
+
+  const registration = await service_worker_unregister_and_register(
+      t, service_worker_url, scope);
+  t.add_cleanup(() => service_worker_unregister(t, scope));
+  await wait_for_state(t, registration.installing, 'activated');
+
+  const frame = await with_iframe(frame_url);
+  t.add_cleanup(_ => frame.remove());
+
+  assert_equals(
+    frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+    get_newest_worker(registration).scriptURL,
+    'the frame should be controlled by a service worker'
+  );
+
+  const result = await frame.contentWindow.getWorkerPromise();
+
+  assert_equals(result,
+                'worker loading was not intercepted by service worker');
+}, `Verify an out-of-scope dedicated worker script request issued from a ` +
+   `controlled document should not be intercepted by document's service ` +
+   `worker.`);
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-synthesized-worker.js?shared';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new SharedWorker(worker_url);
+  const data = await new Promise((resolve, reject) => {
+    w.port.onmessage = e => resolve(e.data);
+    w.onerror = e => reject(e.message);
+  });
+  assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a shared worker script request issued from a uncontrolled ` +
+   `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-same-origin-worker.js?dedicated';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new Worker(worker_url);
+  const data = await new Promise((resolve, reject) => {
+    w.onmessage = e => resolve(e.data);
+    w.onerror = e => reject(e.message);
+  });
+  assert_equals(data, 'dedicated worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+   'in starting a dedicated worker.');
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-same-origin-worker.js?shared';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new SharedWorker(worker_url);
+  const data = await new Promise((resolve, reject) => {
+    w.port.onmessage = e => resolve(e.data);
+    w.onerror = e => reject(e.message);
+  });
+  assert_equals(data, 'shared worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+   'in starting a shared worker.');
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-cors-worker.js?dedicated';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new Worker(worker_url);
+  const watcher = new EventWatcher(t, w, ['message', 'error']);
+  await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails dedicated ' +
+   'worker start.');
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-cors-worker.js?shared';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new SharedWorker(worker_url);
+  const watcher = new EventWatcher(t, w, ['message', 'error']);
+  await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails shared ' +
+   'worker start.');
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-no-cors-worker.js?dedicated';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new Worker(worker_url);
+  const watcher = new EventWatcher(t, w, ['message', 'error']);
+  await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+   'fails dedicated worker start.');
+
+promise_test(async t => {
+  const worker_url = 'resources/sample-no-cors-worker.js?shared';
+  const service_worker_url = 'resources/sample-worker-interceptor.js';
+  const scope = worker_url;
+
+  await setup_service_worker(t, service_worker_url, scope);
+  const w = new SharedWorker(worker_url);
+  const watcher = new EventWatcher(t, w, ['message', 'error']);
+  await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+   'fails shared worker start.');
+
+// ========== Worker subresource interception tests ==========
+
+const scope_for_subresource_interception = 'resources/load_worker.js';
+
+promise_test(async t => {
+  const service_worker_url = 'resources/worker-load-interceptor.js';
+  const r = await service_worker_unregister_and_register(
+      t, service_worker_url, scope_for_subresource_interception);
+  await wait_for_state(t, r.installing, 'activated');
+}, 'Register a service worker for worker subresource interception tests.');
+
+// Do not call this function multiple times without waiting for the promise
+// resolution because this sets new event handlers on |worker|.
+// TODO(nhiroki): To isolate multiple function calls, use MessagePort instead of
+// worker's onmessage event handler.
+async function request_on_worker(worker, resource_type) {
+  const data = await new Promise((resolve, reject) => {
+    if (worker instanceof Worker) {
+      worker.onmessage = e => resolve(e.data);
+      worker.onerror = e => reject(e);
+      worker.postMessage(resource_type);
+    } else if (worker instanceof SharedWorker) {
+      worker.port.onmessage = e => resolve(e.data);
+      worker.onerror = e => reject(e);
+      worker.port.postMessage(resource_type);
+    } else {
+      reject('Unexpected worker type!');
+    }
+  });
+  assert_equals(data, 'This load was successfully intercepted.');
+}
+
+async function subresource_test(worker) {
+  await request_on_worker(worker, 'xhr');
+  await request_on_worker(worker, 'fetch');
+  await request_on_worker(worker, 'importScripts');
+}
+
+promise_test(async t => {
+  await subresource_test(new Worker('resources/load_worker.js'));
+}, 'Requests on a dedicated worker controlled by a service worker.');
+
+promise_test(async t => {
+  await subresource_test(new SharedWorker('resources/load_worker.js'));
+}, 'Requests on a shared worker controlled by a service worker.');
+
+promise_test(async t => {
+  await subresource_test(new Worker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a dedicated worker and ' +
+       'controlled by a service worker');
+
+promise_test(async t => {
+  await subresource_test(new SharedWorker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a shared worker and controlled ' +
+       'by a service worker');
+
+promise_test(async t => {
+  await service_worker_unregister(t, scope_for_subresource_interception);
+}, 'Unregister a service worker for subresource interception tests.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js
new file mode 100644
index 0000000..1ae320e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js
@@ -0,0 +1,55 @@
+// META: script=resources/test-helpers.sub.js
+
+let frame;
+
+promise_test(async (t) => {
+  const scope = "resources/empty.html";
+  const script = "resources/xhr-content-length-worker.js";
+  const registration = await service_worker_unregister_and_register(t, script, scope);
+  await wait_for_state(t, registration.installing, "activated");
+  frame = await with_iframe(scope);
+}, "Setup");
+
+promise_test(async t => {
+  const xhr = new frame.contentWindow.XMLHttpRequest();
+  xhr.open("GET", "test?type=no-content-length");
+  xhr.send();
+  const event = await new Promise(resolve => xhr.onload = resolve);
+  assert_equals(xhr.getResponseHeader("content-length"), null);
+  assert_false(event.lengthComputable);
+  assert_equals(event.total, 0);
+  assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response without Content-Length header`);
+
+promise_test(async t => {
+  const xhr = new frame.contentWindow.XMLHttpRequest();
+  xhr.open("GET", "test?type=larger-content-length");
+  xhr.send();
+  const event = await new Promise(resolve => xhr.onload = resolve);
+  assert_equals(xhr.getResponseHeader("content-length"), "10000");
+  assert_true(event.lengthComputable);
+  assert_equals(event.total, 10000);
+  assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with Content-Length header with value larger than response body length`);
+
+promise_test(async t => {
+  const xhr = new frame.contentWindow.XMLHttpRequest();
+  xhr.open("GET", "test?type=double-content-length");
+  xhr.send();
+  const event = await new Promise(resolve => xhr.onload = resolve);
+  assert_equals(xhr.getResponseHeader("content-length"), "10000, 10000");
+  assert_true(event.lengthComputable);
+  assert_equals(event.total, 10000);
+  assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with two Content-Length headers value larger than response body length`);
+
+promise_test(async t => {
+  const xhr = new frame.contentWindow.XMLHttpRequest();
+  xhr.open("GET", "test?type=bogus-content-length");
+  xhr.send();
+  const event = await new Promise(resolve => xhr.onload = resolve);
+  assert_equals(xhr.getResponseHeader("content-length"), "test");
+  assert_false(event.lengthComputable);
+  assert_equals(event.total, 0);
+  assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with bogus Content-Length header`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html
new file mode 100644
index 0000000..673ca52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XHR responseURL uses the response url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/xhr-iframe.html';
+const script = 'resources/xhr-response-url-worker.js';
+let iframe;
+
+function build_url(options) {
+  const url = new URL('test', window.location);
+  const opts = options ? options : {};
+  if (opts.respondWith)
+    url.searchParams.set('respondWith', opts.respondWith);
+  if (opts.url)
+    url.searchParams.set('url', opts.url.href);
+  return url.href;
+}
+
+promise_test(async (t) => {
+  const registration =
+      await service_worker_unregister_and_register(t, script, scope);
+  await wait_for_state(t, registration.installing, 'activated');
+  iframe = await with_iframe(scope);
+}, 'global setup');
+
+// Test that XMLHttpRequest.responseURL uses the response URL from the service
+// worker.
+promise_test(async (t) => {
+  // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+  const target = new URL('resources/sample.txt', window.location);
+  const url = build_url({
+    respondWith: 'fetch',
+    url: target
+  });
+
+  // Perform the XHR.
+  const xhr = await iframe.contentWindow.xhr(url);
+  assert_equals(xhr.responseURL, target.href, 'responseURL');
+}, 'XHR responseURL should be the response URL');
+
+// Same as above with a generated response.
+promise_test(async (t) => {
+  // Build a URL that tells the service worker to respondWith(new Response()).
+  const url = build_url({respondWith: 'string'});
+
+  // Perform the XHR.
+  const xhr = await iframe.contentWindow.xhr(url);
+  assert_equals(xhr.responseURL, url, 'responseURL');
+}, 'XHR responseURL should be the response URL (generated response)');
+
+// Test that XMLHttpRequest.responseXML is a Document whose URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+  // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+  const target = new URL('resources/blank.html', window.location);
+  const url = build_url({
+    respondWith: 'fetch',
+    url: target
+  });
+
+  // Perform the XHR.
+  const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+  assert_equals(xhr.responseURL, target.href, 'responseURL');
+
+  // The document's URL uses the response URL:
+  // "Set |document|’s URL to |response|’s url."
+  // https://xhr.spec.whatwg.org/#document-response
+  assert_equals(xhr.responseXML.URL, target.href, 'responseXML.URL');
+
+  // The document's base URL falls back to the document URL:
+  // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+  assert_equals(xhr.responseXML.baseURI, target.href, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL');
+
+// Same as above with a generated response from the service worker.
+promise_test(async (t) => {
+  // Build a URL that tells the service worker to
+  // respondWith(new Response()) with a document response.
+  const url = build_url({respondWith: 'document'});
+
+  // Perform the XHR.
+  const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+  assert_equals(xhr.responseURL, url, 'responseURL');
+
+  // The document's URL uses the response URL, which is the request URL:
+  // "Set |document|’s URL to |response|’s url."
+  // https://xhr.spec.whatwg.org/#document-response
+  assert_equals(xhr.responseXML.URL, url, 'responseXML.URL');
+
+  // The document's base URL falls back to the document URL:
+  // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+  assert_equals(xhr.responseXML.baseURI, url, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL (generated response)');
+
+promise_test(async (t) => {
+  if (iframe)
+    iframe.remove();
+  await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html
new file mode 100644
index 0000000..1d3c364
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XSL's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+// This test loads an XML document which is controlled a service worker. The
+// document loads a stylesheet and a service worker responds with another URL.
+// The stylesheet imports a relative URL to test that the base URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+  const SCOPE = 'resources/xsl-base-url-iframe.xml';
+  const SCRIPT = 'resources/xsl-base-url-worker.js';
+  let worker;
+  let frame;
+
+  t.add_cleanup(() => {
+    if (frame)
+      frame.remove();
+    service_worker_unregister(t, SCOPE);
+  });
+
+  const registration = await service_worker_unregister_and_register(
+      t, SCRIPT, SCOPE);
+  worker = registration.installing;
+  await wait_for_state(t, worker, 'activated');
+
+  frame = await with_iframe(SCOPE);
+  assert_equals(frame.contentDocument.body.textContent, 'PASS');
+}, 'base URL when service worker does respondWith(fetch(responseUrl))');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js b/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js
deleted file mode 100644
index 147ea61..0000000
--- a/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js
+++ /dev/null
@@ -1,222 +0,0 @@
-// Adapter for testharness.js-style tests with Service Workers
-
-function service_worker_unregister_and_register(test, url, scope) {
-  if (!scope || scope.length == 0)
-    return Promise.reject(new Error('tests must define a scope'));
-
-  var options = { scope: scope };
-  return service_worker_unregister(test, scope)
-    .then(function() {
-        return navigator.serviceWorker.register(url, options);
-      })
-    .catch(unreached_rejection(test,
-                               'unregister and register should not fail'));
-}
-
-function service_worker_unregister(test, documentUrl) {
-  return navigator.serviceWorker.getRegistration(documentUrl)
-    .then(function(registration) {
-        if (registration)
-          return registration.unregister();
-      })
-    .catch(unreached_rejection(test, 'unregister should not fail'));
-}
-
-function service_worker_unregister_and_done(test, scope) {
-  return service_worker_unregister(test, scope)
-    .then(test.done.bind(test));
-}
-
-function unreached_fulfillment(test, prefix) {
-  return test.step_func(function(result) {
-      var error_prefix = prefix || 'unexpected fulfillment';
-      assert_unreached(error_prefix + ': ' + result);
-    });
-}
-
-// Rejection-specific helper that provides more details
-function unreached_rejection(test, prefix) {
-  return test.step_func(function(error) {
-      var reason = error.message || error.name || error;
-      var error_prefix = prefix || 'unexpected rejection';
-      assert_unreached(error_prefix + ': ' + reason);
-    });
-}
-
-// Adds an iframe to the document and returns a promise that resolves to the
-// iframe when it finishes loading. The caller is responsible for removing the
-// iframe later if needed.
-function with_iframe(url) {
-  return new Promise(function(resolve) {
-      var frame = document.createElement('iframe');
-      frame.src = url;
-      frame.onload = function() { resolve(frame); };
-      document.body.appendChild(frame);
-    });
-}
-
-function normalizeURL(url) {
-  return new URL(url, self.location).toString().replace(/#.*$/, '');
-}
-
-function wait_for_update(test, registration) {
-  if (!registration || registration.unregister == undefined) {
-    return Promise.reject(new Error(
-      'wait_for_update must be passed a ServiceWorkerRegistration'));
-  }
-
-  return new Promise(test.step_func(function(resolve) {
-      registration.addEventListener('updatefound', test.step_func(function() {
-          resolve(registration.installing);
-        }));
-    }));
-}
-
-function wait_for_state(test, worker, state) {
-  if (!worker || worker.state == undefined) {
-    return Promise.reject(new Error(
-      'wait_for_state must be passed a ServiceWorker'));
-  }
-  if (worker.state === state)
-    return Promise.resolve(state);
-
-  if (state === 'installing') {
-    switch (worker.state) {
-      case 'installed':
-      case 'activating':
-      case 'activated':
-      case 'redundant':
-        return Promise.reject(new Error(
-          'worker is ' + worker.state + ' but waiting for ' + state));
-    }
-  }
-
-  if (state === 'installed') {
-    switch (worker.state) {
-      case 'activating':
-      case 'activated':
-      case 'redundant':
-        return Promise.reject(new Error(
-          'worker is ' + worker.state + ' but waiting for ' + state));
-    }
-  }
-
-  if (state === 'activating') {
-    switch (worker.state) {
-      case 'activated':
-      case 'redundant':
-        return Promise.reject(new Error(
-          'worker is ' + worker.state + ' but waiting for ' + state));
-    }
-  }
-
-  if (state === 'activated') {
-    switch (worker.state) {
-      case 'redundant':
-        return Promise.reject(new Error(
-          'worker is ' + worker.state + ' but waiting for ' + state));
-    }
-  }
-
-  return new Promise(test.step_func(function(resolve) {
-      worker.addEventListener('statechange', test.step_func(function() {
-          if (worker.state === state)
-            resolve(state);
-        }));
-    }));
-}
-
-// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
-// is the service worker script URL. This function:
-// - Instantiates a new test with the description specified in |description|.
-//   The test will succeed if the specified service worker can be successfully
-//   registered and installed.
-// - Creates a new ServiceWorker registration with a scope unique to the current
-//   document URL. Note that this doesn't allow more than one
-//   service_worker_test() to be run from the same document.
-// - Waits for the new worker to begin installing.
-// - Imports tests results from tests running inside the ServiceWorker.
-function service_worker_test(url, description) {
-  // If the document URL is https://example.com/document and the script URL is
-  // https://example.com/script/worker.js, then the scope would be
-  // https://example.com/script/scope/document.
-  var scope = new URL('scope' + window.location.pathname,
-                      new URL(url, window.location)).toString();
-  promise_test(function(test) {
-      return service_worker_unregister_and_register(test, url, scope)
-        .then(function(registration) {
-            add_completion_callback(function() {
-                registration.unregister();
-              });
-            return wait_for_update(test, registration)
-              .then(function(worker) {
-                  return fetch_tests_from_worker(worker);
-                });
-          });
-    }, description);
-}
-
-function get_host_info() {
-  var ORIGINAL_HOST = '127.0.0.1';
-  var REMOTE_HOST = 'localhost';
-  var UNAUTHENTICATED_HOST = 'example.test';
-  var HTTP_PORT = 8000;
-  var HTTPS_PORT = 8443;
-  try {
-    // In W3C test, we can get the hostname and port number in config.json
-    // using wptserve's built-in pipe.
-    // http://wptserve.readthedocs.org/en/latest/pipes.html#built-in-pipes
-    HTTP_PORT = eval('{{ports[http][0]}}');
-    HTTPS_PORT = eval('{{ports[https][0]}}');
-    ORIGINAL_HOST = eval('\'{{host}}\'');
-    REMOTE_HOST = 'www1.' + ORIGINAL_HOST;
-  } catch (e) {
-  }
-  return {
-    HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + ':' + HTTP_PORT,
-    HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + ':' + HTTPS_PORT,
-    HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + ':' + HTTP_PORT,
-    HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + ':' + HTTPS_PORT,
-    UNAUTHENTICATED_ORIGIN: 'http://' + UNAUTHENTICATED_HOST + ':' + HTTP_PORT
-  };
-}
-
-function base_path() {
-  return location.pathname.replace(/\/[^\/]*$/, '/');
-}
-
-function test_login(test, origin, username, password, cookie) {
-  return new Promise(function(resolve, reject) {
-      with_iframe(
-        origin +
-        '/serviceworker/resources/fetch-access-control-login.html')
-        .then(test.step_func(function(frame) {
-            var channel = new MessageChannel();
-            channel.port1.onmessage = test.step_func(function() {
-                frame.remove();
-                resolve();
-              });
-            frame.contentWindow.postMessage(
-              {username: username, password: password, cookie: cookie},
-              origin, [channel.port2]);
-          }));
-    });
-}
-
-function login(test) {
-  return test_login(test, 'http://127.0.0.1:8000',
-                    'username1', 'password1', 'cookie1')
-    .then(function() {
-        return test_login(test, 'http://localhost:8000',
-                          'username2', 'password2', 'cookie2');
-      });
-}
-
-function login_https(test) {
-  return test_login(test, 'https://127.0.0.1:8443',
-                    'username1s', 'password1s', 'cookie1')
-    .then(function() {
-        return test_login(test, 'https://localhost:8443',
-                          'username2s', 'password2s', 'cookie2');
-      });
-}
diff --git a/third_party/web_platform_tests/service-workers/specgen.json b/third_party/web_platform_tests/service-workers/specgen.json
deleted file mode 100644
index 5d76da8..0000000
--- a/third_party/web_platform_tests/service-workers/specgen.json
+++ /dev/null
@@ -1,658 +0,0 @@
-{
-    "sections": [
-        {
-            "href": "#introduction",
-            "id": "introduction",
-            "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-            "secno": "1",
-            "testable": false
-        },
-        {
-            "href": "#about",
-            "id": "about",
-            "hash": "8d3cf149aa73cff52328509ebbaffd933e8fb6af",
-            "secno": "1.1",
-            "testable": false
-        },
-        {
-            "href": "#dependencies",
-            "id": "dependencies",
-            "hash": "1355f2d7ec9bf4e617ee632c0db44f834c96435b",
-            "secno": "1.2",
-            "testable": false
-        },
-        {
-            "href": "#motivations",
-            "id": "motivations",
-            "hash": "92d899bc1e63a170d2324638d16f580b97b4f4d6",
-            "secno": "1.3",
-            "testable": false
-        },
-        {
-            "href": "#concepts",
-            "id": "concepts",
-            "hash": "589023372dc033b0a77be1cd01f54f5f8c3ebfa8",
-            "secno": "2",
-            "testable": false
-        },
-        {
-            "href": "#document-context",
-            "id": "document-context",
-            "hash": "34feeb18dea978349a2f76e6b17c127123b3db74",
-            "secno": "3",
-            "testable": false
-        },
-        {
-            "href": "#service-worker-obj",
-            "id": "service-worker-obj",
-            "hash": "6cbd0107199072ab86b36e72d08d5465b42e6da8",
-            "secno": "3.1",
-            "testPageHash": "8dbbc9aa4300f0203524f3e405dbf7ca462e7164",
-            "testPagePath": "stub-3.1-service-worker-obj.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-scope",
-            "id": "service-worker-scope",
-            "hash": "136f25ef227515a7be9b32c44967f68b34ad8924",
-            "secno": "3.1.1",
-            "testPageHash": "965a00b32d56192330aa9f6337072bb3633ad382",
-            "testPagePath": "stub-3.1.1-service-worker-scope.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-url",
-            "id": "service-worker-url",
-            "hash": "df66a1b4b3bfa3e7ab96fd491a6829fab1d18a88",
-            "secno": "3.1.2",
-            "testPageHash": "92f6aed1437bb39c5941b495ac6c5f342c025b38",
-            "testPagePath": "stub-3.1.2-service-worker-url.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-state",
-            "id": "service-worker-state",
-            "hash": "8f80f2b4cbb1c228867c9dd90c05cbecfc92dd77",
-            "secno": "3.1.3",
-            "testPageHash": "4aad1dc47572879fdc2c79a814ad21e1ef9a64ec",
-            "testPagePath": "stub-3.1.3-service-worker-state.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-on-state-change",
-            "id": "service-worker-on-state-change",
-            "hash": "0f8fd9d1431deacea72fe739f42992ab5a396bf2",
-            "secno": "3.1.4",
-            "testPageHash": "6bb309bccc1e7c74ade7fc4c6e400bafb60daceb",
-            "testPagePath": "stub-3.1.4-service-worker-on-state-change.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker",
-            "id": "navigator-service-worker",
-            "hash": "22f1ebbafca6976d0f4814b0fbb8f173bf919f06",
-            "secno": "3.2",
-            "testPageHash": "6d597735816a09ec774150029ed5136198f52ab7",
-            "testPagePath": "stub-3.2-navigator-service-worker.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-installing",
-            "id": "navigator-service-worker-installing",
-            "hash": "9675c3cdf5ba4b4155284e06a19e4de631645509",
-            "secno": "3.2.1",
-            "testPageHash": "2c8e56e74c130104e395de46bad20fb5d3021d95",
-            "testPagePath": "stub-3.2.1-navigator-service-worker-installing.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-waiting",
-            "id": "navigator-service-worker-waiting",
-            "hash": "88b4db92cc49109e6a15ddebdd219690d9648e76",
-            "secno": "3.2.2",
-            "testPageHash": "1cf6ed58bf5ecf963fed8c3d9211b853dab564e2",
-            "testPagePath": "stub-3.2.2-navigator-service-worker-waiting.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-active",
-            "id": "navigator-service-worker-active",
-            "hash": "0da48e885c77da60d1837197780049904789e3cb",
-            "secno": "3.2.3",
-            "testPageHash": "f5dca8c6eb5f29a0f9a5f06e25861e7f3106cc67",
-            "testPagePath": "stub-3.2.3-navigator-service-worker-active.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-controller",
-            "id": "navigator-service-worker-controller",
-            "hash": "293433ccb7bb2a22d8d5a81e788892e071b25e65",
-            "secno": "3.2.4",
-            "testPageHash": "6452f431d0765d7aa3135d18fee43e6664dcbb12",
-            "testPagePath": "stub-3.2.4-navigator-service-worker-controller.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-ready",
-            "id": "navigator-service-worker-ready",
-            "hash": "6240fde8d7168beeb95f4f36aa9e143319b2061b",
-            "secno": "3.2.5",
-            "testPageHash": "ae4fd694c88bab72f338d97bf96b7d23e2e83e87",
-            "testPagePath": "stub-3.2.5-navigator-service-worker-ready.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-getAll",
-            "id": "navigator-service-worker-getAll",
-            "hash": "292ee3af2cc8fadc24302446809d04bf2e9811a5",
-            "secno": "3.2.6",
-            "testPageHash": "4096ae712cc3e753456fbe05bb4d0cfc4399d2c9",
-            "testPagePath": "stub-3.2.6-navigator-service-worker-getAll.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-register",
-            "id": "navigator-service-worker-register",
-            "hash": "c999dc5f67126c9f0f02b25fd943a34b48cff618",
-            "secno": "3.2.7",
-            "testPageHash": "bde900b97dbb08b053ff8115775ea3b79a124b6e",
-            "testPagePath": "stub-3.2.7-navigator-service-worker-register.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-unregister",
-            "id": "navigator-service-worker-unregister",
-            "hash": "fd196f926f181563855e4683cc995405c1e611d0",
-            "secno": "3.2.8",
-            "testPageHash": "dbd99a1dcbcb629431617790a305e840495049eb",
-            "testPagePath": "stub-3.2.8-navigator-service-worker-unregister.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-onupdatefound",
-            "id": "navigator-service-worker-onupdatefound",
-            "hash": "2bb5aabaca24a68f9e6b4c4443968178eb1ccfe8",
-            "secno": "3.2.9",
-            "testPageHash": "eef0c1c39577abefb3654a6e9917ff2da657871b",
-            "testPagePath": "stub-3.2.9-navigator-service-worker-onupdatefound.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-oncontrollerchange",
-            "id": "navigator-service-worker-oncontrollerchange",
-            "hash": "c89a4ffba10d9285e07c38c28718719d87053994",
-            "secno": "3.2.10",
-            "testPageHash": "35e0ce2b8f4527ebbd75d4dfa3436fd7f8c79792",
-            "testPagePath": "stub-3.2.10-navigator-service-worker-oncontrollerchange.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-onreloadpage",
-            "id": "navigator-service-worker-onreloadpage",
-            "hash": "424441910abf2e1bdc3db658fe46827f7abe60a4",
-            "secno": "3.2.11",
-            "testPageHash": "ae614de17e5f339b65f77cafa6e0f5625491abfb",
-            "testPagePath": "stub-3.2.11-navigator-service-worker-onreloadpage.html",
-            "testable": true
-        },
-        {
-            "href": "#navigator-service-worker-onerror",
-            "id": "navigator-service-worker-onerror",
-            "hash": "710f7fcd2f5340147b9e030bc5932b8242cef828",
-            "secno": "3.2.12",
-            "testPageHash": "cd62779e27151d55f14ac6ab7aa41dcf723e0ac7",
-            "testPagePath": "stub-3.2.12-navigator-service-worker-onerror.html",
-            "testable": true
-        },
-        {
-            "href": "#execution-context",
-            "id": "execution-context",
-            "hash": "ddf24f0adf58237e264c3c43cb7ab07af3013c9d",
-            "secno": "4",
-            "testable": false
-        },
-        {
-            "href": "#service-worker-global-scope",
-            "id": "service-worker-global-scope",
-            "hash": "e6b8bb7f99c125f4226fc5b6c51cf03a7437f2ef",
-            "secno": "4.1",
-            "testPageHash": "2f596b6b07bcfb71c01d75f725eb52c84e9c69dd",
-            "testPagePath": "stub-4.1-service-worker-global-scope.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-caches",
-            "id": "service-worker-global-scope-caches",
-            "hash": "43d3c9f441b3a7abd0d3a7f55d93faaceeb7d97d",
-            "secno": "4.1.1",
-            "testPageHash": "f19b91c887f6312688b66b1988147a599cd9470f",
-            "testPagePath": "stub-4.1.1-service-worker-global-scope-caches.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-clients",
-            "id": "service-worker-global-scope-clients",
-            "hash": "cb83230107645229da9776ed0fc9f7bc6fcce747",
-            "secno": "4.1.2",
-            "testPageHash": "45b3aae572f7161748fa98e97b4f2b738c3dcfef",
-            "testPagePath": "stub-4.1.2-service-worker-global-scope-clients.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-scope",
-            "id": "service-worker-global-scope-scope",
-            "hash": "08c808048b647aa9d4cc0b0a0f70b06ca89af4a3",
-            "secno": "4.1.3",
-            "testPageHash": "bfe7eaf8deb8de7d2ccfbba97640478b1c81d6c7",
-            "testPagePath": "stub-4.1.3-service-worker-global-scope-scope.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-fetch",
-            "id": "service-worker-global-scope-fetch",
-            "hash": "b66133d8a2c67f9b10c274d5b05383ff76d2cd42",
-            "secno": "4.1.4",
-            "testPageHash": "2b1ffa915afddeb099dfff23f4ecf555b0710ed4",
-            "testPagePath": "stub-4.1.4-service-worker-global-scope-fetch.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-update",
-            "id": "service-worker-global-scope-update",
-            "hash": "3ddf48cecb4d4a67a329248787dd220ce17f4eff",
-            "secno": "4.1.5",
-            "testPageHash": "15879bf45f460c0ab0c02793655096c1bca418a7",
-            "testPagePath": "stub-4.1.5-service-worker-global-scope-update.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-unregister",
-            "id": "service-worker-global-scope-unregister",
-            "hash": "fff9ef2daa5689b38a17eeb9a6bd7071098ca778",
-            "secno": "4.1.6",
-            "testPageHash": "c4bf327228628b794db9c6f2eb17519e37cea6b9",
-            "testPagePath": "stub-4.1.6-service-worker-global-scope-unregister.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-global-scope-onmessage",
-            "id": "service-worker-global-scope-onmessage",
-            "hash": "bc8f6aed2d515dc7f6b0757afa02f37899082668",
-            "secno": "4.1.7",
-            "testPageHash": "9e6f2732d21871ec06e9541ea881baf962f7cdf4",
-            "testPagePath": "stub-4.1.7-service-worker-global-scope-onmessage.html",
-            "testable": true
-        },
-        {
-            "href": "#client",
-            "id": "client",
-            "hash": "47a1c10cd9e4db9a5c86d9bcf80477f771ea954c",
-            "secno": "4.2",
-            "testPageHash": "21d74c1af0b3176b029c9b62b37fe73436e0f197",
-            "testPagePath": "stub-4.2-client.html",
-            "testable": true
-        },
-        {
-            "href": "#service-worker-clients",
-            "id": "service-worker-clients",
-            "hash": "c2c6f4873f07b53705a46b2bd44ba10f84dd2b56",
-            "secno": "4.3",
-            "testPageHash": "9c0366e6cfd28caaeaf940bad2b3c7ace93037f6",
-            "testPagePath": "stub-4.3-service-worker-clients.html",
-            "testable": true
-        },
-        {
-            "href": "#get-serviced-method",
-            "id": "get-serviced-method",
-            "hash": "299abaa21cf096e423edfa19755987986f742a1f",
-            "secno": "4.3.1",
-            "testPageHash": "efeb1c2dc8144c30e6628cb56b3e532531ee1e88",
-            "testPagePath": "stub-4.3.1-get-serviced-method.html",
-            "testable": true
-        },
-        {
-            "href": "#reloadall-method",
-            "id": "reloadall-method",
-            "hash": "bb4d775d261e69cbeaf65c123e949c24cf542ae7",
-            "secno": "4.3.2",
-            "testPageHash": "d1a4dde873b77201b4de745d2083bf63549b0b8b",
-            "testPagePath": "stub-4.3.2-reloadall-method.html",
-            "testable": true
-        },
-        {
-            "href": "#request-objects",
-            "id": "request-objects",
-            "hash": "65ae6c08f720a2eedb7b140f5635a5ac46ddadfc",
-            "secno": "4.4",
-            "testPageHash": "ec493c70e8a0d8d3eeb0ecaef59610aed97d298e",
-            "testPagePath": "stub-4.4-request-objects.html",
-            "testable": true
-        },
-        {
-            "href": "#response-objects",
-            "id": "response-objects",
-            "hash": "2efbff63c70ab92f93e4acd021409b9df4776882",
-            "secno": "4.5",
-            "testPageHash": "8340b69d62f111f56095c5fe9047d9215fa7aefc",
-            "testPagePath": "stub-4.5-response-objects.html",
-            "testable": true
-        },
-        {
-            "href": "#abstract-response",
-            "id": "abstract-response",
-            "hash": "bddc306a9892c0bca43e8b361c1ee22b87759e23",
-            "secno": "4.5.1",
-            "testable": false
-        },
-        {
-            "href": "#response",
-            "id": "response",
-            "hash": "6471d25755bdab0d4f72413f9367b7bb36c53a6f",
-            "secno": "4.5.2",
-            "testPageHash": "346d63cc7eb8ee412f5f704ba241205c8d437540",
-            "testPagePath": "stub-4.5.2-response.html",
-            "testable": true
-        },
-        {
-            "href": "#header",
-            "id": "header",
-            "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-            "secno": "4.5.3",
-            "testable": false
-        },
-        {
-            "href": "#opaque-response",
-            "id": "opaque-response",
-            "hash": "df5431f4fbd26d81f2d4f567309c6a7a26dbfd4a",
-            "secno": "4.5.4",
-            "testPageHash": "85373f290cf594f0f09eb0a76bc6ef6299be595f",
-            "testPagePath": "stub-4.5.4-opaque-response.html",
-            "testable": true
-        },
-        {
-            "href": "#cors-response",
-            "id": "cors-response",
-            "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-            "secno": "4.5.5",
-            "testable": false
-        },
-        {
-            "href": "#cache-objects",
-            "id": "cache-objects",
-            "hash": "001d0dfb8fbcbcb6443d1be2b722c9a84d6fd95b",
-            "secno": "4.6",
-            "testPageHash": "c1ef341d15a8c76d015eef57842ed10e62c02927",
-            "testPagePath": "stub-4.6-cache-objects.html",
-            "testable": true
-        },
-        {
-            "href": "#cache-lifetimes",
-            "id": "cache-lifetimes",
-            "hash": "7c73698ca9b686a0314ddf368bf8ad4ca6af392f",
-            "secno": "4.6.1",
-            "testPageHash": "f3524320a98f2fbdc5d711de82770957a7f5ec4b",
-            "testPagePath": "stub-4.6.1-cache-lifetimes.html",
-            "testable": true
-        },
-        {
-            "href": "#cache",
-            "id": "cache",
-            "hash": "bf1fe844577ab57a60eb550be24335a3321ca2ee",
-            "secno": "4.6.2",
-            "testPageHash": "c55b7b05c8e2f4b65722e16cdbcd78ffdfe1e4bf",
-            "testPagePath": "stub-4.6.2-cache.html",
-            "testable": true
-        },
-        {
-            "href": "#cache-storage",
-            "id": "cache-storage",
-            "hash": "9cdaac070f56e55d66a89cd4b6e669a04aa73b82",
-            "secno": "4.6.3",
-            "testPageHash": "ee6902f170d94cc1e3a4a00f4c90e7e19c4dff95",
-            "testPagePath": "stub-4.6.3-cache-storage.html",
-            "testable": true
-        },
-        {
-            "href": "#events",
-            "id": "events",
-            "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-            "secno": "4.7",
-            "testable": false
-        },
-        {
-            "href": "#install-phase-event",
-            "id": "install-phase-event",
-            "hash": "8495382b418adbbed436b2002ab0155a3a295ef2",
-            "secno": "4.7.1",
-            "testPageHash": "e48e98d51936bd57d21903615203f2b78d3f4b12",
-            "testPagePath": "stub-4.7.1-install-phase-event.html",
-            "testable": true
-        },
-        {
-            "href": "#wait-until-method",
-            "id": "wait-until-method",
-            "hash": "295fb5d4932396fd13365ed2fe57aa672f1f2a56",
-            "secno": "4.7.1.1",
-            "testPageHash": "c3769e51852b8438a97c39c50fa62351a73c4ee6",
-            "testPagePath": "stub-4.7.1.1-wait-until-method.html",
-            "testable": true
-        },
-        {
-            "href": "#install-event",
-            "id": "install-event",
-            "hash": "3a0f6da1771c22ab21ddc00729433a4d95ac6782",
-            "secno": "4.7.2",
-            "testPageHash": "9a103cc461eaca3da75db583ce08f13ecd2b1a98",
-            "testPagePath": "stub-4.7.2-install-event.html",
-            "testable": true
-        },
-        {
-            "href": "#install-event-section",
-            "id": "install-event-section",
-            "hash": "4631577df2efc1a4350000461629bc1ca93dbd14",
-            "secno": "4.7.2.1",
-            "testPageHash": "32f54e74bef784d2f0ac772b44abeee06573062d",
-            "testPagePath": "stub-4.7.2.1-install-event-section.html",
-            "testable": true
-        },
-        {
-            "href": "#replace-method",
-            "id": "replace-method",
-            "hash": "b9093b05204d09748311023b4c737ede02ff8115",
-            "secno": "4.7.2.2",
-            "testPageHash": "372bed923f8c35c4923634ae27fa121919ac0fec",
-            "testPagePath": "stub-4.7.2.2-replace-method.html",
-            "testable": true
-        },
-        {
-            "href": "#activate-event",
-            "id": "activate-event",
-            "hash": "ac3d03aa0ed961fb1122850aeab92c302c55ecd0",
-            "secno": "4.7.3",
-            "testPageHash": "6241762ab1d6f430fa9b7cc8f02a00e6591c6bc6",
-            "testPagePath": "stub-4.7.3-activate-event.html",
-            "testable": true
-        },
-        {
-            "href": "#fetch-event",
-            "id": "fetch-event",
-            "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-            "secno": "4.7.4",
-            "testable": false
-        },
-        {
-            "href": "#fetch-event-section",
-            "id": "fetch-event-section",
-            "hash": "ae24fda9664a3bd7b7fe2a8712ac469c3ee7128e",
-            "secno": "4.7.4.1",
-            "testPageHash": "393fc7b65e9f5afd18da666b6b206ccd639397cd",
-            "testPagePath": "stub-4.7.4.1-fetch-event-section.html",
-            "testable": true
-        },
-        {
-            "href": "#respond-with-method",
-            "id": "respond-with-method",
-            "hash": "7e4f010e2ec1ea0500b435cf599ba58942164457",
-            "secno": "4.7.4.2",
-            "testPageHash": "31e0acd058b9a5b722ae9f405b50bc94d31596b8",
-            "testPagePath": "stub-4.7.4.2-respond-with-method.html",
-            "testable": true
-        },
-        {
-            "href": "#default-method",
-            "id": "default-method",
-            "hash": "4d6f8f93b2e10ab0e486dbf464ff107ec1a6aa4c",
-            "secno": "4.7.4.3",
-            "testPageHash": "34e015c973887e2b3bf8b6db62f75d5d417a43cc",
-            "testPagePath": "stub-4.7.4.3-default-method.html",
-            "testable": true
-        },
-        {
-            "href": "#is-reload-attribute",
-            "id": "is-reload-attribute",
-            "hash": "6e1afd9e8940e9cd38aa7de1ed57e8c5b1a60e3d",
-            "secno": "4.7.4.4",
-            "testPageHash": "703a6469782d37be3c25e2214f897d1064acca47",
-            "testPagePath": "stub-4.7.4.4-is-reload-attribute.html",
-            "testable": true
-        },
-        {
-            "href": "#security-considerations",
-            "id": "security-considerations",
-            "hash": "5b02b143172647dd7f74f0464dffa7ec7d0e8f94",
-            "secno": "5",
-            "testable": false
-        },
-        {
-            "href": "#origin-relativity",
-            "id": "origin-relativity",
-            "hash": "72bbbd7d3d43a859af6ff9f19353210ddfcc26de",
-            "secno": "5.1",
-            "testPageHash": "1c92607dfac57b0f59654d059a4a67e0f984b84d",
-            "testPagePath": "stub-5.1-origin-relativity.html",
-            "testable": true
-        },
-        {
-            "href": "#cross-origin-resources",
-            "id": "cross-origin-resources",
-            "hash": "6176879ecfb5ae769679ceef4ee1e8889be8df92",
-            "secno": "5.2",
-            "testPageHash": "bcf85ba278c70c086645c416cee729ce753bc528",
-            "testPagePath": "stub-5.2-cross-origin-resources.html",
-            "testable": true
-        },
-        {
-            "href": "#storage-considerations",
-            "id": "storage-considerations",
-            "hash": "e101cee2062749b1a73086492377458251a5e875",
-            "secno": "6",
-            "testable": false
-        },
-        {
-            "href": "#extensibility",
-            "id": "extensibility",
-            "hash": "ef1b382bb89c52e01edad421b02b237765a21ce7",
-            "secno": "7",
-            "testable": false
-        },
-        {
-            "href": "#algorithms",
-            "id": "algorithms",
-            "hash": "d130247eab1d368efea646ff369e65f6c0c19481",
-            "secno": "8",
-            "testable": false
-        },
-        {
-            "href": "#registration-algorithm",
-            "id": "registration-algorithm",
-            "hash": "b688d090671c08ca17ea7cadc561e6d471ee099e",
-            "secno": "8.1",
-            "testable": false
-        },
-        {
-            "href": "#update-algorithm",
-            "id": "update-algorithm",
-            "hash": "679a19fef428affc83103c1eec0dbd3be40c4e2a",
-            "secno": "8.2",
-            "testable": false
-        },
-        {
-            "href": "#soft-update-algorithm",
-            "id": "soft-update-algorithm",
-            "hash": "8eb103f5cd0e595ee5e25f075e8c6239211e482a",
-            "secno": "8.3",
-            "testable": false
-        },
-        {
-            "href": "#installation-algorithm",
-            "id": "installation-algorithm",
-            "hash": "5874d9247d979009b67aedf964ae097837cfb3d9",
-            "secno": "8.4",
-            "testable": false
-        },
-        {
-            "href": "#activation-algorithm",
-            "id": "activation-algorithm",
-            "hash": "648b34baf6e7c2096a842e6d367949117843108e",
-            "secno": "8.5",
-            "testable": false
-        },
-        {
-            "href": "#on-fetch-request-algorithm",
-            "id": "on-fetch-request-algorithm",
-            "hash": "e1da43671071ec307f99cd781fc9b46353f3adfd",
-            "secno": "8.6",
-            "testable": false
-        },
-        {
-            "href": "#on-document-unload-algorithm",
-            "id": "on-document-unload-algorithm",
-            "hash": "8a7196b5dd04ad4fb9b96e16a52f4f7ac1906763",
-            "secno": "8.7",
-            "testable": false
-        },
-        {
-            "href": "#unregistration-algorithm",
-            "id": "unregistration-algorithm",
-            "hash": "0114db166d42211d0d7ab4b8e77de64a9fc97517",
-            "secno": "8.8",
-            "testable": false
-        },
-        {
-            "href": "#update-state-algorithm",
-            "id": "update-state-algorithm",
-            "hash": "2ed8a1e7479f1a8ad038aa44ccdd5e4f6b65cf05",
-            "secno": "8.9",
-            "testable": false
-        },
-        {
-            "href": "#scope-match-algorithm",
-            "id": "scope-match-algorithm",
-            "hash": "a2117fb34a8fa4ca3e832d9276477cfc1318dd1a",
-            "secno": "8.10",
-            "testable": false
-        },
-        {
-            "href": "#get-registration-algorithm",
-            "id": "get-registration-algorithm",
-            "hash": "b20332db952ba8f4b7e5f65b740a18da4a199c2e",
-            "secno": "8.11",
-            "testable": false
-        },
-        {
-            "href": "#get-newest-worker-algorithm",
-            "id": "get-newest-worker-algorithm",
-            "hash": "72dc1cbee8c98501931c411018fd1cad4376142b",
-            "secno": "8.12",
-            "testable": false
-        },
-        {
-            "href": "#acknowledgements",
-            "id": "acknowledgements",
-            "hash": "6347067ca5a574f8cc80c76d95dee568042d059b",
-            "secno": "9",
-            "testable": false
-        }
-    ],
-    "specUrl": "https://slightlyoff.github.io/ServiceWorker/spec/service_worker/"
-}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html b/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html
deleted file mode 100644
index 588720e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorker</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-obj">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Constructor()] // no-op constructor
-interface ServiceWorker : Worker {
-  readonly attribute DOMString scope;
-  readonly attribute DOMString url;
-  readonly attribute ServiceWorkerState state;
-
-  // event
-  attribute EventHandler onstatechange;
-};
-
-enum ServiceWorkerState {
-  "installing",
-  "installed",
-  "activating",
-  "activated",
-  "redundant"
-};
-</pre>
-
-<!--
-The `ServiceWorker` interface represents the document-side view of a Service
-Worker. This object provides a no-op constructor. Callers should note that only
-`ServiceWorker` objects created by the user agent (see
-`navigator.serviceWorker.installing`, `navigator.serviceWorker.waiting`,
-`navigator.serviceWorker.active` and `navigator.serviceWorker.controller`) will
-provide meaningful functionality.
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface EventHandler {};
-        interface Worker {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            ServiceWorker: ["throw new Error ('No object defined for the ServiceWorker interface')"],
-            ServiceWorkerState: ["throw new Error ('No object defined for the ServiceWorkerState enum')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html b/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html
deleted file mode 100644
index 47b4935..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: scope</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-scope">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The `scope` of a `ServiceWorker` object reflects the [URL scope][1] of the
-associated Service Worker [registration][2]. The `scope` attribute must return
-the [serialization][3] of the URL representing the [URL scope][1] of the
-associated Service Worker [registration][2].
-
-For example, consider a document created by a navigation to
-`https://example.com/app.html` which [matches][4] via the following
-registration call which has been previously executed:
-// Script on the page https://example.com/app.html
-navigator.serviceWorker.register("/service_worker.js", { scope: "/*" });
-The value of `navigator.serviceWorker.controller.scope` will be
-`"https://example.com/*"`.
-
-
-
-[1]: #url-scope
-[2]: #registration
-[3]: http://url.spec.whatwg.org/#concept-url-serializer
-[4]: #on-fetch-request-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section scope so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html b/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html
deleted file mode 100644
index be17bb8..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: url</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-url">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The `url` attribute must return the [serialization][1] of the URL of the script
-of the Service Worker, identified by its [URL scope][2], that is associated
-with the [ServiceWorkerGlobalScope][3] object. The `url` attribute is always an
-[absolute URL][4] corresponding to the script file which the Service Worker
-evaluates.
-
-In the example in section 3.1.1, the value of
-`navigator.serviceWorker.controller.url` will be
-`"https://example.com/service_worker.js"`.
-
-
-
-[1]: http://url.spec.whatwg.org/#concept-url-serializer
-[2]: #url-scope
-[3]: #service-worker-global-scope-interface
-[4]: http://url.spec.whatwg.org/#concept-absolute-url
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section url so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html b/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html
deleted file mode 100644
index 40f4da4..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html
+++ /dev/null
@@ -1,76 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: state</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-state">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The [ServiceWorker][1] object can be in several states. The `state` attribute
-must return the current state, which must be one of the following values
-defined in the [ServiceWorkerState][2] enumeration:
-
-`"installing"`:
-    The Service Worker represented by the [ServiceWorker][1] object has entered
-    and is running the steps in the [installation process][3]. During this
-    state, `e.waitUntil(p)` can be called inside the `oninstall` event handler
-    of the associcated [ServiceWorkerGloberScope][4] object to extend the life
-    of the [installing worker][5] until the passed [Promise][6] resolves
-    successfully. This is primarily used to ensure that the Service Worker is
-    not active until all of the core caches are populated.
-`"installed"`:
-    The Service Worker represented by the [ServiceWorker][1] object has
-    completed the steps in the [installation process][3]. The Service Worker in
-    this state is considered the [worker in waiting][7].
-`"activating"`:
-    The Service Worker represented by the [ServiceWorker][1] object has entered
-    and is running the steps in the [activation process][8]. During this state,
-    `e.waitUntil(p)` can be called inside the `onactivate` event handler of the
-    associated [ServiceWorkerGloberScope][9] object to extend the life of the
-    activating [active worker][10] until the passed [Promise][6] resolves
-    successfully. Note that no [functional events][11] are dispatched until the
-    state becomes `"activated"`.
-`"activated"`:
-    The Service Worker represented by the [ServiceWorker][1] object has
-    completed the steps in the [activation process][8]. The Service Worker in
-    this state is considered the [active worker][10] ready to [control][12] the
-    documents in matching scope upon subsequence [navigation][13].
-`"redundant"`:
-    A newly created Service Worker [registration][14] is replacing the current
-    [registration][14] of the Service Worker.
-
-
-
-[1]: #service-worker-interface
-[2]: #service-worker-state-enum
-[3]: #installation-process
-[4]: #service-worker-glober-scope-interface
-[5]: #installing-worker
-[6]: http://goo.gl/3TobQS
-[7]: #worker-in-waiting
-[8]: #activation-process
-[9]: #service-worker-global-scope-interface
-[10]: #active-worker
-[11]: #functional-events
-[12]: #document-control
-[13]: http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#navigate
-[14]: #registration
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section state so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html b/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html
deleted file mode 100644
index 3613874..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onstatechange</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-on-state-change">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`onstatechange` is the [event handler][1] that must be supported as attribute
-by the `[ServiceWorker][2]` object. A `statechange` event using the
-`[Event][3]` interface is dispatched on `[ServiceWorker][2]` object when the
-`state` attribute of the `ServiceWorker` object is changed.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-interface
-[3]: http://goo.gl/Mzv7Dv
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section onstatechange so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html b/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html
deleted file mode 100644
index 0855d6c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: navigator.serviceWorker</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<!--
-The `serviceWorker` attribute of the [Navigator][1] interface must return an
-instance of the `ServiceWorkerContainer` interface, which provides access to
-registration, removal, upgrade, and communication with Service Workers that are
-(or will become) active for the current document. Communication with these
-workers is provided via standard [HTML5 messaging APIs][2], and [messaging
-occurs as per usual with Web Workers][3].
--->
-<script type=text/plain id="idl_0">
-partial interface Navigator {
-  readonly attribute ServiceWorkerContainer serviceWorker;
-};
-
-interface ServiceWorkerContainer : EventTarget {
-  [Unforgeable] readonly attribute ServiceWorker? installing;
-  [Unforgeable] readonly attribute ServiceWorker? waiting;
-  [Unforgeable] readonly attribute ServiceWorker? active;
-  [Unforgeable] readonly attribute ServiceWorker? controller;
-  readonly attribute Promise<ServiceWorker> ready;
-
-  Promise<sequence<ServiceWorker>?> getAll();
-  Promise<ServiceWorker> register(DOMString url, optional RegistrationOptionList options);
-  Promise<any> unregister(DOMString? scope);
-
-  // events
-  attribute EventHandler onupdatefound;
-  attribute EventHandler oncontrollerchange;
-  attribute EventHandler onreloadpage;
-  attribute EventHandler onerror;
-};
-
-dictionary RegistrationOptionList {
-  DOMString scope = "/*";
-};
-
-interface ReloadPageEvent : Event {
-  void waitUntil(Promise<any> f);
-};
-</script>
-
-<!--
-[1]: http://goo.gl/I7WAhg
-[2]: http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html
-[3]: http://www.w3.org/TR/workers/#dom-worker-postmessage
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface ServiceWorker {};
-        interface EventHandler {};
-        interface EventTarget {};
-        interface Event {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            Navigator: ["throw new Error ('No object defined for the Navigator interface')"],
-            ServiceWorkerContainer: ["throw new Error ('No object defined for the ServiceWorkerContainer interface')"],
-            RegistrationOptionList: ["throw new Error ('No object defined for the RegistrationOptionList dictionary')"],
-            ReloadPageEvent: ["throw new Error ('No object defined for the ReloadPageEvent interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html b/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html
deleted file mode 100644
index d73a35b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: installing</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-installing">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.installing` must return a [ServiceWorker][1] object
-representing the [installing worker][2] that is currently undergoing the
-installation process (from step 1 to step 7 of the [_Installation
-algorithm][3]) for the given [URL scope][4] in which the document may be
-[controlled][5] when the Service Worker becomes the [active worker][6].
-`navigator.serviceWorker.installing` returns `null` if no Service Worker
-[registration][7] is in the [installation process][8].
-
-[1]: #service-worker-interface
-[2]: #installing-worker
-[3]: #installation-algorithm
-[4]: #url-scope
-[5]: #document-control
-[6]: #active-worker
-[7]: #service-worker-registration-internal-interface
-[8]: #installation-process
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section installing so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html b/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html
deleted file mode 100644
index 1e23e82..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: oncontrollerchange</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-oncontrollerchange">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.oncontrollerchange` is the [event handler][1] that
-must be supported as attribute by the `[ServiceWorkerContainer][2]` object. A
-`controllerchange` event using the `[Event][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object (See step 7 of the [_Activation
-algorithm][4]) when the associated Service Worker [registration][5] for the
-document enters the [activation process][6]. When the [activation process][6]
-is triggered by `replace()` method call within the event handler of the
-`install` event, `navigator.serviceWorker.controller` immediately reflects the
-[active worker][7] as the Service Worker that [controls][8] the document.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/Mzv7Dv
-[4]: #activation-algorithm
-[5]: #registration
-[6]: #activation-process
-[7]: #active-worker
-[8]: #document-control
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section oncontrollerchange so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html b/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html
deleted file mode 100644
index ef3fd10..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onreloadpage</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onreloadpage">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.onreloadpage` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An event
-named `reloadpage` using the `[ReloadPageEvent][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object when the page reload is triggered by the
-`[self.clients.reloadAll()][4]` method call from the [active worker][5],
-represented by its associated [ServiceWorkerGlobalScope][6] object, for the
-document.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: #reload-page-event-interface
-[4]: #reloadall-method
-[5]: #active-worker
-[6]: #service-worker-global-scope-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section onreloadpage so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html b/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html
deleted file mode 100644
index e254256..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onerror</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onerror">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.onerror` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An event
-named `error` using the `[ErrorEvent][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object for any error from the associated
-`[ServiceWorker][4]` objects.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/FKuWgu
-[4]: #service-worker-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section onerror so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html b/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html
deleted file mode 100644
index 2852d97..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: waiting</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-waiting">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.waiting` must return a [ServiceWorker][1] object
-representing the waiting Service Worker that is considered the [worker in
-waiting][2] for the document. `navigator.serviceWorker.waiting` returns `null`
-if there is no [worker in waiting][2] for the document.
-
-[1]: #service-worker-interface
-[2]: #worker-in-waiting
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section waiting so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html b/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html
deleted file mode 100644
index 3ac45d1..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: active</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-active">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.active` must return a [ServiceWorker][1] object
-representing the [active worker][2] that is currently undergoing or completed
-the activation process (from step 4 to step 9 of the [_Activation
-algorithm][3]) for the given [URL scope][4] in which the document is controlled
-(or to be controlled). `navigator.serviceWorker.active` returns `null` if no
-Service Worker [registration][5] is in the [activation process][6].
-
-[1]: #service-worker-interface
-[2]: #active-worker
-[3]: #activation-algorithm
-[4]: #url-scope
-[5]: #service-worker-registration-internal-interface
-[6]: #activation-process
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section active so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html b/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html
deleted file mode 100644
index 90378dd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: controller</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-controller">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.controller` must return a [ServiceWorker][1] object
-representing the [active worker][2] that currently handles resource requests
-for the document. `navigator.serviceWorker.controller` returns `null` if the
-current document was not [created under a Service Worker][3] (See step 6-1 of
-[_OnFetchRequest][3] algorithm) or the request is a force refresh
-(shift+refresh).
-
-[1]: #service-worker-interface
-[2]: #active-worker
-[3]: #on-fetch-request-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section controller so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html b/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html
deleted file mode 100644
index f3b1ca7..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ready</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-ready">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.ready` attribute must return the result of running
-these steps:
-
-1.  Let `promise` be a newly-created [promise][1].
-2.  Return `promise`.
-3.  Run the following steps asynchronously:
-    1.  Let `registration` be the result of running [_ScopeMatch
-        algorithm][2] with document's url as its argument.
-    2.  If `registration` is null, then:
-        1.  Wait for the document to have a matching [registration][3].
-    3.  If the [registration][3], represented by `registration`, for the
-        document has an [active worker][4], then:
-        1.  Resolve `promise` with the [ServiceWorker][5] object associated
-            with the [active worker][4].
-        2.  Abort these steps.
-    4.  If the [registration][3], represented by `registration`, for the
-        document has a [worker in waiting][6], then:
-        1.  Resolve `promise` with the [ServiceWorker][5] object associated
-            with the [worker in waiting][6].
-        2.  Abort these steps.
-    5.  Wait until the [registration][3], represented by `registration`,
-        for the document acquires a [worker in waiting][6] through a new
-        [installation process][7].
-    6.  Resolve `promise` with the [ServiceWorker][5] object associated
-        with the [worker in waiting][6].
-Note that `ready` attribute is desinged in a way that the returned [promise][1]
-will never reject. Instead, it waits until the [promise][1] resolves with a
-newly installed [worker in waiting][6]. Hence, the `state` of the acquired
-[`ServiceWorker`][8] object is either `installed`, `activating` or `activated`.
-
-
-
-[1]: http://goo.gl/3TobQS
-[2]: #scope-match-algorithm
-[3]: #registration
-[4]: #active-worker
-[5]: #service-worker-interface
-[6]: #worker-in-waiting
-[7]: #installation-process
-[8]: #service-worker
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section ready so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html b/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html
deleted file mode 100644
index 18180b9..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: getAll()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-getAll">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.getAll()` method must return a promise that resolves
-with the array of the ServiceWorker objects in `installing`, `installed`,
-`activating` and `activated` states.
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section getAll() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html b/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html
deleted file mode 100644
index c9253dd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: register()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-register">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.register(url, options)` method must run the
-[Registration algorithm][1] passing `url` and `options`.`scope` as the
-arguments.
-
-[1]: #registration-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section register() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html b/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html
deleted file mode 100644
index c4c0c24..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: unregister()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-unregister">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.unregister(scope)` method must run the [Unregistration
-algorithm][1] passing `scope` as the argument.
-
-[1]: #unregistration-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section unregister() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html b/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html
deleted file mode 100644
index 4502b2e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onupdatefound</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onupdatefound">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`navigator.serviceWorker.onupdatefound` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An
-`updatefound` event using the `[Event][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object (See step 4 of the [_Installation
-algorithm][4]) when the associated Service Worker [registration][5] for the
-document enters the [installation process][6] such that
-`navigator.serviceWorker.installing` becomes the new [installing worker][7].
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/Mzv7Dv
-[4]: #installation-algorithm
-[5]: #registration
-[6]: #installation-process
-[7]: #installing-worker
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section onupdatefound so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html b/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html
deleted file mode 100644
index ce6a045..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorkerGlobalScope</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Global]
-interface ServiceWorkerGlobalScope : WorkerGlobalScope {
-  readonly attribute CacheStorage caches;
-  // A container for a list of window objects, identifiable by ID, that
-  // correspond to windows (or workers) that are "controlled" by this SW
-  readonly attribute ServiceWorkerClients clients;
-  [Unforgeable] readonly attribute DOMString scope;
-
-  Promise<any> fetch((Request or ScalarValueString) request);
-
-  void update();
-  void unregister();
-
-  attribute EventHandler oninstall;
-  attribute EventHandler onactivate;
-  attribute EventHandler onfetch;
-  attribute EventHandler onbeforeevicted;
-  attribute EventHandler onevicted;
-
-  // The event.source of these MessageEvents are instances of Client
-  attribute EventHandler onmessage;
-
-  // close() method inherited from WorkerGlobalScope is not exposed.
-};
-</pre>
-
-<!--
-The `ServiceWorkerGlobalScope` interface represents the global execution
-context of a Service Worker. `ServiceWorkerGlobalScope` object provides
-generic, event-driven, time-limited script execution contexts that run at an
-origin. Once successfully [registered][1], a Service Worker is started, kept
-alive and killed by their relationship to events, not documents. Any type of
-synchronous requests MUST NOT be initiated inside of a Service Worker.
-
-[1]: #navigator-service-worker-register
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface CacheStorage {};
-        interface ServiceWorkerClients {};
-        interface Request {};
-        interface ScalarValueString {};
-        interface EventHandler {};
-        interface WorkerGlobalScope {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            ServiceWorkerGlobalScope: ["throw new Error ('No object defined for the ServiceWorkerGlobalScope interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html b/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html
deleted file mode 100644
index 4e68cc2..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: caches</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-caches">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`self.caches` must return the `[CacheStorage][1]` object that is the global
-asynchronous map object for the `[ServiceWorkerGlobalScope][2]` execution
-context containing the cache objects keyed by the name of the caches. Caches
-are always enumerable via `self.caches` in insertion order (per [ECMAScript 6
-Map objects][3].)
-
-[1]: #cache-storage-interface
-[2]: #service-worker-global-scope-interface
-[3]: http://goo.gl/gNnDPO
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section caches so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html b/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html
deleted file mode 100644
index 8499c71..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: clients</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-clients">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`self.clients` must return the `[ServiceWorkerClients][1]` object containing a
-list of client objects, identifiable by ID, that correspond to windows or
-workers that are [controlled][2] by this Service Worker.
-
-[1]: #service-worker-clients-interface
-[2]: #document-control
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section clients so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html b/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html
deleted file mode 100644
index 3784e1e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: scope</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-scope">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The `scope` attribute of a [ServiceWorkerGlobalScope][1] object reflects the
-[URL scope][2] of the associated Service Worker [registration][3]. The `scope`
-attribute must return the [serialization][4] of the URL representing the [URL
-scope][2] of the associated Service Worker [registration][3].
-
-[1]: #service-worker-global-scope-interface
-[2]: #url-scope
-[3]: #registration
-[4]: http://url.spec.whatwg.org/#concept-url-serializer
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section scope so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html b/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html
deleted file mode 100644
index 29548a7..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: fetch(request)</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-fetch">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`self.fetch(request)` method must run these steps:
-
-1.  Let `request` be a [request][1] represented by `request`.
-2.  Set [`client`][2] of `request` to the [JavaScript global
-    environment][3] represented by `self` object.
-3.  Let `promise` be a newly-created [promise][4].
-4.  Return `promise.`
-5.  Run the following steps asynchronously:
-    1.  Let `response` be the result of running [fetch algorithm][5] with
-        `request` as its argument.
-    2.  If `response` is a [network error][6], then:
-        1.  Reject `promise` with a new [DOMException][7] whose name is
-            "[NetworkError][8]".
-    3.  Else,
-        1.  Resolve `promise` with a new [Response][9] object associated
-            with `response`.
-
-
-
-[1]: http://goo.gl/ucOuXl
-[2]: http://goo.gl/Oxj4xQ
-[3]: http://goo.gl/ifwwCC
-[4]: http://goo.gl/3TobQS
-[5]: http://goo.gl/fGMifs
-[6]: http://goo.gl/jprjjc
-[7]: http://goo.gl/A0U8qC
-[8]: http://goo.gl/lud5HB
-[9]: http://goo.gl/Deazjv
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section fetch(request) so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html b/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html
deleted file mode 100644
index ee9552b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: update()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-update">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`update()` pings the server for an updated version of this script without
-consulting caches. `self.update()` method must run the [_SoftUpdate
-algorithm][1] passing its serviceWorkerRegistration object as the argument
-which is the result of running the [_GetRegistration algorithm][2] with
-`self.scope` as the argument. (This is conceptually the same operation that UA
-does maximum once per every 24 hours.)
-
-[1]: #soft-update-algorithm
-[2]: #get-registration-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section update() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html b/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html
deleted file mode 100644
index 9f76ee3..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: unregister()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-unregister">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`self.unregister()` method must run the [Unregistration algorithm][1]
-implicitly passing `self.scope` as the argument.
-
-[1]: #unregistration-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section unregister() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html b/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html
deleted file mode 100644
index d536a2c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onmessage</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-onmessage">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`self.onmessage` is the [event handler][1] that must be supported as attribute
-by the `ServiceWorkerGlobalScope` object. `ServiceWorkerGlobalScope` objects
-act as if they had an implicit `[MessagePort][2]` associated with them. This
-port is part of a channel that is set up when the worker is created, but it is
-not exposed. This object must never be garbage collected before the
-`ServiceWorkerGlobalScope` object.
-
-All messages received by that port must immediately be retargeted at the
-`ServiceWorkerGlobalScope` object. That is, an event named `message` using the
-`[MessageEvent][3]` interface is dispatched on ServiceWorkerGlobalScope object.
-The `event.source` of these `[MessageEvent][3]`s are instances of `[Client][4]`.
-
-
-
-[1]: http://goo.gl/rBfiz0
-[2]: http://goo.gl/tHBrI6
-[3]: http://goo.gl/S5e0b6
-[4]: #client-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section onmessage so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.2-client.html b/third_party/web_platform_tests/service-workers/stub-4.2-client.html
deleted file mode 100644
index 96976c1..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.2-client.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Client</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#client">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Constructor()] // no-op constructor
-interface Client {
-  readonly attribute unsigned long id;
-  void postMessage(any message, DOMString targetOrigin,
-                   optional sequence<Transferable> transfer);
-};
-</pre>
-
-<!--
-The `Client` interface represents the window or the worker (defined as client)
-that is [controlled][1] by the Service Worker. This object provides a no-op
-constructor. Callers should note that only `Client` objects created by the user
-agent (see [`this.clients.getServiced()`][2]) will provide meaningful
-functionality.
-
-The `id` of a `Client` identifies the specific client object from the list of
-client objects serviced by the Service Worker. The `postMessage(message,
-targetOrigin, transfer)` method of a `[Client][3]`, when called, causes a
-`[MessageEvent][4]` to be dispatched at the client object.
-
-
-
-[1]: #document-control
-[2]: #get-serviced-method
-[3]: #client-interface
-[4]: http://goo.gl/4SLWiH
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface Transferable {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            Client: ["throw new Error ('No object defined for the Client interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html b/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html
deleted file mode 100644
index beb5d59..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorkerClients</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-clients">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-interface ServiceWorkerClients {
-  // A list of client objects, identifiable by ID, that correspond to windows
-  // (or workers) that are "controlled" by this SW
-  Promise<sequence<Client>?> getServiced();
-  Promise<any> reloadAll();
-};
-</pre>
-
-<!--
-The `ServiceWorkerClients` interface represents a container for a list of
-`[Client][1]` objects.
-
-[1]: #client-interface
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface Client {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            ServiceWorkerClients: ["throw new Error ('No object defined for the ServiceWorkerClients interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html b/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html
deleted file mode 100644
index 8543bd4..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: getServiced()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#get-serviced-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The `getServiced()` method of a `ServiceWorkerClients`, when called, returns a
-[Promise][1] that will resolve with a list of `[Client][2]` objects that are
-[controlled][3] by this Service Worker.
-
-[1]: http://goo.gl/3TobQS
-[2]: #client-interface
-[3]: #document-control
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section getServiced() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html b/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html
deleted file mode 100644
index dd79a91..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: reloadAll()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#reloadall-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`reloadAll()` provides a mechanism for the worker to request synchronized
-re-fetch of all documents whose URLs match the registration's [URL scope][1].
-An event named `reloadpage` is dispatched on the `navigator.serviceWorker`
-object of each document. The in-document handlers may allow the event to
-continue, request an extension (via [`e.waitUntil()`][2]), or cancel the
-collective reload by calling [`e.preventDefault()`][3].
-
-[1]: #url-scope
-[2]: #wait-until-method
-[3]: http://goo.gl/2zH6ie
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section reloadAll() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html b/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html
deleted file mode 100644
index aa3502b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Request Objects</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#request-objects">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Constructor(optional RequestInit init)]
-interface Request {
-  attribute unsigned long timeout;
-  attribute DOMString url;
-  attribute ByteString method;
-  readonly attribute DOMString origin;
-  readonly attribute Mode mode;
-  attribute boolean synchronous;
-  attribute boolean forcePreflight;
-  attribute boolean omitCredentials;
-  readonly attribute DOMString referrer;
-  readonly attribute HeaderMap headers; // alternative: sequence<Header> headers;
-  attribute any body;
-};
-
-dictionary RequestInit {
-  unsigned long timeout = 0;
-  DOMString url;
-  boolean synchronous = false;
-  boolean forcePreflight = false;
-  boolean omitCredentials = false;
-  ByteString method = "GET";
-  HeaderMap headers;
-  any body;
-};
-
-enum Mode {
-  "same origin",
-  "tainted cross-origin",
-  "CORS",
-  "CORS-with-forced-preflight"
-};
-
-[MapClass(DOMString, DOMString)]
-interface HeaderMap {
-};
-</pre>
-
-
-
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            Request: ["throw new Error ('No object defined for the Request interface')"],
-            RequestInit: ["throw new Error ('No object defined for the RequestInit dictionary')"],
-            Mode: ["throw new Error ('No object defined for the Mode enum')"],
-            HeaderMap: ["throw new Error ('No object defined for the HeaderMap interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html b/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html
deleted file mode 100644
index a334586..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Response Objects</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#response-objects">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<!--
-`Response` objects model HTTP responses.
--->
-<script type=text/plain id="idl_0">
-[Constructor]
-interface AbstractResponse {
-};
-
-interface OpaqueResponse : AbstractResponse {
-  readonly attribute unsigned short status;
-  readonly attribute ByteString statusText;
-  // Returns a filtered list of headers. See prose for details.
-  readonly attribute HeaderMap headers;
-  // No setter for headers
-  readonly attribute DOMString url;
-};
-
-interface CORSResponse : Response {
-  readonly attribute HeaderMap headers;
-};
-
-[Constructor(optional ResponseInit responseInitDict)]
-interface Response : AbstractResponse {
-  attribute unsigned short status;
-  attribute ByteString statusText;
-  readonly attribute HeaderMap headers;
-  attribute DOMString url;
-  Promise<Blob> toBlob();
-};
-
-dictionary ResponseInit {
-  unsigned short status = 200;
-  ByteString statusText = "OK";
-  HeaderMap headers;
-};
-</pre>
-
-
-
-    <script type=text/plain id="untested_idls">
-        interface HeaderMap {};
-        interface Blob {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            AbstractResponse: ["throw new Error ('No object defined for the AbstractResponse interface')"],
-            OpaqueResponse: ["throw new Error ('No object defined for the OpaqueResponse interface')"],
-            CORSResponse: ["throw new Error ('No object defined for the CORSResponse interface')"],
-            Response: ["throw new Error ('No object defined for the Response interface')"],
-            ResponseInit: ["throw new Error ('No object defined for the ResponseInit dictionary')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html b/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html
deleted file mode 100644
index 0a8715c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Response</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#response">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`Response` objects are mutable and constructable. They model HTTP responses.
-The `fetch()` API returns this type for same-origin responses.
-
-It may be possible to set the `Location` header of a `Response` object to
-someplace not in the current origin but this is not a security issue.
-Cross-origin response bodies are opaque to script, and since only same-origin
-documents will encounter these responses, the only systems the Service Worker
-can "lie to" are same-origin (and therefore safe from the perspective of other
-origins).
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section Response so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html b/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html
deleted file mode 100644
index 1698558..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: OpaqueResponse</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#opaque-response">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`OpaqueResponse` objects are immutable but constructable. The `fetch()` API
-returns this type for cross-origin responses.
-
-Their role is to encapsulate the security properties of the web platform. As
-such, their `body` attribute will always be `undefined` and the list of
-readable `headers` is heavily filtered.
-
-`OpaqueResponse` objects may be forwarded on to rendering documents in exactly
-the same way as mutable `Response` objects.
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section OpaqueResponse so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html b/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html
deleted file mode 100644
index 3bb47b2..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Caches</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-objects">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-To allow authors to fully manage their content caches for offline use, the
-`[ServiceWorkerGlobalScope][1]` execution context provides the caching methods
-largely conforming to [ECMAScript 6 Map objects][2] with additional convenience
-methods. A domain can have multiple, named `[Cache][3]` objects, whose contents
-are entirely under the control of scripts. Caches are not shared across
-domains, and they are completely isolated from the browser's HTTP cache.
-
-[1]: #service-worker-global-scope-interface
-[2]: http://goo.gl/gNnDPO
-[3]: #cache-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section Caches so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html b/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html
deleted file mode 100644
index 9068d0c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Understanding Cache Lifetimes</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-lifetimes">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-The `[Cache][1]` instances are not part of the browser's HTTP cache. The
-`[Cache][1]` objects are exactly what authors have to manage themselves. The
-`[Cache][1]` objects do not get updated unless authors explicitly request them
-to be. The `[Cache][1]` objects do not expire unless authors delete the
-entries. The `[Cache][1]` objects do not disappear just because the Service
-Worker script is updated. That is, caches are not updated automatically.
-Updates must be manually managed. This implies that authors should version
-their caches by name and make sure to use the caches only from the version of
-the ServiceWorker that can safely operate on.
-
-[1]: #cache-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section Understanding Cache Lifetimes so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html b/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html
deleted file mode 100644
index faee336..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Cache</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Exposed=(Window,Worker)]
-interface Cache {
-  Promise<Response> match(RequestInfo request, optional CacheQueryOptions options);
-  Promise<sequence<Response>> matchAll(optional RequestInfo request, optional CacheQueryOptions options);
-  Promise<void> add(RequestInfo request);
-  Promise<void> addAll(sequence<RequestInfo> requests);
-  Promise<void> put(RequestInfo request, Response response);
-  Promise<boolean> delete(RequestInfo request, optional CacheQueryOptions options);
-  Promise<sequence<Request>> keys(optional RequestInfo request, optional CacheQueryOptions options);
-};
-
-dictionary CacheQueryOptions {
-  boolean ignoreSearch = false;
-  boolean ignoreMethod = false;
-  boolean ignoreVary = false;
-  DOMString cacheName;
-};
-
-dictionary CacheBatchOperation {
-  DOMString type;
-  Request request;
-  Response response;
-  CacheQueryOptions options;
-};
-</pre>
-
-
-
-    <script type=text/plain id="untested_idls">
-        interface AbstractResponse {};
-        interface Request {};
-        interface ScalarValueString {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            Cache: ["throw new Error ('No object defined for the Cache interface')"],
-            QueryParams: ["throw new Error ('No object defined for the QueryParams dictionary')"],
-            CacheIterationCallback: ["throw new Error ('No object defined for the CacheIterationCallback callback')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html b/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html
deleted file mode 100644
index 875220e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: CacheStorage</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Constructor(sequence<any> iterable)]
-interface CacheStorage {
-  Promise<any> match(ScalarValueString url, optional DOMString cacheName);
-  Promise<Cache> get(DOMString key);
-  Promise<boolean> has(DOMString key);
-  Promise<any> set(DOMString key, Cache val);
-  Promise<any> clear();
-  Promise<any> delete(DOMString key);
-  void forEach(CacheStorageIterationCallback callback, optional object thisArg);
-  Promise<sequence<any>> entries();
-  Promise<sequence<DOMString>> keys();
-  Promise<sequence<Cache>> values();
-  Promise<unsigned long> size();
-};
-
-callback CacheStorageIterationCallback = void (Cache value, DOMString key, CacheStorage map);
-</pre>
-
-<!--
-**Note**:[CacheStorage][1]interface is designed to largely conform
-to[ECMAScript 6 Map objects][2]but entirely async, and with additional
-convenience methods.
-
-[1]: #cache-storage-interface
-[2]: http://goo.gl/gNnDPO
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface ScalarValueString {};
-        interface Cache {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            CacheStorage: ["throw new Error ('No object defined for the CacheStorage interface')"],
-            CacheStorageIterationCallback: ["throw new Error ('No object defined for the CacheStorageIterationCallback callback')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html
deleted file mode 100644
index 195c38d..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: InstallPhaseEvent</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-phase-event">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-interface InstallPhaseEvent : Event {
-  Promise<any> waitUntil(Promise<any> f);
-};
-</pre>
-
-<!--
-Service Workers have two [Lifecycle events][1], `[install][2]` and
-`[activate][3]`. Service Workers use the `[InstallPhaseEvent][4]` interface for
-`[activate][3]` event and the `[InstallEvent][5]` interface, which inherits
-from the `[InstallPhaseEvent][4]` interface, for `[install][2]` event.
-
-[1]: #lifecycle-events
-[2]: #install-event
-[3]: #activate-event
-[4]: #install-phase-event-interface
-[5]: #install-event-interface
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface Event {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            InstallPhaseEvent: ["throw new Error ('No object defined for the InstallPhaseEvent interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html
deleted file mode 100644
index 84b730f..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.waitUntil(f)</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#wait-until-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`event.waitUntil(f)` method, when called in `oninstall` or `onactivate`,
-extends the lifetime of the event. When called in `oninstall`, it delays
-treating the installing worker until the passed [Promise][1] resolves
-successfully. This is primarily used to ensure that a `ServiceWorker` is not
-active until all of the core caches it depends on are populated. When called in
-`onactivate`, it delays treating the activating worker until the passed
-[Promise][1] resolves successfully. This is primarily used to ensure that any
-[Functional events][2] are not dispatched to the `ServiceWorker` until it
-upgrades database schemas and deletes the outdated cache entries.
-
-[1]: http://goo.gl/3TobQS
-[2]: #functional-events
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section event.waitUntil(f) so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html
deleted file mode 100644
index a2a5b1d..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: install Event</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-event">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-An event named `[install][1]` using the `[InstallEvent][2]` interface is
-dispatched on `ServiceWorkerGlobalScope` object when the state of the
-associated `ServiceWorker` changes its value to `installing`. (See step 3 of
-[_Installation algorithm][3])
-
-[1]: #install-event
-[2]: #install-event-interface
-[3]: #installation-algorithm
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section install Event so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html b/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html
deleted file mode 100644
index c305159..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: InstallEvent</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-event-section">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-interface InstallEvent : InstallPhaseEvent {
-  readonly attribute ServiceWorker? activeWorker;
-  void replace();
-};
-</pre>
-
-<!--
-Service Workers use the `[InstallEvent][1]` interface for `[install][2]` event.
-
-[1]: #install-event-interface
-[2]: #install-event
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface ServiceWorker {};
-        interface InstallPhaseEvent {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            InstallEvent: ["throw new Error ('No object defined for the InstallEvent interface')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html
deleted file mode 100644
index 78c916f..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.replace()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#replace-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`replace()` interacts with `waitUntil` method in the following way:
-
--   Successful installation can be delayed by `waitUntil`, perhaps by
-    subsequent event handlers.
--   Replacement only happens upon successful installation
--   Therefore, replacement of the [active worker][1] (if any) is not
-    immediate, however it may occur as soon as the end of the current turn.
-
-
-
-[1]: #navigator-service-worker-active
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section event.replace() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html
deleted file mode 100644
index 82c049a..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: activate Event</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#activate-event">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-An event named `[activate][1]` using the `[InstallPhaseEvent][2]` interface is
-dispatched on `ServiceWorkerGlobalScope` object when the state of the
-associated `ServiceWorker` changes its value to `activating`. (See step 6 of
-[_Activation algorithm][3])
-
-Service Workers use the `[InstallPhaseEvent][4]` interface for `[activate][1]`
-event.
-
-
-
-[1]: #activate-event
-[2]: #install-phase-event
-[3]: #activation-algorithm
-[4]: #install-phase-event-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section activate Event so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html
deleted file mode 100644
index 8555903..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html
+++ /dev/null
@@ -1,71 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: FetchEvent</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#fetch-event-section">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-        <script src=/resources/WebIDLParser.js></script>
-        <script src=/resources/idlharness.js></script>
-
-    </head>
-    <body>
-
-<script type=text/plain id="idl_0">
-[Constructor]
-interface FetchEvent : Event {
-  readonly attribute Request request;
-  readonly attribute Client client; // The window issuing the request.
-  readonly attribute Context context;
-  readonly attribute boolean isReload;
-
-  void respondWith(Promise<AbstractResponse> r);
-  Promise<any> forwardTo(ScalarValueString url);
-  Promise<any> default();
-};
-
-enum Context {
-  "connect",
-  "font",
-  "img",
-  "object",
-  "script",
-  "style",
-  "worker",
-  "popup",
-  "child",
-  "navigate"
-};
-</pre>
-
-<!--
-Service Workers use the `[FetchEvent][1]` interface for `[fetch][2]` event.
-
-[1]: #fetch-event-interface
-[2]: #fetch-event
--->
-
-
-    <script type=text/plain id="untested_idls">
-        interface Request {};
-        interface Client {};
-        interface AbstractResponse {};
-        interface ScalarValueString {};
-        interface Event {};
-    </pre>
-
-    <script>
-        var idl_array = new IdlArray();
-        idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
-        idl_array.add_idls(document.getElementById("idl_0").textContent);
-        idl_array.add_objects({
-            FetchEvent: ["throw new Error ('No object defined for the FetchEvent interface')"],
-            Context: ["throw new Error ('No object defined for the Context enum')"]
-        });
-        idl_array.test();
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html
deleted file mode 100644
index f178a50..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.respondWith(r)</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#respond-with-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`event.respondWith(r)` method must run the steps, from step 10 to step 15,
-defined in the [_OnFetchRequest algorithm][1].
-
-The `r` argument must resolve with a [AbstractResponse][2], else a
-[NetworkError][3] is thrown. If the request is a top-level navigation and the
-return value is a [OpaqueResponse][4] (an opaque response body), a
-[NetworkError][3] is thrown. The final URL of all successful (non
-network-error) responses is the [requested][5] URL. Renderer-side security
-checks about tainting for cross-origin content are tied to the transparency (or
-opacity) of the [Response][6] body, not URLs.
-
-
-
-[1]: #on-fetch-request-algorithm
-[2]: #abstract-response-interface
-[3]: http://w3c.github.io/dom/#networkerror
-[4]: #opaque-response-interface
-[5]: #request-objects
-[6]: #response-interface
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section event.respondWith(r) so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html
deleted file mode 100644
index 52a8dbd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.default()</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#default-method">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-`event.default()` method must run these steps:
-
-1.  Let `promise` be a newly-created [promise][1].
-2.  Return `promise.`
-3.  Run the following steps asynchronously:
-    1.  Let `request` be `event`'s `request`.
-    2.  Set `request`'s [skip service worker flag][2].
-    3.  Let `response` be the result of running [fetch algorithm][3] with
-        `request` as its argument.
-    4.  If `response` is a [network error][4], then:
-        1.  Reject `promise` with a new [DOMException][5] whose name is
-            "[NetworkError][6]".
-    5.  Else,
-        1.  Resolve `promise` with a new [Response][7] object associated
-            with `response`.
-
-
-
-[1]: http://goo.gl/3TobQS
-[2]: http://goo.gl/gP7IWW
-[3]: http://goo.gl/fGMifs
-[4]: http://goo.gl/jprjjc
-[5]: http://goo.gl/A0U8qC
-[6]: http://goo.gl/lud5HB
-[7]: http://goo.gl/Deazjv
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section event.default() so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html
deleted file mode 100644
index f116b68..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.isReload</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#is-reload-attribute">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-Returns true if `event` was dispatched with the user's intention for the page
-reload, and false otherwise. Pressing the refresh button should be considered a
-reload while clicking a link and pressing the back button should not. The
-behavior of the `Ctrl+l enter` is left to the implementations of the user
-agents.
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section event.isReload so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html b/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html
deleted file mode 100644
index e885de6..0000000
--- a/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Origin Relativity</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#origin-relativity">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-One of the advanced concerns that major applications would encounter is whether
-they can be hosted from a CDN. By definition, these are servers in other
-places, often on other domains. Therefore, Service Workers cannot be hosted on
-CDNs. But they can include resources via [importScripts()][1]. The reason for
-this restriction is that Service Workers create the opportunity for a bad actor
-to turn a bad day into a bad eternity.
-
-[1]: http://goo.gl/Owcfs2
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section Origin Relativity so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html b/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html
deleted file mode 100644
index 3a10c9e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Cross-Origin Resources &amp; CORS</title>
-    <head>
-        <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cross-origin-resources">
-        <script src="/resources/testharness.js"></script>
-        <script src="/resources/testharnessreport.js"></script>
-
-    </head>
-    <body>
-
-<!--
-
-Applications tend to cache items that come from a CDN or other domain. It is
-possible to request many of them directly using <script>, <img>, <video> and
-<link> elements. It would be hugely limiting if this sort of runtime
-collaboration broke when offline. Similarly, it is possible to XHR many sorts
-of off-domain resources when appropriate CORS headers are set.
-
-ServiceWorkers enable this by allowing `Cache`s to fetch and cache off-origin
-items. Some restrictions apply, however. First, unlike same-origin resources
-which are managed in the `Cache` as `[Promise][1]`s for `Response` instances,
-the objects stored are `[Promise][1]`s for `OpaqueResponse` instances.
-`OpaqueResponse` provides a much less expressive API than `Response`; the
-bodies and headers cannot be read or set, nor many of the other aspects of
-their content inspected. They can be passed to `respondWith()` and
-`forwardTo()` in the same manner as `Response`s, but cannot be meaningfully
-created programmatically. These limitations are necessary to preserve the
-security invariants of the platform. Allowing `Cache`s to store them allows
-applications to avoid re-architecting in most cases.
-
-
-
-[1]: http://goo.gl/3TobQS
-
--->
-
-
-
-    <script>
-        test(function() {
-            // not_implemented();
-        }, "There are no tests for section Cross-Origin Resources &amp; CORS so far.");
-    </script>
-
-    </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/tools/blink-import.py b/third_party/web_platform_tests/service-workers/tools/blink-import.py
deleted file mode 100644
index 84c958c..0000000
--- a/third_party/web_platform_tests/service-workers/tools/blink-import.py
+++ /dev/null
@@ -1,204 +0,0 @@
-import os
-import re
-import shutil
-import glob
-import tempfile
-import sys
-from collections import defaultdict
-
-here = os.path.abspath(os.path.split(__file__)[0])
-
-def get_extra_files(chromium_root):
-    return [(os.path.join(chromium_root, "LayoutTests", "http", "tests", "resources", "testharness-helpers.js"),
-             os.path.join("resources", "testharness-helpers.js"))]
-
-resources_re = re.compile("/?(?:\.\./)*resources/(testharness(?:report)?)\.js")
-
-def resources_path(line, depth):
-    return False, resources_re.sub(r"/resources/\1.js", line)
-
-php_re = re.compile("\.php")
-
-def python_to_php(line, depth):
-    return False, php_re.sub(".py", line)
-
-abs_testharness_helpers_re = re.compile("([\"'])/resources/testharness-helpers.js")
-testharness_helpers_re = re.compile("\.\./((?:\.\./)*)resources/testharness-helpers.js")
-
-def testharness_helpers(line, depth):
-    if abs_testharness_helpers_re.findall(line):
-        return False, abs_testharness_helpers_re.sub(r"\1%sresources/testharness-helpers.js" % ("../" * (depth - 1)), line)
-    return False, testharness_helpers_re.sub(r"\1resources/testharness-helpers.js", line)
-
-serviceworker_path_re = re.compile("/serviceworker/")
-def service_worker_path(line, depth):
-    return False, serviceworker_path_re.sub("/service-workers/", line)
-
-localhost_re = re.compile("localhost")
-alt_host_re = re.compile("127\.0\.0\.1")
-port_http_re = re.compile("8000")
-port_https_re = re.compile("8000")
-
-
-def server_names(line, depth):
-    line, count_0 = localhost_re.subn("{{host}}", line)
-    line, count_1 = alt_host_re.subn("{{domains[www]}}", line)
-    line, count_2 = port_http_re.subn("{{ports[http][0]}}", line)
-    line, count_3 = port_https_re.subn("{{ports[https][0]}}", line)
-
-    count = count_0 + count_1 + count_2 + count_3
-
-    return bool(count), line
-
-
-def source_paths(chromium_root):
-    for dirpath, dirnames, filenames in os.walk(chromium_root):
-        if "chromium" in dirnames:
-            dirnames.remove("chromium")
-        for filename in filenames:
-            if filename.endswith("-expected.txt") or filename.endswith(".php"):
-                continue
-            yield os.path.relpath(os.path.join(dirpath, filename), chromium_root)
-
-
-def do_subs(path, line):
-    depth = len(os.path.split(os.path.sep))
-    subs = [resources_path, python_to_php, testharness_helpers, service_worker_path, server_names]
-    file_is_template = False
-    for sub in subs:
-        added_template, line = sub(line, depth)
-        if added_template:
-            file_is_template = True
-    return file_is_template, line
-
-def get_head(git):
-    return git("rev-parse", "HEAD")
-
-def get_changes(git, path, old, new):
-    data = git("diff", "--name-status", "-z", "--no-renames", "%s..%s" % (old, new), "--", path)
-    items = data.split("\0")
-    rv = defaultdict(list)
-    for status, path in items:
-        rv[status].append(path)
-
-    return rv
-
-def copy(src_path, out_dir, rel_path):
-    dest = os.path.normpath(os.path.join(out_dir, rel_path))
-    dest_dir = os.path.split(dest)[0]
-    if not os.path.exists(dest_dir):
-        os.makedirs(dest_dir)
-    shutil.copy2(src_path, dest)
-
-def copy_local_files(local_files, out_root, tmp_dir):
-    for path in local_files:
-        rel_path = os.path.relpath(path, out_root)
-        copy(path, tmp_dir, rel_path)
-
-def copy_extra_files(chromium_root, tmp_dir):
-    for in_path, rel_path in get_extra_files(chromium_root):
-        copy(in_path, tmp_dir, rel_path)
-
-def sub_changed_filenames(filename_changes, f):
-    rv = []
-    for line in f:
-        for in_name, out_name in filename_changes.items():
-            line = line.replace(in_name, out_name)
-        rv.append(line)
-    return "".join(rv)
-
-testharness_re = re.compile("<script[^>]*src=[\"']?/resources/testharness.js[\"' ][^>]*>")
-
-def is_top_level_test(path, data):
-    if os.path.splitext(path)[1] != ".html":
-        return False
-    for line in data:
-        if testharness_re.findall(line):
-            return True
-    return False
-
-def add_suffix(path, suffix):
-    root, ext = os.path.splitext(path)
-    return root + ".%s" % suffix + ext
-
-def main():
-    if "--cache-tests" in sys.argv:
-        sw_path = os.path.join("LayoutTests", "http", "tests", "cachestorage")
-        out_root = os.path.abspath(os.path.join(here, "..", "cache-storage"))
-    elif "--sw-tests" in sys.argv:
-        sw_path = os.path.join("LayoutTests", "http", "tests", "serviceworkers")
-        out_root = os.path.abspath(os.path.join(here, "..", "service-worker"))
-    else:
-        raise ValueError("Must supply either --cache-tests or --sw-tests")
-
-    chromium_root = os.path.abspath(sys.argv[1])
-
-    work_path = tempfile.mkdtemp()
-
-    test_path = os.path.join(chromium_root, sw_path)
-
-    local_files = glob.glob(os.path.normpath(os.path.join(here, "..", "resources", "*.py")))
-
-    if not os.path.exists(out_root):
-        os.mkdir(out_root)
-
-    copy_local_files(local_files, out_root, work_path)
-    copy_extra_files(chromium_root, work_path)
-
-    path_changes = {}
-
-    for path in source_paths(test_path):
-        out_path = os.path.join(work_path, path)
-        out_dir = os.path.dirname(out_path)
-        if not os.path.exists(out_dir):
-            os.makedirs(out_dir)
-        with open(os.path.join(test_path, path)) as in_f:
-            data = []
-            sub = False
-            for line in in_f:
-                sub_flag, output_line = do_subs(path, line)
-                data.append(output_line)
-                if sub_flag:
-                    sub = True
-            is_test = is_top_level_test(out_path, data)
-
-        initial_path = out_path
-
-        if is_test:
-            path_1 = add_suffix(out_path, "https")
-        else:
-            path_1 = out_path
-
-        if sub:
-            path_2 = add_suffix(out_path, "sub")
-        else:
-            path_2 = path_1
-
-        if path_2 != initial_path:
-            path_changes[initial_path] = path_2
-
-        with open(path_2, "w") as out_f:
-            out_f.write("".join(data))
-
-    filename_changes = {}
-
-    for k, v in path_changes.items():
-        if os.path.basename(k) in filename_changes:
-            print "Got duplicate name:" + os.path.basename(k)
-        filename_changes[os.path.basename(k)] = os.path.basename(v)
-
-    for path in source_paths(work_path):
-        full_path = os.path.join(work_path, path)
-        with open(full_path) as f:
-            data = sub_changed_filenames(filename_changes, f)
-        with open(full_path, "w") as f:
-            f.write(data)
-
-    for dirpath, dirnames, filenames in os.walk(work_path):
-        for filename in filenames:
-            in_path = os.path.join(dirpath, filename)
-            rel_path = os.path.relpath(in_path, work_path)
-            copy(in_path, out_root, rel_path)
-
-if __name__ == "__main__":
-    main()
diff --git a/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py b/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
index 0da9e31..9463a15 100644
--- a/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
+++ b/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
@@ -1,4 +1,4 @@
-import cgi
+import html
 import json
 import os
 import traceback
@@ -72,7 +72,7 @@
 <ul>
 %(items)s
 </ul>
-""" % {"path": cgi.escape(url_path),
+""" % {"path": html.escape(url_path),
        "items": "\n".join(self.list_items(url_path, path))}  # flake8: noqa
 
     def list_items(self, base_path, path):
@@ -89,14 +89,14 @@
             yield ("""<li class="dir"><a href="%(link)s">%(name)s</a></li>""" %
                    {"link": link, "name": ".."})
         for item in sorted(os.listdir(path)):
-            link = cgi.escape(quote(item))
+            link = html.escape(quote(item))
             if os.path.isdir(os.path.join(path, item)):
                 link += "/"
                 class_ = "dir"
             else:
                 class_ = "file"
             yield ("""<li class="%(class)s"><a href="%(link)s">%(name)s</a></li>""" %
-                   {"link": link, "name": cgi.escape(item), "class": class_})
+                   {"link": link, "name": html.escape(item), "class": class_})
 
 
 def wrap_pipeline(path, request, response):
diff --git a/tools/create_archive.py b/tools/create_archive.py
index 6e55eab..0c69440 100755
--- a/tools/create_archive.py
+++ b/tools/create_archive.py
@@ -44,7 +44,6 @@
 # Use strict include filter to pass artifacts to Mobile Harness
 TEST_PATTERNS = [
     'content/*',
-    'deploy/*',
     'install/*',
     # TODO: All of those should be built under deploy/
     '*.apk',
@@ -70,15 +69,17 @@
   return sys.platform in ['win32']
 
 
+class InvalidArchiveExtensionException(RuntimeError):
+  pass
+
+
 def _CheckArchiveExtension(archive_path, is_parallel):
   if is_parallel and (_LINUX_PARALLEL_ZIP_EXTENSION not in archive_path):
-    raise RuntimeError(
-        'Invalid archive extension. Parallelized zip requested, but path is %s'
-        % archive_path)
+    raise InvalidArchiveExtensionException(
+        f'Parallelized zip requested, but path is {archive_path}')
   if not is_parallel and (_LINUX_PARALLEL_ZIP_EXTENSION in archive_path):
-    raise RuntimeError(
-        'Invalid archive extension. Serialized zip requested, but path is: %s' %
-        archive_path)
+    raise InvalidArchiveExtensionException(
+        f'Serialized zip requested, but path is: {archive_path}')
 
 
 def _CreateCompressedArchive(source_paths,
@@ -138,12 +139,11 @@
       logging.info('included subpaths: %s', included_paths)
 
       if not os.path.exists(source_path):
-        raise ValueError('Failed to find source path "{}".'.format(source_path))
+        raise ValueError(f'Failed to find source path "{source_path}".')
 
       for target_path in included_paths:
         if not os.path.exists(os.path.join(source_path, target_path)):
-          raise ValueError(
-              'Failed to find target path "{}".'.format(target_path))
+          raise ValueError(f'Failed to find target path "{target_path}".')
 
       source_parent_dir, source_dir_name = os.path.split(
           os.path.abspath(source_path))
@@ -253,7 +253,7 @@
   if intermediate:
     exclude_patterns = worker_tools.BUILDBOT_INTERMEDIATE_FILE_NAMES_WILDCARDS
   excludes = ' '.join([
-      '-xr!{}'.format(x.replace(worker_tools.SOURCE_DIR, source_path))
+      f'-xr!{x.replace(worker_tools.SOURCE_DIR, source_path)}'
       for x in exclude_patterns
   ])
   contents = [
@@ -261,9 +261,9 @@
       for pattern in patterns
       if glob.glob(os.path.join(source_path, pattern))
   ]
-  return '"{}" a {} -bsp1 -snl -ttar {} {}'.format(_7Z_PATH, excludes,
-                                                   intermediate_tar_path,
-                                                   ' '.join(contents))
+  files_to_tar = ' '.join(contents)
+  return (f'"{_7Z_PATH}" a {excludes} -bsp1 -snl -ttar'
+          f'{intermediate_tar_path} {files_to_tar}')
 
 
 def _CreateLinuxTarCmd(source_path, intermediate_tar_path, patterns,
@@ -272,7 +272,7 @@
   if intermediate:
     exclude_patterns = worker_tools.BUILDBOT_INTERMEDIATE_FILE_NAMES_WILDCARDS
   excludes = ' '.join([
-      '--exclude="{}"'.format(x.replace(worker_tools.SOURCE_DIR, source_path))
+      f'--exclude="{x.replace(worker_tools.SOURCE_DIR, source_path)}"'
       for x in exclude_patterns
   ])
   mode = 'r' if os.path.exists(intermediate_tar_path) else 'c'
@@ -281,42 +281,38 @@
       for pattern in patterns
       if glob.glob(os.path.join(source_path, pattern))
   ]
-  return 'tar -{}vf {} --format=posix {} {}'.format(mode, intermediate_tar_path,
-                                                    excludes,
-                                                    ' '.join(contents))
+  files_to_tar = ' '.join(contents)
+  return (f'tar -{mode}vf {intermediate_tar_path} --format=posix'
+          f'{excludes} {files_to_tar}')
 
 
 def _CreateUntarCommand(intermediate_tar_path):
   if _IsWindows():
-    return '"{}" x -bsp1 {}'.format(_7Z_PATH, intermediate_tar_path)
+    return f'"{_7Z_PATH}" x -bsp1 {intermediate_tar_path}'
   else:
-    return 'tar -xvf {}'.format(intermediate_tar_path)
+    return f'tar -xvf {intermediate_tar_path}'
 
 
 def _CreateZipCommand(intermediate_tar_path, dest_path, is_parallel=False):
   if _IsWindows():
-    return '"{}" a -bsp1 -mx={} -mmt=on {} {}'.format(_7Z_PATH,
-                                                      _COMPRESSION_LEVEL,
-                                                      dest_path,
-                                                      intermediate_tar_path)
+    return (f'"{_7Z_PATH}" a -bsp1 -mx={_COMPRESSION_LEVEL}'
+            f'-mmt=on {dest_path} {intermediate_tar_path}')
   else:
     zip_program = (
         _LINUX_PARALLEL_ZIP_PROGRAM
         if is_parallel else _LINUX_SERIAL_ZIP_PROGRAM)
-    return '{} -vc -1 {} > {}'.format(zip_program, intermediate_tar_path,
-                                      dest_path)
+    return f'{zip_program} -vc -1 {intermediate_tar_path} > {dest_path}'
 
 
 def _CreateUnzipCommand(source_path, intermediate_tar_path, is_parallel=False):
   if _IsWindows():
     tar_parent = os.path.dirname(intermediate_tar_path)
-    return '"{}" x -bsp1 {} -o{}'.format(_7Z_PATH, source_path, tar_parent)
+    return f'"{_7Z_PATH}" x -bsp1 {source_path} -o{tar_parent}'
   else:
     zip_program = (
         _LINUX_PARALLEL_ZIP_PROGRAM
         if is_parallel else _LINUX_SERIAL_ZIP_PROGRAM)
-    return '{} -dvc {} > {}'.format(zip_program, source_path,
-                                    intermediate_tar_path)
+    return f'{zip_program} -dvc {source_path} > {intermediate_tar_path}'
 
 
 def _CleanUp(intermediate_tar_path):
diff --git a/tools/on_device_tests_gateway.proto b/tools/on_device_tests_gateway.proto
index ea0b465..16211fa 100644
--- a/tools/on_device_tests_gateway.proto
+++ b/tools/on_device_tests_gateway.proto
@@ -40,7 +40,7 @@
   string archive_path = 5;
   string config = 6;
   string tag = 7;
-  string label = 8;
+  repeated string labels = 8;
   string builder_name = 9;
   string change_id = 10;
   string build_number = 11;
@@ -49,6 +49,7 @@
   string version = 14;
   optional bool dry_run = 15;
   repeated string dimension = 16;
+  string unittest_shard_index = 17;
 }
 
 // Working directory and command line arguments to be passed to the gateway.
@@ -56,7 +57,8 @@
   string workdir = 1;
   string token = 2;
   string session_id = 3;
-  optional bool dry_run = 15;
+  string change_id = 4;
+  optional bool dry_run = 5;
 }
 
 // Response from the on-device tests.
diff --git a/tools/on_device_tests_gateway_client.py b/tools/on_device_tests_gateway_client.py
index 50848ae..38d9e10 100644
--- a/tools/on_device_tests_gateway_client.py
+++ b/tools/on_device_tests_gateway_client.py
@@ -76,7 +76,7 @@
             archive_path=args.archive_path,
             config=args.config,
             tag=args.tag,
-            label=args.label,
+            labels=args.label,
             builder_name=args.builder_name,
             change_id=args.change_id,
             build_number=args.build_number,
@@ -85,6 +85,7 @@
             version=args.version,
             dry_run=args.dry_run,
             dimension=args.dimension or [],
+            unittest_shard_index=args.unittest_shard_index,
         )):
 
       print(response_line.response)
@@ -100,6 +101,7 @@
         on_device_tests_gateway_pb2.OnDeviceTestsWatchCommand(
             workdir=workdir,
             token=args.token,
+            change_id=args.change_id,
             session_id=args.session_id,
         )):
 
@@ -128,6 +130,12 @@
       '--dry_run',
       action='store_true',
       help='Specifies to show what would be done without actually doing it.')
+  parser.add_argument(
+      '-i',
+      '--change_id',
+      type=str,
+      help='ChangeId that triggered this test, if any. '
+      'Saved with performance test results.')
 
   subparsers = parser.add_subparsers(
       dest='action', help='On-Device tests commands', required=True)
@@ -170,6 +178,8 @@
       '-l',
       '--label',
       type=str,
+      default=[],
+      action='append',
       help='Additional labels to assign to the test.')
   trigger_parser.add_argument(
       '-b',
@@ -178,12 +188,6 @@
       help='Name of the builder that built the artifacts, '
       'if any. Saved with performance test results')
   trigger_parser.add_argument(
-      '-i',
-      '--change_id',
-      type=str,
-      help='ChangeId that triggered this test, if any. '
-      'Saved with performance test results.')
-  trigger_parser.add_argument(
       '-n',
       '--build_number',
       type=str,
@@ -211,6 +215,13 @@
       type=str,
       default='COBALT',
       help='Cobalt version being tested.')
+  trigger_parser.add_argument(
+      '--unittest_shard_index',
+      type=str,
+      required=False,
+      help='Optional argument to specify which unit testing shard to run in '
+      'the On-Device Tests Job. Defaults behavior is to run all tests without '
+      'sharding enabled.')
 
   watch_parser = subparsers.add_parser('watch', help='Trigger On-Device tests')
   watch_parser.add_argument(