This document describes an API to facility cross origin communication between service workers and webpages.

Introduction

With the navigator.connect API clients (either webpages or some flavor of worker) can connect to services, with these services being implemented by service workers.

Example

// http://example.com/webapp.js
navigator.connect('https://example.com/services/echo').then(
  function(port) {
    port.postMessage('hello');
    port.onmessage = function(event) {
      // Handle reply from the service.
    };
  }
);

// https://example.com/serviceworker.js
this.addEventListener('crossoriginconnect', function(event) {
  // Optionally check event.client.origin to determine if that origin should be
  // allowed access to this service.
  event.acceptConnection(event.client.targetUrl === 'https://example.com/services/echo');
});

this.addEventListener('crossoriginmessage', function(event) {
  event.source.postMessage(event.data, event.ports);
});

Terminology

The terms event handler, event handler event type, and queue a task are defined in [[!HTML5]].

Promise is defined in [[!ECMASCRIPT]].

EventInit, DOMException, and AbortError are defined in [[!DOM]].

MessageChannel, MessagePort, and MessageEvent are defined in [[!WEBMESSAGING]]

Service Worker, ServiceWorkerRegistration, and ServiceWorkerGlobalScope are defined in [[!SERVICE-WORKERS]].

Security and privacy considerations

Most of what is described here can already be achieved using cross origin iframes, regular cross origin messaging, and regular service worker messaging. As such the security and privacy implications of this API are minor. Having said that, there are a few parts that are a few interesting security and privacy considerations to keep in mind:

User agents should not leak any information on sites a user has visited etc to other webpages. In particular this means that a service rejecting a connection attempt should be indistinguishable from no service being installed to service a particular URL.

Currently this API only allows connecting to services that have already been installed. If in the future some mechanism where attempting to connect to a service can trigger installation of a service worker, this of course has its own set of security and privacy implications.

Client Context

Promise<MessagePort> connect(USVString url)

The connect method when invoked runs the "Connect" algorithm.

Maybe connect should have an additional parameter to connect to pass extra structured data to the connection event handler.

Somehow describe that both closing the message port returned by connect and the context connect was called in being destroyed result in this webpage being removed from the cross origin clients for all service workers.

Service Worker Context

CrossOriginServiceWorkerClient

Every cross origin client connected to, or attempting to connect to a service worker, is represented by a CrossOriginServiceWorkerClient instance. Clients that are currently connected are stored in an internal bookkeeping list of clients associated with each Service Worker Registration.

readonly attribute USVString origin
readonly attribute USVString targetUrl
void postMessage(any message, optional sequence<Transferable> transfer)

The origin attribute exposes the origin of the client that connected to this Service Worker.

The targetUrl attribute exposes the url it connected to.

The postMessage method when invoked calls postMessage on the hidden MessagePort associated with this client. If postMessage is called on a client whose connection has not been accepted yet (or when the connection has been rejected), postMessage will raise some exception.

Whenever a message is targetted at the implicit MessagePort associated with a CrossOriginServiceWorkerClient, the "Deliver Message" algorithm is executed.

CrossOriginServiceWorkerClients

Promise<sequence<CrossOriginServiceWorkerClient>> getAll()

The getAll method returns all cross origin clients that are currently connected to this service worker. Specifically this includes all clients for which an oncrossoriginconnect event has been dispatched, and whose connection has been accepted. If a client has closed its messageport it will no longer be returned by getAll, and similarly when a client is unloaded it will no longer be returned by getAll.

The getAll method returns the same list of clients for all service workers that are part of the same service worker registration, even though connect and message events are only dispatched to the active service worker version for a particular registration.

It might be useful to allow filtering on targetUrl in a call to getAll, although that can easily enough be added on top of it.

ServiceWorkerGlobalScope

The Service Worker specification defines a ServiceWorkerGlobalScope interface [[!SERVICE-WORKERS]], which this specification extends.

readonly attribute CrossOriginServiceWorkerClients crossOriginClients
attribute EventHandler oncrossoriginconnect
attribute EventHandler oncrossoriginmessage

Events

The oncrossoriginconnect attribute is an event handler whose corresponding event handler event type is crossoriginconnect.

The oncrossoriginmessage attribute is an event handler whose corresponding event handler event type is crossoriginmessage.

The crossoriginconnect event

The CrossOriginConnectEvent interface represents a received cross origin connection attempts.

readonly attribute CrossOriginServiceWorkerClient client
void acceptConnection(Promise<boolean> shouldAccept)

The acceptConnection method must be called with a promise that resolves to true or false.

The crossoriginmessage event

This is a MessageEvent whose event.source is an instance of CrossOriginServiceWorkerClient.

This isn't possible with the current spec for MessageEvent. The service worker spec has a similar problem with their onmessage event and using a ServiceWorkerClient as source.

Algorithms

Connect

  1. Let promise be a new Promise.
  2. Return promise and continue the following steps asynchronously.
  3. Let registration be the result of running the "Match Scope" algorithm on url
  4. If registration is null, reject promise with a DOMException whose name is "AbortError" and terminate these steps.

    It might be nice to be able to connect to a service without that service having to have been installed first. Maybe web manifests can help here to provide a way to determine what service worker to install for a specific service URL?

  5. If registration's active worker is not null, then:
    1. Run the "Handle Functional Event" algorithm with registration and the following steps as callbackSteps.
    2. Fire an event named crossoriginconnect using CrossOriginConnectEvent interface at the returned global object.

      Better define these next steps.

    3. Let shouldAccept be the parameter passed to the first call of acceptConnection, or null if no event listeners called acceptConnection.
    4. If shouldAccept is true or a Promise resolving to true, do the following:
      1. Let messageChannel be a new MessageChannel.
      2. Resolve promise with the first port of messageChannel.
      3. Let crossOriginClient be a new CrossOriginServiceWorkerClient associated with the second port of messageChannel.
      4. Add crossOriginClient to the list of cross origin clients associated with registration.
    5. Else, reject promise with a DOMException whose name is "AbortError".
  6. Else:

    Figure out sensible behavior when there is no active worker. Probably wait for some worker to become the active worker?

Somehow phrase this to allow other ways for a user agent to connect to services, in particular services that are not implemented as a service worker.

Deliver message

  1. Let messageEvent be the MessageEvent targetted at the implicit MessagePort.
  2. Let registration be the ServiceWorkerRegistration associated with this connection.
  3. Run the "Handle Functional Event" algorithm with registration and the following steps as callbackSteps.
  4. Retarget messageEvent at the returned global object, setting its source to the CrossOriginServiceWorkerClient.
  5. Fire messageEvent as an event named crossoriginmessage at the returned global object.

Note that this means that a message is always delivered to the currently active service worker for a given registration, even if the crossoriginconnect event was delivered to a different version.

Also note that this means that a later service worker registration with a narrower scope that would have captured the connection attempt had it existed at the time the connection was made does not now suddenly get messages for this connection. A ServiceWorkerRegistration is associated with a connection at connection time; further messages don't try to look up a different service worker registration for the target URL, but reuse the same registration.