Cookie Store API

Editor’s Draft,

This version:
https://wicg.github.io/cookie-store/
Issue Tracking:
GitHub
Inline In Spec
Editors:
(Google Inc.)
(Google Inc.)
(Google Inc.)
Not Ready For Implementation

This spec is not yet ready for implementation. It exists in this repository to record the ideas and promote discussion.

Before attempting to implement this spec, please contact the editors.


Abstract

An asynchronous Javascript cookies API for documents and workers

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

logo

1. Introduction

This section is non-normative.

This is a proposal to bring an asynchronous cookie API to scripts running in HTML documents and service workers.

HTTP cookies have, since their origins at Netscape (documentation preserved by archive.org), provided a valuable state-management mechanism for the web.

The synchronous single-threaded script-level document.cookie interface to cookies has been a source of complexity and performance woes further exacerbated by the move in many browsers from:

... to the modern web which strives for smoothly responsive high performance:

On the modern web a cookie operation in one part of a web application cannot block:

Newer parts of the web built in service workers need access to cookies too but cannot use the synchronous, blocking document.cookie interface at all as they both have no document and also cannot block the event loop as that would interfere with handling of unrelated events.

1.1. A Taste of the Proposed Change

Although it is tempting to rethink cookies entirely, web sites today continue to rely heavily on them, and the script APIs for using them are largely unchanged over their first decades of usage.

Today writing a cookie means blocking your event loop while waiting for the browser to synchronously update the cookie jar with a carefully-crafted cookie string in Set-Cookie format:

document.cookie =
  '__Secure-COOKIENAME=cookie-value' +
  '; Path=/' +
  '; expires=Fri, 12 Aug 2016 23:05:17 GMT' +
  '; Secure' +
  '; Domain=example.org';
// now we could assume the write succeeded, but since
// failure is silent it is difficult to tell, so we
// read to see whether the write succeeded
var successRegExp =
  /(^|; ?)__Secure-COOKIENAME=cookie-value(;|$)/;
if (String(document.cookie).match(successRegExp)) {
  console.log('It worked!');
} else {
  console.error('It did not work, and we do not know why');
}

What if you could instead write:

cookieStore.set(
  '__Secure-COOKIENAME',
  'cookie-value',
  {
    expires: Date.now() + 24*60*60*1000,
    domain: 'example.org'
  }).then(function() {
    console.log('It worked!');
  }, function(reason) {
    console.error(
      'It did not work, and this is why:',
      reason);
  });
// Meanwhile we can do other things while waiting for
// the cookie store to process the write...

This also has the advantage of not relying on document and not blocking, which together make it usable from Service Workers, which otherwise do not have cookie access from script.

This proposal also includes a power-efficient monitoring API to replace setTimeout-based polling cookie monitors with cookie change observers.

1.2. Summary

This proposal outlines an asynchronous API using Promises/async functions for the following cookie operations:

1.2.1. Script visibility

A cookie is script-visible when it is in-scope and does not have the HttpOnly cookie flag.

1.2.2. Motivations

Some service workers need access to cookies but cannot use the synchronous, blocking document.cookie interface as they both have no document and also cannot block the event loop as that would interfere with handling of unrelated events.

A new API may also provide a rare and valuable chance to address some outstanding cross-browser incompatibilities and bring divergent specs and user-agent behavior into closer correspondence.

A well-designed and opinionated API may actually make cookies easier to deal with correctly from scripts, with the potential effect of reducing their accidental misuse. An efficient monitoring API, in particular, can be used to replace power-hungry polling cookie scanners.

The API must interoperate well enough with existing cookie APIs (HTTP-level, HTML-level and script-level) that it can be adopted incrementally by a large or complex website.

1.2.3. Opinions

This API defaults cookie paths to / for cookie write operations, including deletion/expiration. The implicit relative path-scoping of cookies to . has caused a lot of additional complexity for relatively little gain given their security equivalence under the same-origin policy and the difficulties arising from multiple same-named cookies at overlapping paths on the same domain. Cookie paths without a trailing / are treated as if they had a trailing / appended for cookie write operations. Cookie paths must start with / for write operations, and must not contain any .. path segments. Query parameters and URL fragments are not allowed in paths for cookie write operations.

URLs without a trailing / are treated as if the final path segment had been removed for cookie read operations, including change monitoring. Paths for cookie read operations are resolved relative to the default read cookie path.

This API defaults cookies to "Secure" when they are written from a secure web origin. This is intended to prevent unintentional leakage to unsecured connections on the same domain. Furthermore it disallows (to the extent permitted by the browser implementation) creation or modification of Secure-flagged cookies from unsecured web origins and enforces special rules for the __Host- and __Secure- cookie name prefixes [RFC6265bis].

This API defaults cookies to "Domain"-less, which in conjunction with "Secure" provides origin-scoped cookie behavior in most modern browsers. When practical the __Host- cookie name prefix should be used with these cookies so that cooperating browsers origin-scope them.

Serialization of expiration times for non-session cookies in a special cookie-specific format has proven cumbersome, so this API allows JavaScript Date objects and numeric timestamps (milliseconds since the beginning of the Unix epoch) to be used instead. The inconsistently-implemented Max-Age parameter is not exposed, although similar functionality is available for the specific case of expiring a cookie.

Cookies without U+003D (=) code points in their HTTP Cookie header serialization are treated as having an empty name, consistent with the majority of current browsers. Cookies with an empty name cannot be set using values containing U+003D (=) code points as this would result in ambiguous serializations in the majority of current browsers.

Internationalized cookie usage from scripts has to date been slow and browser-specific due to lack of interoperability because although several major browsers use UTF-8 interpretation for cookie data, historically Safari and browsers based on WinINet have not. This API mandates UTF-8 interpretation for cookies read or written by this API.

Use of cookie-change-driven scripts has been hampered by the absence of a power-efficient (non-polling) API for this. This API provides observers for efficient monitoring in document contexts and interest registration for efficient monitoring in service worker contexts.

Scripts should not have to write and then read "test cookies" to determine whether script-initiated cookie write access is possible, nor should they have to correlate with cooperating server-side versions of the same write-then-read test to determine that script-initiated cookie read access is impossible despite cookies working at the HTTP level.

1.2.4. Compatiblity

Some user-agents implement non-standard extensions to cookie behavior. The intent of this specification, though, is to first capture a useful and interoperable (or mostly-interoperable) subset of cookie behavior implemented across modern browsers. As new cookie features are specified and adopted it is expected that this API will be extended to include them. A secondary goal is to converge with document.cookie behavior and the http cookie specification. See https://github.com/whatwg/html/issues/804 and https://inikulin.github.io/cookie-compat/ for the current state of this convergence.

Differences across browsers in how bytes outside the printable-ASCII subset are interpreted has led to long-lasting user- and developer-visible incompatibilities across browsers making internationalized use of cookies needlessly cumbersome. This API requires UTF-8 interpretation of cookie data and uses USVString for the script interface, with the additional side-effects that subsequent uses of document.cookie to read a cookie read or written through this interface and subsequent uses of document.cookie to update a cookie previously read or written through this interface will also use a UTF-8 interpretation of the cookie data. In practice this will change the behavior of WinINet-based user agents and Safari but should bring their behavior into concordance with other modern user agents.

2. Concepts

A cookie is normatively defined for user agents by [RFC6265bis].

A cookie has the following fields: name, value, expiry-time, domain, path, creation-time, last-access-time, persistent-flag, host-only-flag, secure-only-flag, http-only-flag, same-site-flag.

2.2. Extensions to Service Worker

[Service-Workers] defines service worker registration, which this specification extends.

A service worker registration has an associated cookie change subscription list which is a list; each member is a cookie change subscription. A cookie change subscription is a tuple of name, url, and matchType. .

3. The CookieStore Interface

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options);

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options);

  Promise<void> set(USVString name, USVString value,
                    optional CookieStoreSetOptions options);
  Promise<void> set(CookieStoreSetExtraOptions options);

  Promise<void> delete(USVString name);
  Promise<void> delete(CookieStoreDeleteOptions options);

  [Exposed=ServiceWorker]
  Promise<void> subscribeToChanges(sequence<CookieStoreGetOptions> subscriptions);

  [Exposed=ServiceWorker]
  Promise<sequence<CookieStoreGetOptions>> getChangeSubscriptions();

  attribute EventHandler onchange;
};

enum CookieMatchType {
  "equals",
  "starts-with"
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
  CookieMatchType matchType = "equals";
};

enum CookieSameSite {
  "strict",
  "lax",
  "unrestricted"
};

dictionary CookieStoreSetOptions {
  DOMTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  boolean secure = true;
  boolean httpOnly = false;
  CookieSameSite sameSite = "strict";
};

dictionary CookieStoreSetExtraOptions : CookieStoreSetOptions {
  required USVString name;
  required USVString value;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean secure = true;
  CookieSameSite sameSite = "strict";
};

dictionary CookieListItem {
  required USVString name;
  USVString value;
  USVString? domain = null;
  USVString path = "/";
  DOMTimeStamp? expires = null;
  boolean secure = true;
  CookieSameSite sameSite = "strict";
};

typedef sequence<CookieListItem> CookieList;

3.1. Methods

3.1.1. get

cookie = await cookieStore . get(name)
cookie = await cookieStore . get(options)

You can read the first in-scope script-visible value for a given cookie name. In a service worker context this defaults to the path of the service worker’s registered scope. In a document it defaults to the path of the current document and does not respect changes from replaceState() or document.domain.

function getOneSimpleOriginCookie() {
  return cookieStore.get('__Host-COOKIENAME').then(function(cookie) {
    console.log(cookie ? ('Current value: ' + cookie.value) : 'Not set');
  });
}

getOneSimpleOriginCookie().then(
  () => console.log('getOneSimpleOriginCookie succeeded!'),
  reason => console.error('getOneSimpleOriginCookie did not succeed: ', reason)
);

You can use exactly the same Promise-based API with the newer async ... await syntax and arrow functions for more readable code:

async function getOneSimpleOriginCookieAsync() {
  let cookie = await cookieStore.get('__Host-COOKIENAME');
  console.log(cookie ? ('Current value: ' + cookie.value) : 'Not set');
}

getOneSimpleOriginCookieAsync().then(
  () => console.log('getOneSimpleOriginCookieAsync succeeded!'),
  reason => console.error('getOneSimpleOriginCookieAsync did not succeed: ', reason));

Remaining examples use this syntax along with destructuring for clarity, and omit the calling code.

In a service worker context you can read a cookie from the point of view of a particular in-scope URL, which may be useful when handling regular (same-origin, in-scope) fetch events or foreign fetch events.

async function getOneCookieForRequestUrl() {
  let cookie = await cookieStore.get('__Secure-COOKIENAME', {url: '/cgi-bin/reboot.php'});
  console.log(cookie ? ('Current value in /cgi-bin is ' + cookie.value) : 'Not set in /cgi-bin');
}
The get(name) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let list be the results of running the steps to query cookies with url and name.

    2. If list is failure, reject p with a TypeError and abort these steps.

    3. If list is empty, resolve p with undefined.

    4. Otherwise, resolve p with the first item of list.

  6. Return p.

The get(options) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. If optionsurl dictionary member is present, then run these steps:

    1. Let parsed be the result of running the basic URL parser on optionsurl dictionary member with url.

    2. If the current global object is a Window object and parsed does not equal url, then return a new promise rejected with an "InvalidStateError" DOMException.

    3. If parsed’s origin and url’s origin are not the same origin, then return a new promise rejected with an "InvalidStateError" DOMException.

    4. Set url to parsed.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let list be the results of running the steps to query cookies with url, optionsname dictionary member (if present), and optionsmatchType dictionary member.

    2. If list is failure, reject p with a TypeError and abort these steps.

    3. If list is empty, resolve p with undefined.

    4. Otherwise, resolve p with the first item of list.

  7. Return p.

3.1.2. getAll

cookies = await cookieStore . getAll(name)
cookies = await cookieStore . getAll(options)

Sometimes you need to see the whole script-visible in-scope subset of the cookie jar, including potential reuse of the same cookie name at multiple paths and/or domains (the paths and domains are not exposed to script by this API, though):

async function countCookies() {
  let cookieList = await cookieStore.getAll();
  console.log('How many cookies? %d', cookieList.length);
  cookieList.forEach(cookie => console.log('Cookie %s has value %o', cookie.name, cookie.value));
}

Sometimes an expected cookie is known by a prefix rather than by an exact name, for instance when reading all cookies managed by a particular library (e.g. in this one the name prefix identifies the library) or when reading all cookie names owned by a particular application on a shared web host (a name prefix is often used to identify the owning application):

async function countMatchingSimpleOriginCookies() {
  let cookieList = await cookieStore.getAll({name: '__Host-COOKIEN', matchType: 'starts-with'});
  console.log('How many matching cookies? %d', cookieList.length);
  cookieList.forEach(({name, value}) => console.log('Matching cookie %s has value %o', name, value));
}
The getAll(name) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let list be the results of running the steps to query cookies with url and name.

    2. If list is failure, reject p with a TypeError.

    3. Otherwise, resolve p with list.

  6. Return p.

The getAll(options) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. If optionsurl dictionary member is present, then run these steps:

    1. Let parsed be the result of running the basic URL parser on optionsurl dictionary member with url.

    2. If the current global object is a Window object and parsed does not equal url, then return a new promise rejected with an "InvalidStateError" DOMException.

    3. If parsed’s origin and url’s origin are not the same origin, then return a new promise rejected with an "InvalidStateError" DOMException.

    4. Set url to parsed.

  5. Let p be a new promise.

  6. Run the following steps in parallel:

    1. Let list be the results of running the steps to query cookies with url, optionsname dictionary member (if present), and optionsmatchType dictionary member.

    2. If list is failure, reject p with a TypeError.

    3. Otherwise, resolve p with list.

  7. Return p.

3.1.3. set

await cookieStore . set(name, value)
await cookieStore . set(options)
Writing (setting) a cookie is done using these methods.
async function setOneSimpleOriginSessionCookie() {
  await cookieStore.set('__Host-COOKIENAME', 'cookie-value');
  console.log('Set!');
}

That defaults to path "/" and implicit domain, and defaults to a Secure-if-https-origin, non-HttpOnly session cookie which will be visible to scripts. You can override any of these defaults except for HttpOnly (which is not settable from script in modern browsers) if needed:

async function setOneDaySecureCookieWithDate() {
  // one day ahead, ignoring a possible leap-second
  let inTwentyFourHours = new Date(Date.now() + 24 * 60 * 60 * 1000);
  await cookieStore.set('__Secure-COOKIENAME', 'cookie-value', {
      path: '/cgi-bin/',
      expires: inTwentyFourHours,
      secure: true,
      domain: 'example.org'
    });
  console.log('Set!');
}

Of course the numeric form (milliseconds since the beginning of 1970 UTC) works too:

async function setOneDayUnsecuredCookieWithMillisecondsSinceEpoch() {
  // one day ahead, ignoring a possible leap-second
  let inTwentyFourHours = Date.now() + 24 * 60 * 60 * 1000;
  await cookieStore.set('LEGACYCOOKIENAME', 'cookie-value', {
      path: '/cgi-bin/',
      expires: inTwentyFourHours,
      secure: false,
      domain: 'example.org'
    });
  console.log('Set!');
}

Sometimes an expiration date comes from existing script it’s not easy or convenient to replace, though:

async function setSecureCookieWithHttpLikeExpirationString() {
  await cookieStore.set('__Secure-COOKIENAME', 'cookie-value', {
      path: '/cgi-bin/',
      expires: 'Mon, 07 Jun 2021 07:07:07 GMT',
      secure: true,
      domain: 'example.org'
    });
  console.log('Set!');
}

In this case the syntax is that of the HTTP cookies spec; any other syntax will result in promise rejection.

You can set multiple cookies too, but - as with HTTP Set-Cookie - the multiple write operations have no guarantee of atomicity:

async function setThreeSimpleOriginSessionCookiesSequentially() {
  await cookieStore.set('__Host-🍪', '🔵cookie-value1🔴');
  await cookieStore.set('__Host-🌟', '🌠cookie-value2🌠');
  await cookieStore.set('__Host-🌱', '🔶cookie-value3🔷');
  console.log('All set!');
  // NOTE: this assumes no concurrent writes from elsewhere; it also
  // uses three separate cookie jar read operations where a single getAll
  // would be more efficient, but this way the CookieStore does the filtering
  // for us.
  let matchingValues = await Promise.all(['🍪', '🌟', '🌱'].map(
    async ಠ_ಠ => (await cookieStore.get('__Host-' + ಠ_ಠ)).value));
  let actual = matchingValues.join(';');
  let expected = '🔵cookie-value1🔴;🌠cookie-value2🌠;🔶cookie-value3🔷';
  if (actual !== expected) {
    throw new Error([
      'Expected ',
      JSON.stringify(expected),
      ' but got ',
      JSON.stringify(actual)].join(''));
  }
  console.log('All verified!');
}

If the relative order is unimportant the operations can be performed without specifying the order:

async function setThreeSimpleOriginSessionCookiesNonsequentially() {
  await Promise.all([
    cookieStore.set('__Host-unordered🍪', '🔵unordered-cookie-value1🔴'),
    cookieStore.set('__Host-unordered🌟', '🌠unordered-cookie-value2🌠'),
    cookieStore.set('__Host-unordered🌱', '🔶unordered-cookie-value3🔷')]);
  console.log('All set!');
  // NOTE: this assumes no concurrent writes from elsewhere; it also
  // uses three separate cookie jar read operations where a single getAll
  // would be more efficient, but this way the CookieStore does the filtering
  // for us.
  let matchingCookies = await Promise.all(['🍪', '🌟', '🌱'].map(
    ಠ_ಠ => cookieStore.get('__Host-unordered' + ಠ_ಠ)));
  let actual = matchingCookies.map(({value}) => value).join(';');
  let expected =
    '🔵unordered-cookie-value1🔴;🌠unordered-cookie-value2🌠;🔶unordered-cookie-value3🔷';
  if (actual !== expected) {
    throw new Error([
      'Expected ',
      JSON.stringify(expected),
      ' but got ',
      JSON.stringify(actual)].join(''));
  }
  console.log('All verified!');
}
The set(name, value, options) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let r be the result of running the steps to set a cookie with url, name, value, optionsexpires dictionary member, optionsdomain dictionary member, optionspath dictionary member, optionssecure dictionary member, optionshttpOnly dictionary member, and optionssameSite dictionary member.

    2. If r is failure, reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  6. Return p.

The set(options) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let r be the result of running the steps to set a cookie with url, optionsname dictionary member, optionsvalue dictionary member, optionsexpires dictionary member, optionsdomain dictionary member, optionspath dictionary member, optionssecure dictionary member, optionshttpOnly dictionary member, and optionssameSite dictionary member.

    2. If r is failure, reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  6. Return p.

3.1.4. delete

await cookieStore . set(name)
await cookieStore . set(options)
Deleting (or clearing) a cookie is done using these methods.

Deleting a cookie is accomplished by expiration, that is by replacing it with an equivalent-scope cookie with an expiration in the past:

async function setExpiredSecureCookieWithDomainPathAndFallbackValue() {
  let theVeryRecentPast = Date.now();
  let expiredCookieSentinelValue = 'EXPIRED';
  await cookieStore.set('__Secure-COOKIENAME', expiredCookieSentinelValue, {
      expires: theVeryRecentPast
    });
  console.log('Expired! Deleted!! Cleared!!1!');
}

In this case the cookie’s value is not important unless a clock is somehow re-set incorrectly or otherwise behaves nonmonotonically or incoherently.

A syntactic shorthand is also provided which is equivalent to the above except that the clock’s accuracy and monotonicity becomes irrelevant:

async function deleteSimpleOriginCookie() {
  await cookieStore.delete('__Host-COOKIENAME');
  console.log('Expired! Deleted!! Cleared!!1!');
}

Again, the path and/or domain can be specified explicitly here.

async function deleteSecureCookieWithDomainAndPath() {
  await cookieStore.delete('__Secure-COOKIENAME', {
      path: '/cgi-bin/',
      domain: 'example.org',
      secure: true
    });
  console.log('Expired! Deleted!! Cleared!!1!');
}

This API has semantics aligned with the interpretation of Max-Age=0 common to most modern browsers.

The delete(name) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let r be the result of running the steps to delete a cookie with url, name, null, "/", true, and "strict".

    2. If r is failure, reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  6. Return p.

The delete(options) method, when invoked, must run these steps:
  1. Let origin be environment’s origin.

  2. If origin is an opaque origin, then return a new promise rejected with a "SecurityError" DOMException.

  3. Let url be the current settings object's creation URL.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. Let r be the result of running the steps to delete a cookie with url, optionsname dictionary member, optionsdomain dictionary member, optionspath dictionary member, optionssecure dictionary member, and optionssameSite dictionary member.

    2. If r is failure, reject p with a TypeError and abort these steps.

    3. Resolve p with undefined.

  6. Return p.

3.1.5. subscribeToChanges

Monitoring cookies in a Service Worker requires by subscribing to change events.

await cookieStore . subscribeToChanges(subscriptions)
This method can only be called during a Service Worker install phase.
self.addEventListener('install', event => {
  event.waitFor(async () => {
    await cookieStore.subscribeToChanges([{
      name: 'session',  // Get change events for session-related cookies.
      matchType: 'starts-with',  // Matches session_id, session-id, etc.
    }]);
  });
});

Once subscribed, notifications are delivered as "cookiechange" events fired against the Service Worker's global scope:

self.addEventListener('cookiechange', event => {
  // The event has |changed| and |deleted| properties with
  // the same semantics as the Document events.
  console.log('changed cookies: ' + event.changed.length);
  console.log('deleted cookies: ' + event.deleted.length);
});
subscriptions = await cookieStore . getChangeSubscriptions()
This method returns a promise which resolves to a list of the cookie change subscriptions made for this Service Worker registration.
   const subscriptions = await cookieStore.getChangeSubscriptions();
   for (const sub of subscriptions) {
     console.log(sub.name, sub.url, sub.matchType);
   }
The subscribeToChanges(subscriptions) method, when invoked, must run these steps:
  1. Let serviceWorker be the context object's global object's service worker.

  2. If serviceWorker’s state is not installing, then return a new promise rejected with a TypeError.

  3. Let registration be serviceWorker’s associated containing service worker registration.

  4. Let p be a new promise.

  5. Run the following steps in parallel:

    1. For each entry in subscriptions, run these steps:

      1. Let name be entry’s name member.

      2. Let url be entry’s url member.

      3. Let matchType be entry’s matchType member.

      4. Let subscription be the cookie change subscription (name, url, matchType).

      5. Append subscription to registration’s associated cookie change subscription list.

  6. Return p.

3.1.6. getChangeSubscriptions

The getChangeSubscriptions() method, when invoked, must run these steps:
  1. Let serviceWorker be the context object's global object's service worker.

  2. Let registration be serviceWorker’s associated containing service worker registration.

  3. Let p be a new promise.

  4. Run the following steps in parallel:

    1. Let subscriptions be registration’s associated cookie change subscription list.

    2. Let result be a new list.

    3. For each subscription in subscriptions, run these steps:

      1. Let options be a new CookieStoreGetOptions dictionary.

      2. Set options’s name member to subscription’s name.

      3. Set options’s url member to subscription’s url.

      4. Set options’s matchType member to subscription’s matchType.

    4. Resolve p with result.

  5. Return p.

3.2. Attributes

3.2.1. onchange

onchange, of type EventHandler

An EventHandler of type CookieChangeEvent.

3.3. Events

3.3.1. CookieChangeEvent

A CookieChangeEvent is dispatched against CookieStore objects in window contexts when any script-visible cookie changes have occurred, and against ServiceWorkerGlobalScope objects when any script-visible cookie changes have occurred which match the Service Worker's cookie change subscription list.

[Exposed=(ServiceWorker,Window),
 SecureContext,
 Constructor(DOMString type, optional CookieChangeEventInit eventInitDict)]
interface CookieChangeEvent : Event {
  readonly attribute CookieList changed;
  readonly attribute CookieList deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

4. Global Interfaces

4.1. The Window Interface

[SecureContext]
partial interface Window {
  [Replaceable, SameObject] readonly attribute CookieStore cookieStore;
};

The cookieStore attribute’s getter must return context object’s relevant settings object’s CookieStore object.

4.2. The ServiceWorkerGlobalScope Interface

partial interface ServiceWorkerGlobalScope {
  [Replaceable, SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};

The cookieStore attribute’s getter must return context object’s relevant settings object’s CookieStore object.

5. Algorithms

5.1. Query Cookies

Specify the query cookies algorithm. <https://github.com/WICG/cookie-store/issues/69>

To query cookies with url, optional name, and optional matchType, run the following steps:

  1. If ..., return failure.

  2. ...

  3. ... name ... url

  4. ...

  5. ... being generated for a "non-HTTP" API ...

  6. ...

  7. ... cookie-list ...

  8. Let list be a new list.

  9. For each cookie in cookie-list, run these steps:

    1. Assert: cookie’s http-only-flag is false.

    2. ... matchType ... continue ...

    3. Let item be the result of running the steps to create a CookieListItem from cookie.

    4. Append item to list.

  10. Return list.

To create a CookieListItem from cookie, run the following steps.

  1. Let item be a new CookieListItem.

  2. Set item’s name to cookie’s name.

  3. Set item’s value to cookie’s value.

  4. Set item’s domain to cookie’s domain.

  5. Set item’s path to cookie’s path.

  6. Set item’s expires to cookie’s expiry-time, as the number of milliseconds since 00:00:00 UTC, 1 January 1970, assuming that there are exactly 86,400,000 milliseconds per day.

    Note: This is the same representation used for time values in [ECMAScript].

  7. Set item’s secure to cookie’s secure-only-flag.

  8. Switch on cookie’s same-site-flag:

    "None"

    Set item’s sameSite to "unrestricted".

    "Strict"

    Set item’s sameSite to "strict".

    "Lax"

    Set item’s sameSite to "lax".

  9. Return item.

Note: The cookie’s creation-time, last-access-time, persistent-flag, host-only-flag, and http-only-flag attributes are not exposed to script.

Specify the set a cookie algorithm. <https://github.com/WICG/cookie-store/issues/72>

To set a cookie with url, name, value, optional expires, domain, path, secure flag, httpOnly flag, and sameSite, run the following steps:

  1. If ..., return failure.

  2. ...

  3. ... name ... expires ... httpOnly ...

Specify the delete a cookie algorithm. <https://github.com/WICG/cookie-store/issues/73>

To delete a cookie with url, name, domain, path, secure flag, and sameSite, run the following steps:

  1. If ..., return failure.

  2. ...

  3. ... name

6. Security

Other than cookie access from service worker contexts, this API is not intended to expose any new capabilities to the web.

6.1. Gotcha!

Although browser cookie implementations are now evolving in the direction of better security and fewer surprising and error-prone defaults, there are at present few guarantees about cookie data security.

For these reasons it is best to use caution when interpreting any cookie’s value, and never execute a cookie’s value as script, HTML, CSS, XML, PDF, or any other executable format.

6.2. Restrict?

This API may have the unintended side-effect of making cookies easier to use and consequently encouraging their further use. If it causes their further use in unsecured http contexts this could result in a web less safe for users. For that reason it may be desirable to restrict its use, or at least the use of the set and delete operations, to secure origins running in secure contexts.

6.3. Surprises

Some existing cookie behavior (especially domain-rather-than-origin orientation, unsecured contexts being able to set cookies readable in secure contexts, and script being able to set cookies unreadable from script contexts) may be quite surprising from a web security standpoint.

Other surprises are documented in Section 1 of Cookies: HTTP State Management Mechanism (RFC 6265bis) - for instance, a cookie may be set for a superdomain (e.g. app.example.com may set a cookie for the whole example.com domain), and a cookie may be readable across all port numbers on a given domain name.

Further complicating this are historical differences in cookie-handling across major browsers, although some of those (e.g. port number handling) are now handled with more consistency than they once were.

6.4. Prefixes

Where feasible the examples use the __Host- and __Secure- name prefixes which causes some current browsers to disallow overwriting from unsecured contexts, disallow overwriting with no Secure flag, and — in the case of __Host- — disallow overwriting with an explicit Domain or non-'/' Path attribute (effectively enforcing same-origin semantics.) These prefixes provide important security benefits in those browsers implementing Secure Cookies and degrade gracefully (i.e. the special semantics may not be enforced in other cookie APIs but the cookies work normally and the async cookies API enforces the secure semantics for write operations) in other browsers. A major goal of this API is interoperation with existing cookies, though, so a few examples have also been provided using cookie names lacking these prefixes.

Prefix rules are also enforced in write operations by this API, but may not be enforced in the same browser for other APIs. For this reason it is inadvisable to rely on their enforcement too heavily until and unless they are more broadly adopted.

6.5. URL scoping

Although a service worker script cannot directly access cookies today, it can already use controlled rendering of in-scope HTML and script resources to inject cookie-monitoring code under the remote control of the service worker script. This means that cookie access inside the scope of the service worker is technically possible already, it’s just not very convenient.

When the service worker is scoped more narrowly than / it may still be able to read path-scoped cookies from outside its scope’s path space by successfully guessing/constructing a 404 page URL which allows IFRAME-ing and then running script inside it the same technique could expand to the whole origin, but a carefully constructed site (one where no out-of-scope pages are IFRAME-able) can actually deny this capability to a path-scoped service worker today and I was reluctant to remove that restriction without further discussion of the implications.

6.6. Cookie aversion

To reduce complexity for developers and eliminate the need for ephemeral test cookies, this async cookies API will explicitly reject attempts to write or delete cookies when the operation would be ignored. Likewise it will explicitly reject attempts to read cookies when that operation would ignore actual cookie data and simulate an empty cookie jar. Attempts to observe cookie changes in these contexts will still "work", but won’t invoke the callback until and unless read access becomes allowed (due e.g. to changed site permissions.)

Today writing to document.cookie in contexts where script-initiated cookie-writing is disallowed typically is a no-op. However, many cookie-writing scripts and frameworks always write a test cookie and then check for its existence to determine whether script-initiated cookie-writing is possible.

Likewise, today reading document.cookie in contexts where script-initiated cookie-reading is disallowed typically returns an empty string. However, a cooperating web server can verify that server-initiated cookie-writing and cookie-reading work and report this to the script (which still sees empty string) and the script can use this information to infer that script-initiated cookie-reading is disallowed.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[RFC6265bis]
A. Barth; M. West. Cookies: HTTP State Management Mechanism. Internet-Draft. URL: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-02
[SERVICE-WORKERS-2]
Service Workers URL: https://w3c.github.io/ServiceWorker/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[ECMAScript]
ECMAScript Language Specification. URL: https://tc39.github.io/ecma262/
[Service-Workers]
Alex Russell; et al. Service Workers 1. 2 November 2017. WD. URL: https://www.w3.org/TR/service-workers-1/

IDL Index

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options);

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options);

  Promise<void> set(USVString name, USVString value,
                    optional CookieStoreSetOptions options);
  Promise<void> set(CookieStoreSetExtraOptions options);

  Promise<void> delete(USVString name);
  Promise<void> delete(CookieStoreDeleteOptions options);

  [Exposed=ServiceWorker]
  Promise<void> subscribeToChanges(sequence<CookieStoreGetOptions> subscriptions);

  [Exposed=ServiceWorker]
  Promise<sequence<CookieStoreGetOptions>> getChangeSubscriptions();

  attribute EventHandler onchange;
};

enum CookieMatchType {
  "equals",
  "starts-with"
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
  CookieMatchType matchType = "equals";
};

enum CookieSameSite {
  "strict",
  "lax",
  "unrestricted"
};

dictionary CookieStoreSetOptions {
  DOMTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  boolean secure = true;
  boolean httpOnly = false;
  CookieSameSite sameSite = "strict";
};

dictionary CookieStoreSetExtraOptions : CookieStoreSetOptions {
  required USVString name;
  required USVString value;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean secure = true;
  CookieSameSite sameSite = "strict";
};

dictionary CookieListItem {
  required USVString name;
  USVString value;
  USVString? domain = null;
  USVString path = "/";
  DOMTimeStamp? expires = null;
  boolean secure = true;
  CookieSameSite sameSite = "strict";
};

typedef sequence<CookieListItem> CookieList;

[Exposed=(ServiceWorker,Window),
 SecureContext,
 Constructor(DOMString type, optional CookieChangeEventInit eventInitDict)]
interface CookieChangeEvent : Event {
  readonly attribute CookieList changed;
  readonly attribute CookieList deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

[SecureContext]
partial interface Window {
  [Replaceable, SameObject] readonly attribute CookieStore cookieStore;
};

partial interface ServiceWorkerGlobalScope {
  [Replaceable, SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};

Issues Index

Specify the query cookies algorithm. <https://github.com/WICG/cookie-store/issues/69>
Specify the set a cookie algorithm. <https://github.com/WICG/cookie-store/issues/72>
Specify the delete a cookie algorithm. <https://github.com/WICG/cookie-store/issues/73>