The Serial API provides a way for websites to read and write from a serial device through script. Such an API would bridge the web and the physical world, by allowing documents to communicate with devices such as microcontrollers, 3D printers, and other serial devices. There is also a companion explainer document.
This is a work in progress. All contributions welcome.

Extensions to the {{Navigator}} interface

    [Exposed=Window, SecureContext]
    partial interface Navigator {
      [SameObject] readonly attribute Serial serial;
    };
  

serial attribute

When getting, the {{Navigator/serial}} attribute always returns the same instance of the {{Serial}} object.

Extensions to the {{WorkerNavigator}} interface

    [Exposed=DedicatedWorker, SecureContext]
    partial interface WorkerNavigator {
      [SameObject] readonly attribute Serial serial;
    };
  

serial attribute

When getting, the {{WorkerNavigator/serial}} attribute always returns the same instance of the {{Serial}} object.

{{Serial}} interface

    [Exposed=(DedicatedWorker, Window), SecureContext]
    interface Serial : EventTarget {
      attribute EventHandler onconnect;
      attribute EventHandler ondisconnect;
      Promise<sequence<SerialPort>> getPorts();
      [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
    };
  

requestPort() method

The {{Serial/requestPort()}} method steps are:
  1. Let |promise:Promise| be [=a new promise=].
  2. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=allowed to use=] the [=policy-controlled feature=] named `"serial"`, [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|.
  3. If the [=relevant global object=] of [=this=] does not have [=transient activation=], [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|.
  4. If |options|["{{SerialPortRequestOptions/filters}}"] is present, then for each |filter:SerialPortFilter| in |options|["{{SerialPortRequestOptions/filters}}"] run the following steps:
    1. If |filter|["{{SerialPortFilter/usbVendorId}}"] is not present, [=reject=] |promise| with a {{TypeError}} and return |promise|.
      This check implements the combined rule that a {{SerialPortFilter}} cannot be empty and if {{SerialPortFilter/usbProductId}} is specified then {{SerialPortFilter/usbVendorId}} must also be specified.
  5. Run the following steps [=in parallel=]:
    1. Prompt the user to grant the site access to a serial port by presenting them with a list of available ports that [=match any filter=] in |options|["{{SerialPortRequestOptions/filters}}"] if present and all available ports otherwise.
    2. If the user does not choose a port, [=queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=reject=] |promise| with an {{"AbortError"}} {{DOMException}} and abort these steps.
    3. Let |port:SerialPort| be a {{SerialPort}} representing the port chosen by the user.
    4. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with |port|.
  6. Return |promise|.

SerialPortRequestOptions dictionary

        dictionary SerialPortRequestOptions {
          sequence<SerialPortFilter> filters;
        };
      
filters member
Filters for serial ports

SerialPortFilter dictionary

        dictionary SerialPortFilter {
          unsigned short usbVendorId;
          unsigned short usbProductId;
        };
      
usbVendorId member
USB Vendor ID
usbProductId member
USB Product ID

A serial port matches the filter |filter:SerialPortFilter| if these steps return `true`:

  1. If |filter|["{{SerialPortFilter/usbVendorId}}"] is not present, return `true`.
  2. If the serial port is not part of a USB device, return `false`.
  3. If the USB device's vendor ID is not equal to |filter|["{{SerialPortFilter/usbVendorId}}"], return `false`.
  4. If |filter|["{{SerialPortFilter/usbProductId}}"] is not present, return `true`.
  5. If the USB device's product ID is not equal to |filter|["{{SerialPortFilter/usbProductId}}"], return `false`.
  6. Otherwise, return `true`.

A serial port matches any filter in a sequence of {{SerialPortFilter}} if these steps return `true`:

  1. For each |filter| in the sequence, run these sub-steps:
    1. If the serial port does not [=match the filter=] |filter|, return false.
  2. Return true.

getPorts() method

The {{Serial/getPorts()}} method steps are:
  1. Let |promise:Promise| be [=a new promise=].
  2. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=allowed to use=] the [=policy-controlled feature=] named `"serial"`, [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|.
  3. Run the following steps [=in parallel=]:
    1. Let |availablePorts| be the sequence of available serial ports on the system which the user has allowed the site to access as the result of a previous call to {{Serial/requestPort()}}.
    2. Let |ports| be the sequence of the {{SerialPort}}s representing the ports in |availablePorts|.
    3. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with |ports|.
  4. Return |promise|.

onconnect attribute

{{Serial/onconnect}} is an [=event handler IDL attribute=] for the connect event type.

ondisconnect attribute

{{Serial/ondisconnect}} is an [=event handler IDL attribute=] for the disconnect event type.

SerialPort interface

    [Exposed=(DedicatedWorker,Window), SecureContext]
    interface SerialPort : EventTarget {
      attribute EventHandler onconnect;
      attribute EventHandler ondisconnect;
      readonly attribute ReadableStream readable;
      readonly attribute WritableStream writable;

      SerialPortInfo getInfo();

      Promise<undefined> open(SerialOptions options);
      Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
      Promise<SerialInputSignals> getSignals();
      Promise<undefined> close();
    };
  
Methods on this interface typically complete asynchronously, queuing work on the serial port task source.

The [=get the parent=] algorithm for {{SerialPort}} returns the same {{Serial}} instance that is returned by the {{SerialPort}}'s [=relevant global object=]'s {{Navigator}} object's {{Navigator/serial}} getter.

Instances of {{SerialPort}} are created with the internal slots described in the following table:

Internal slot Initial value Description (non-normative)
[[\state]] `"closed"` Tracks the active state of the {{SerialPort}}
[[\bufferSize]] undefined The amount of data to buffer for transmit and receive
[[\readable]] `null` A {{ReadableStream}} that receives data from the port
[[\readFatal]] `false` A flag indicating that the port has encountered a fatal read error
[[\writable]] `null` A {{WritableStream}} that transmits data to the port
[[\writeFatal]] `false` A flag indicating that the port has encountered a fatal write error
[[\pendingClosePromise]] `null` A {{Promise}} used to wait for {{SerialPort/readable}} and {{SerialPort/writable}} to close

onconnect attribute

{{SerialPort/onconnect}} is an [=event handler IDL attribute=] for the {{connect}} event type.

When a serial port becomes available on the system that the user has allowed the site to access as the result of a previous call to {{Serial/requestPort()}}, run the following steps:

  1. Let |port:SerialPort| be a {{SerialPort}} representing the port.
  2. [=Fire an event=] named {{connect}} at |port| with its {{Event/bubbles}} attribute initialized to `true`.

ondisconnect attribute

{{SerialPort/ondisconnect}} is an [=event handler IDL attribute=] for the {{disconnect}} event type.

When a serial port becomes unavailable on the system that the user has allowed the site to access as the result of a previous call to {{Serial/requestPort()}}, run the following steps:

  1. Let |port:SerialPort| be a {{SerialPort}} representing the port.
  2. [=Fire an event=] named {{disconnect}} at |port| with its {{Event/bubbles}} attribute initialized to `true`.

getInfo() method

The {{SerialPort/getInfo()}} method steps are:
  1. Let |info:SerialPortInfo| be a [=new=] {{SerialPortInfo}} dictionary.
  2. If the port is part of a USB device, perform the following steps:
    1. Set |info|["{{SerialPortInfo/usbVendorId}}"] to the vendor ID of the device.
    2. Set |info|["{{SerialPortInfo/usbProductId}}"] to the product ID of the device.
  3. Return |info|.

SerialPortInfo dictionary

      dictionary SerialPortInfo {
        unsigned short usbVendorId;
        unsigned short usbProductId;
      };
    
usbVendorId member
If the port is part of a USB device this member will be the 16-bit vendor ID of that device. Otherwise it will be `undefined`.
usbProductId member
If the port is part of a USB device this member will be the 16-bit product ID of that device. Otherwise it will be `undefined`.

open() method

The {{SerialPort/open()}} method steps are:
  1. Let |promise| be [=a new promise=].
  2. If [=this=].{{[[state]]}} is not `"closed"`, reject |promise| with an "{{InvalidStateError}}" {{DOMException}} and return |promise|.
  3. If |options|["{{SerialOptions/dataBits}}"] is not 7 or 8, reject |promise| with {{TypeError}} and return |promise|.
  4. If |options|["{{SerialOptions/stopBits}}"] is not 1 or 2, reject |promise| with {{TypeError}} and return |promise|.
  5. If |options|["{{SerialOptions/bufferSize}}"] is 0, reject |promise| with {{TypeError}} and return |promise|.
  6. Optionally, if |options|["{{SerialOptions/bufferSize}}"] is larger than the implementation is able to support, reject |promise| with a {{TypeError}} and return |promise|.
  7. Set [=this=].{{[[state]]}} to `"opening"`.
  8. Perform the following steps [=in parallel=].
    1. Invoke the operating system to open the serial port using the connection parameters (or their defaults) specified in |options|.
    2. If this fails for any reason, [=queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}} and abort these steps.
    3. Set [=this=].{{[[state]]}} to `"opened"`.
    4. Set [=this=].{{[[bufferSize]]}} to |options|["{{SerialOptions/bufferSize}}"].
    5. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with `undefined`.
  9. Return |promise|.

SerialOptions dictionary

        dictionary SerialOptions {
          required [EnforceRange] unsigned long baudRate;
          [EnforceRange] octet dataBits = 8;
          [EnforceRange] octet stopBits = 1;
          ParityType parity = "none";
          [EnforceRange] unsigned long bufferSize = 255;
          FlowControlType flowControl = "none";
        };
      
baudRate member
A positive, non-zero value indicating the baud rate at which serial communication should be established.
{{SerialOptions/baudRate}} is the only required member of this dictionary. While there are common default for other connection parameters it is important for developers to consider and consult with the documentation for devices they intend to connect to determine the correct values. While some values are common there is no standard baud rate. Requiring this parameter reduces the potential for confusion if an arbitrary default were chosen by this specification.
dataBits member
The number of data bits per frame. Either 7 or 8.
stopBits member
The number of stop bits at the end of a frame. Either 1 or 2.
parity member
The parity mode.
bufferSize member
A positive, non-zero value indicating the size of the read and write buffers that should be created.
flowControl member
The flow control mode.
ParityType enum
          enum ParityType {
            "none",
            "even",
            "odd"
          };
        
none
No parity bit is sent for each data word.
even
Data word plus parity bit has even parity.
odd
Data word plus parity bit has odd parity.
FlowControlType enum
          enum FlowControlType {
            "none",
            "hardware"
          };
        
none
No flow control is enabled.
hardware
Hardware flow control using the RTS and CTS signals is enabled.

readable attribute

The {{SerialPort/readable}} getter steps are:
  1. If [=this=].{{[[readable]]}} is not `null`, return [=this=].{{[[readable]]}}.
  2. If [=this=].{{[[state]]}} is not `"opened"`, return `null`.
  3. If [=this=].{{[[readFatal]]}} is `true`, return `null`.
  4. Let |stream| be a [=new=] {{ReadableStream}}.
  5. Let |pullAlgorithm| be the following steps:
    1. Let |desiredSize| be the desired size of [=this=].{{[[readable]]}}'s internal queue.
    2. Run the following steps [=in parallel=]:
      1. Invoke the operating system to read up to |desiredSize| bytes from the port, putting the result in the [=byte sequence=] |bytes|.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to run the following steps:
        1. If no errors were encountered run the following steps:
          1. Let |buffer| be a [=new=] {{ArrayBuffer}} created from |bytes|.
          2. Let |chunk| be a [=new=] {{Uint8Array}} view over |buffer|, who's length is the length of |bytes|.
          3. Invoke [=ReadableStream/enqueue=] on [=this=].{{[[readable]]}} with |chunk|.
        2. If a buffer overrun condition was encountered, invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with a "BufferOverrunError" {{DOMException}} and invoke the steps to [=handle closing the readable stream=].
        3. If a break condition was encountered, invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with a "BreakError" {{DOMException}} and invoke the steps to [=handle closing the readable stream=].
        4. If a framing error was encountered, invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with a "FramingError" {{DOMException}} and invoke the steps to [=handle closing the readable stream=].
        5. If a parity error was encountered, invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with a "ParityError" {{DOMException}} and invoke the steps to [=handle closing the readable stream=].
        6. If an operating system error was encountered, invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with an "{{UnknownError}}" {{DOMException}} and invoke the steps to [=handle closing the readable stream=].
        7. If the port was disconnected, run the following steps:
          1. Set [=this=].{{[[readFatal]]}} to `true`,
          2. Invoke [=ReadableStream/error=] on [=this=].{{[[readable]]}} with a "{{NetworkError}}" {{DOMException}}.
          3. Invoke the steps to [=handle closing the readable stream=].
    3. Return [=a promise resolved with=] `undefined`.

    The {{Promise}} returned by this algorithm is immediately resolved so that it does not block canceling the stream. [[STREAMS]] specifies that this algorithm will not be invoked again until a chunk is enqueued.

  6. Let |cancelAlgorithm| be the following steps:
    1. Let |promise| be [=a new promise=].
    2. Run the following steps [=in parallel=].
      1. Invoke the operating system to discard the contents of all software and hardware receive buffers for the port.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to run the following steps:
        1. Invoke the steps to [=handle closing the readable stream=].
        2. [=Resolve=] |promise| with `undefined`.
    3. Return |promise|.
  7. [=ReadableStream/Set up=] |stream| with pullAlgorithm set to |pullAlgorithm|, cancelAlgorithm set to |cancelAlgorithm|, highWaterMark set to [=this=].{{[[bufferSize]]}}, and sizeAlgorithm set to a byte-counting size algorithm.
  8. Set [=this=].{{[[readable]]}} to |stream|.
  9. Return |stream|.
To handle closing the readable stream perform the following steps:
  1. Set [=this=].{{[[readable]]}} to `null`.
  2. If [=this=].{{[[writable]]}} is `null` and [=this=].{{[[pendingClosePromise]]}} is not `null`, [=resolve=] [=this=].{{[[pendingClosePromise]]}} with `undefined`.

writable attribute

The {{SerialPort/writable}} getter steps are:
  1. If [=this=].{{[[writable]]}} is not `null`, return [=this=].{{[[writable]]}}.
  2. If [=this=].{{[[state]]}} is not `"opened"`, return `null`.
  3. If [=this=].{{[[writeFatal]]}} is `true`, return `null`.
  4. Let |stream:WritableStream| be a [=new=] {{WritableStream}}.
  5. Let |writeAlgorithm| be the following steps, given |chunk|:
    1. Let |promise:Promise| be [=a new promise=].
    2. If |chunk| cannot be [=converted to an IDL value=] of type {{BufferSource}}, reject |promise| with a {{TypeError}} and return |promise|. Otherwise, save the result of the conversion in |source:BufferSource|.
    3. [=Get a copy of the buffer source=] |source| and save the result in |bytes|.
    4. [=In parallel=], run the following steps:
      1. Invoke the operating system to write |bytes| to the port. Alternately, store the chunk for future coalescing.
        The operating system may return from this operation once |bytes| has been queued for transmission rather than after it has been transmitted.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to run the following steps:
        1. If the chunk was successfully written, or was stored for future coalescing, [=resolve=] |promise| with `undefined`.
          [[STREAMS]] specifies that |writeAlgorithm| will only be invoked after the {{Promise}} returned by a previous invocation of this algorithm has resolved. For efficiency an implementation is allowed to resolve this {{Promise}} early in order to coalesce multiple chunks waiting in the {{WritableStream}}'s internal queue into a single request to the operating system.
        2. If an operating system error was encountered, [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}.
        3. If the port was disconnected, run the following steps:
          1. Set [=this=].{{[[writeFatal]]}} to `true`.
          2. [=Reject=] |promise| with a "{{NetworkError}}" {{DOMException}}.
          3. Invoke the steps to [=handle closing the writable stream=].
    5. Return |promise|.
  6. Let |abortAlgorithm| be the following steps:
    1. Let |promise| be [=a new promise=].
    2. Run the following steps [=in parallel=].
      1. Invoke the operating system to discard the contents of all software and hardware transmit buffers for the port.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to run the following steps:
        1. Invoke the steps to [=handle closing the writable stream=].
        2. [=Resolve=] |promise| with `undefined`.
    3. Return |promise|.
    [[STREAMS]] specifies that |abortAlgorithm| will only be invoked after the {{Promise}} returned by a previous invocation of |writeAlgorithm| (if any) has resolved. This blocks abort on completion of the most recent write operation. This could be fixed by passing an {{AbortSignal}} to |writeAlgorithm|.

    This enhancement is tracked in whatwg/streams#1015.

  7. Let |closeAlgorithm| be the following steps:
    1. Let |promise| be [=a new promise=].
    2. Run the following steps [=in parallel=].
      1. Invoke the operating system to flush the contents of all software and hardware transmit buffers for the port.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to run the following steps:
        1. Invoke the steps to [=handle closing the writable stream=].
        2. [=Resolve=] |promise| with `undefined`.
    3. Return |promise|.
  8. [=WritableStream/Set up=] |stream| with writeAlgorithm set to |writeAlgorithm|, abortAlgorithm set to |abortAlgorithm|, closeAlgorithm set to |closeAlgorithm|, highWaterMark set to [=this=].{{[[bufferSize]]}}, and sizeAlgorithm set to a byte-counting size algorithm.
  9. Set [=this=].{{[[writable]]}} to |stream|.
  10. Return |stream|.
To handle closing the writable stream perform the following steps:
  1. Set [=this=].{{[[writable]]}} to `null`.
  2. If [=this=].{{[[readable]]}} is `null` and [=this=].{{[[pendingClosePromise]]}} is not `null`, [=resolve=] [=this=].{{[[pendingClosePromise]]}} with `undefined`.

setSignals() method

The {{SerialPort/setSignals()}} method steps are:
  1. Let |promise| be [=a new promise=].
  2. If [=this=].{{[[state]]}} is not `"opened"`, reject |promise| with an "{{InvalidStateError}}" {{DOMException}} and return |promise|.
  3. If all of the specified members of |signals| are not present reject |promise| with {{TypeError}} and return |promise|.
  4. Perform the following steps [=in parallel=]:
    1. If |signals|["{{SerialOutputSignals/dataTerminalReady}}"] is present, invoke the operating system to either assert (if `true`) or deassert (if `false`) the "data terminal ready" or "DTR" signal on the serial port.
    2. If |signals|["{{SerialOutputSignals/requestToSend}}"] is present, invoke the operating system to either assert (if `true`) or deassert (if `false`) the "request to send" or "RTS" signal on the serial port.
    3. If |signals|["{{SerialOutputSignals/break}}"] is present, invoke the operating system to either assert (if `true`) or deassert (if `false`) the "break" signal on the serial port.
      The "break" signal is typically implemented as an in-band signal by holding the transmit line at the "mark" voltage and thus prevents data transmission for as long as it remains asserted.
    4. If the operating system fails to change the state of any of these signals for any reason, [=queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to reject |promise| with a "{{NetworkError}}" {{DOMException}}.
    5. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with `undefined`.
  5. Return |promise|.

SerialOutputSignals dictionary

        dictionary SerialOutputSignals {
          boolean dataTerminalReady;
          boolean requestToSend;
          boolean break;
        };
      
dataTerminalReady
Data Terminal Ready (DTR)
requestToSend
Request To Send (RTS)
break
Break

getSignals() method

The {{SerialPort/getSignals()}} method steps are:
  1. Let |promise:Promise| be [=a new promise=].
  2. If [=this=].{{[[state]]}} is not `"opened"`, reject |promise| with an "{{InvalidStateError}}" {{DOMException}} and return |promise|.
  3. Perform the following steps [=in parallel=]:
    1. Query the operating system for the status of the control signals that may be asserted by the device connected to the serial port.
    2. If the operating system fails to determine the status of these signals for any reason, [=queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to reject |promise| with a "{{NetworkError}}" {{DOMException}} and abort these steps.
    3. Let |signals:SerialInputSignals| be a [=new=] {{SerialInputSignals}}.
    4. Set |signals|["{{SerialInputSignals/dataCarrierDetect}}"] to `true` if the "data carrier detect" or "DCD" signal has been asserted by the device, and `false` otherwise.
    5. Set |signals|["{{SerialInputSignals/clearToSend}}"] to `true` if the "clear to send" or "CTS" signal has been asserted by the device, and `false` otherwise.
    6. Set |signals|["{{SerialInputSignals/dataCarrierDetect}}"] to `true` if the "ring indicator" or "RI" signal has been asserted by the device, and `false` otherwise.
    7. Set |signals|["{{SerialInputSignals/dataCarrierDetect}}"] to `true` if the "data set ready" or "DSR" signal has been asserted by the device, and `false` otherwise.
    8. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with |signals|.
  4. Return |promise|.

SerialInputSignals dictionary

      dictionary SerialInputSignals {
        required boolean dataCarrierDetect;
        required boolean clearToSend;
        required boolean ringIndicator;
        required boolean dataSetReady;
      };
    
dataCarrierDetect member
Data Carrier Detect (DCD)
clearToSend member
Clear To Send (CTS)
ringIndicator member
Ring Indicator (RI)
dataSetReady member
Data Set Ready (DSR)

close() method

The {{SerialPort/close()}} method steps are:
  1. Let |promise| be [=a new promise=].
  2. Let |cancelPromise:Promise| be the result of invoking [=ReadableStream/cancel=] on [=this=].{{[[readable]]}} or [=a promise resolved with=] `undefined` if [=this=].{{[[readable]]}} is `null`.
  3. Let |abortPromise:Promise| be the result of invoking [=WritableStream/abort=] on [=this=].{{[[writable]]}} or [=a promise resolved with=] `undefined` if [=this=].{{[[writable]]}} is `null`.
  4. Let |pendingClosePromise| be [=a new promise=].
  5. If [=this=].{{[[readable]]}} and [=this=].{{[[writable]]}} are `null`, [=resolve=] |pendingClosePromise| with `undefined`.
  6. Set [=this=].{{[[pendingClosePromise]]}} to |pendingClosePromise|.
  7. Let |combinedPromise:Promise| be the result of [=getting a promise to wait for all=] with «|cancelPromise|, |abortPromise|, |pendingClosePromise|».
  8. Set [=this=].{{[[state]]}} to `"closing"`.
  9. [=promise/React=] to |combinedPromise|.
    • If |combinedPromise| was fulfilled, then:
      1. Run the following steps [=in parallel=]:
        1. Invoke the operating system to close the serial port and release any associated resources.
        2. Set [=this=].{{[[state]]}} to `"closed"`.
        3. Set [=this=].{{[[readFatal]]}} and [=this=].{{[[writeFatal]]}} to `false`.
        4. Set [=this=].{{[[pendingClosePromise]]}} to `null`.
        5. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=resolve=] |promise| with `undefined`.
    • If |combinedPromise| was rejected with reason |r|, then:
      1. Set [=this=].{{[[pendingClosePromise]]}} to `null`.
      2. [=Queue a global task=] on the [=relevant global object=] of [=this=] using the [=serial port task source=] to [=reject=] |promise| with |r|.
  10. Return |promise|.

Security considerations

This API poses similar a security risk to [[?WEB-BLUETOOTH]] and [[?WEBUSB]] and so lessons from those are applicable here. The primary threats are: The primary mitigation to all of these attacks is the {{Serial/requestPort()}} pattern, which requires user interaction and only supports granting access to a single device at a time. This prevents drive-by attacks because a site cannot enumerate all connected devices to determine whether a vulnerable device exists and must instead proactively inform the user that it desires access. Implementations may also provide a visual indication that a site is currently communicating with a device and controls for revoking that permission at any time.

This specification requires the site to be served from a [=secure context=] in order to prevent malicious code from being injected by a network-based attacker. This ensures that the site identity shown to the user when making permission decisions is accurate. This specification also requires top-level documents to opt-in through [[?PERMISSIONS-POLICY]] before allowing a cross-origin iframe to use the API. When combined with [[?CSP3]] these mechanisms provide protection against malicious code injection attacks.

The remaining concern is the exploitation of a connected device through a phishing attack that convinces the user to grant a malicious site access to a device. These attacks can be used to either exploit the device’s capabilities as designed or to install malicious firmware on the device that will in turn attack the host computer. Host software may be vulnerable to attack because it improperly validates input from connected devices. Security research in this area has encouraged software vendors to treat connected devices as untrustworthy.

There is no mechanism that will completely prevent this type of attack because data sent from a page to the device is an opaque sequence of bytes. Efforts to block a particular type of data from being sent are likely be met by workarounds on the part of device manufacturers who nevertheless want to send this type of data to their devices.

User agents can implement additional mechanisms to control access to devices:

Implementations of [[?WEB-BLUETOOTH]] and [[?WEBUSB]] have experimented with these mitigations however there are limits to their effectiveness. First, it is difficult to define whether a device is exploitable. For example, this API will allow a site to upload firmware to a microcontroller development board. This is a key use case for this API as these devices are common in the educational and hobbyist markets. These boards do not implement firmware signature verification and so can easily be turned into a malicious device. These boards are clearly exploitable but should not be blocked.

In addition, maintaining a list of vulnerable devices works well for USB and Bluetooth because those protocols define out-of-band mechanisms to gather device metadata. The make and model of such devices can thus be easily identified even if they present themselves to the host as a virtual serial ports. However, there are generic USB- or Bluetooth-to-serial adapters as well as systems with "real" serial ports using a DB-25, DE-9 or RJ-45 connector. For these there is no metadata that can be read to determine the identity of the device connected to the port and so blocking access to these is not possible.

Privacy considerations

Serial ports and serial devices contain two kinds of sensitive information. When a port is a USB or Bluetooth device there are identifiers such as the vendor and product IDs (which identify the make and model) as well as a serial number or MAC address. The serial device itself may also have its own identifier that is available through commands sent via the serial port. The device may also store other private information which may or may not be identifying.

In order to manage device permissions an implementation will likely store device identifiers such as the USB vendor ID, product ID and serial number in its user preferences file to be used as stable identifiers for devices the user has granted sites access to. These would not be shared directly with sites and would be cleared when permission is revoked or site data in general is cleared.

Commands a page can send to the device after it has been granted access a page may also be able to access any of the other sensitive information stored by the device. For the reasons mentioned in [[[#security]]] it is impractical and undesirable to attempt to prevent a page from accessing this information.

Implementations should provide users with complete control over which devices a site can access and not grant device access without user interaction. This is the intention of the {{Serial/requestPort()}} method. This prevents a site from silently enumerating and collecting data from all connected devices. This is similar to the file picker UI. A site has no knowledge of the filesystem, only the files or directories that have been chosen by the user. An implementation could notify the user when a site is using these permissions with an indicator icon appearing in the tab or address bar.

Implementations that provide a "private" or "incognito" browsing mode should ensure that permissions from the user's normal profile do not carry over to such a session and permissions granted in this session are not persisted when the session ends. An implementation may warn the user when granting access to a device in such as session as, similar to entering identifying information by hand, device identifiers and other unique properties available from communicating with the device mentioned previously can be used to identify the user between sessions.

Users may be surprised by the capabilities granted by this API if they do not understand the ways in which granting access to a device breaks traditional isolation boundaries in the web security model. Security UI and documentation should explain that granting a site access to a device could give the site full control over the device and any data contained within.

Acknowledgements

The following people contributed to the development of this document.