From 895dd362c324072abba2a39e2eeae90464b831c2 Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Tue, 19 Nov 2024 13:24:20 +0100 Subject: [PATCH 1/3] Expose pushManager on Window This is part of the Declarative Web Push initiative (see #360). This makes window.pushManager work by making push subscriptions tied to a scope rather than a service worker registration. Most often push subscriptions remain 1:1 with service worker registrations, but the scope whose serialized path is "/" is treated specially from now on and can exist independently. --- index.html | 310 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 210 insertions(+), 100 deletions(-) diff --git a/index.html b/index.html index 603265b..9a7e299 100644 --- a/index.html +++ b/index.html @@ -699,9 +699,19 @@

A push subscription is a message delivery context established between the - user agent and the push service on behalf of a web application. Each - push subscription is associated with a service worker registration and a - service worker registration has at most one push subscription. + user agent and the push service on behalf of a web application. +

+

+ A [=push subscription=] has an associated scope, which is a [=/URL=]. +

+

+ A [=push subscription=] is considered to have a window-accessible scope when its [=push subscription/scope=] is + a [=/list=] of [=list/size=] 1 and [=push subscription/scope=][0] is the empty string. +

+

+ I.e., the [=url/path=] component of the [=/URL=] serializes as "`/`".

A push subscription has an associated push endpoint. It MUST be the @@ -721,12 +731,9 @@

creating the push subscription.

- If the user agent has to change the keys for any reason, it MUST fire the - "`pushsubscriptionchange`" event with the service worker registration - associated with the push subscription as |registration|, a {{PushSubscription}} - instance representing the push subscription having the old keys as - |oldSubscription| and a {{PushSubscription}} instance representing the push - subscription having the new keys as |newSubscription|. + If the user agent has to change the keys of a [=push subscription=] for any reason + and the [=push subscription=]'s [=associated service worker registration=] is non-null, + it MUST [=refresh=] the [=push subscription=].

To create a push subscription, given an {{PushSubscriptionOptionsInit}} @@ -771,17 +778,41 @@

- Subscription Refreshes + Relationship to service worker registrations +

+

+ A [=push subscription=]'s associated service worker registration is the + [=service worker registration=] whose [=service worker registration/scope URL=] + [=URL/equals=] the [=push subscription=]'s [=push subscription/scope=], if any; + otherwise null. +

+

+ A [=push subscription=]'s [=associated service worker registration=] can only be null + when it has a [=push subscription/window-accessible scope=]. +

+

+ And vice versa, a [=service worker registration=]'s associated push + subscription is the [=push subscription=] whose [=push subscription/scope=] + [=URL/equals=] the [=service worker registration=]'s [=service worker + registration/scope URL=], if any; otherwise null. +

+
+
+

+ Subscription refreshes

A user agent or push service MAY choose to refresh a push - subscription at any time, for example because it has reached a certain age. + subscription whose [=associated service worker registration=] is non-null at any + time, for example because it has reached a certain age.

When this happens, the user agent MUST run the steps to create a push subscription given the PushSubscriptionOptions that were provided for - creating the current push subscription. The new push subscription MUST - have a key pair that's different from the original subscription. + creating the current push subscription, and set the new [=push subscription=]'s + [=push subscription/scope=] to the original subscription's [=push subscription/scope=]. + The new push subscription MUST have a key pair that's different from the + original subscription.

When successful, user agent then MUST fire the "`pushsubscriptionchange`" @@ -808,7 +839,7 @@

- Subscription Deactivation + Subscription deactivation

When a push subscription is deactivated, both @@ -817,13 +848,13 @@

delivered.

- A push subscription is deactivated when its associated service worker - registration is unregistered, though a push subscription MAY be - deactivated earlier. + A push subscription without a [=push subscription/window-accessible scope=] is + deactivated when its associated service worker registration is + unregistered, though a push subscription MAY be deactivated earlier.

- A push subscription is removed when service worker registration is - cleared. + A push subscription without a [=push subscription/window-accessible scope=] is + removed when service worker registration is cleared.

@@ -1049,23 +1080,23 @@

-

- Extensions to the `ServiceWorkerRegistration` Interface +

+ `PushManagerAttribute` mixin

- The Service Worker specification defines a {{ServiceWorkerRegistration}} interface - [[SERVICE-WORKERS]], which this specification extends. + This specifications extends the {{Window}} and {{ServiceWorkerRegistration}} objects + through the {{PushManagerAttribute}} mixin. [[HTML]] [[SERVICE-WORKERS]]

-
+      
         [SecureContext]
-        partial interface ServiceWorkerRegistration {
+        interface mixin PushManagerAttribute {
           readonly attribute PushManager pushManager;
         };
+        Window includes PushManagerAttribute;
+        ServiceWorkerRegistration includes PushManagerAttribute;
       

- The pushManager attribute exposes a {{PushManager}}, which has an associated - service worker registration represented by the {{ServiceWorkerRegistration}} on - which the attribute is exposed. + The pushManager attribute exposes a {{PushManager}}.

@@ -1107,7 +1138,23 @@

  • Let |global| be [=this=]' [=relevant global object=].
  • -
  • Return |promise| and continue [=in parallel=]. +
  • Let |scope| be null. +
  • +
  • Let |registration| be null. +
  • +
  • If [=this=] is a {{Window}} object, then set |scope| to the result of running the + [=basic URL parser=] given "`/`" and |global|'s associated Document's + [=Document/URL=]. +
  • +
  • Otherwise: +
      +
    1. [=/Assert=]: [=this=] is a {{ServiceWorkerRegistration}} object. +
    2. +
    3. Set |registration| to [=this=]'s associated [=service worker registration=]. +
    4. +
    +
  • +
  • Run these steps [=in parallel=]: -
  • -
  • If the |options| argument has a {{PushSubscriptionOptionsInit/userVisibleOnly}} value - set to `false` and the user agent requires it to be `true`, [=queue a global task=] on the - [=networking task source=] using |global| to [=reject=] |promise| {{"NotAllowedError"}} - {{DOMException}} -
  • -
  • If the |options| argument does not include a non-null value for the - {{PushSubscriptionOptionsInit/applicationServerKey}} member, and the push service - requires one to be given, [=queue a global task=] on the [=networking task source=] using - |global| to [=reject=] |promise| with a {{"NotSupportedError"}} {{DOMException}}. -
  • -
  • If the |options| argument includes a non-null value for the - {{PushSubscriptionOptions/applicationServerKey}} attribute, run the following sub-steps:
      -
    1. If |options|'s {{PushSubscriptionOptionsInit/applicationServerKey}} is a - {{DOMString}}, set its value to an {{ArrayBuffer}} containing the sequence of octets - that result from decoding |options|'s - {{PushSubscriptionOptionsInit/applicationServerKey}} using the base64url encoding - [[RFC7515]]. +
    2. If the |options| argument has a {{PushSubscriptionOptionsInit/userVisibleOnly}} + value set to `false` and the user agent requires it to be `true`, [=queue a global + task=] on the [=networking task source=] using |global| to [=reject=] |promise| + {{"NotAllowedError"}} {{DOMException}}
    3. -
    4. If decoding fails, [=queue a global task=] on the [=networking task source=] using - |global| to [=reject=] |promise| with an {{"InvalidCharacterError"}} {{DOMException}} - and terminate these steps. +
    5. If the |options| argument does not include a non-null value for the + {{PushSubscriptionOptionsInit/applicationServerKey}} member, and the push + service requires one to be given, [=queue a global task=] on the [=networking task + source=] using |global| to [=reject=] |promise| with a {{"NotSupportedError"}} + {{DOMException}}.
    6. -
    7. Ensure that |options|'s {{PushSubscriptionOptionsInit/applicationServerKey}} - describes a valid point on the P-256 curve. If its value is invalid, [=queue a global - task=] on the [=networking task source=] using |global| to [=reject=] |promise| with an - {{"InvalidAccessError"}} {{DOMException}} and terminate these steps. +
    8. If the |options| argument includes a non-null value for the + {{PushSubscriptionOptions/applicationServerKey}} attribute: +
        +
      1. If |options|'s {{PushSubscriptionOptionsInit/applicationServerKey}} is a + {{DOMString}}, set its value to an {{ArrayBuffer}} containing the sequence of + octets that result from decoding |options|'s + {{PushSubscriptionOptionsInit/applicationServerKey}} using the base64url encoding + [[RFC7515]]. +
      2. +
      3. If decoding fails, [=queue a global task=] on the [=networking task source=] + using |global| to [=reject=] |promise| with an {{"InvalidCharacterError"}} + {{DOMException}} and terminate these steps. +
      4. +
      5. Ensure that |options|'s {{PushSubscriptionOptionsInit/applicationServerKey}} + describes a valid point on the P-256 curve. If its value is invalid, [=queue a + global task=] on the [=networking task source=] using |global| to [=reject=] + |promise| with an {{"InvalidAccessError"}} {{DOMException}} and terminate these + steps. +
      6. +
    9. -
    -
  • -
  • Let |registration:ServiceWorkerRegistration| be [=this=]'s associated service worker - registration. -
  • -
  • If |registration|'s [=service worker registration/active worker=] is null, [=queue a - global task=] on the [=networking task source=] using |global| to [=reject=] |promise| with - an {{"InvalidStateError"}} {{DOMException}} and terminate these steps. -
  • -
  • Let |permission| be [=request permission to use=] "push". -
  • -
  • If |permission| is {{PermissionState/"denied"}}, [=queue a global task=] on the [=user - interaction task source=] using |global| to [=reject=] |promise| with a - {{"NotAllowedError"}} {{DOMException}} and terminate these steps. -
  • -
  • If |registration| has a push subscription: -
      -
    1. Let |subscription| be the result of obtaining |registration|'s push - subscription. If there is an error, [=queue a global task=] on the [=networking - task source=] using |global| to [=reject=] |promise| with an {{"AbortError"}} - {{DOMException}} and terminate these steps. +
    2. Let |subscription| be null. +
    3. +
    4. If |scope| is non-null: +
        +
      1. If there is a [=push subscription=] with a [=push + subscription/window-accessible scope=] whose [=push subscription/scope=] + [=URL/equals=] |scope|, then set |subscription| to that [=push subscription=]. +
      2. +
    5. -
    6. Compare the |options| argument with the `options` attribute of |subscription|. The - contents of {{BufferSource}} values are compared for equality rather than - [=ECMAScript/reference record|reference=]. +
    7. Otherwise: +
        +
      1. [=/Assert=]: |registration| is non-null. +
      2. +
      3. If |registration|'s [=service worker registration/active worker=] is null, + [=queue a global task=] on the [=networking task source=] using |global| to + [=reject=] |promise| with an {{"InvalidStateError"}} {{DOMException}} and terminate + these steps. +
      4. +
      5. If |registration|'s [=associated push subscription=] is non-null, then set + |subscription| to |registration|'s [=associated push subscription=]. +
      6. +
      7. Set |scope| to |registration|'s [=service worker registration/scope URL=]. +
      8. +
    8. -
    9. If any attribute on |options| contains a different value to that stored for - |subscription|, then [=queue a global task=] on the [=networking task source=] using - |global| to [=reject=] |promise| with an {{"InvalidStateError"}} {{DOMException}} and - terminate these steps. +
    10. Let |permission| be [=request permission to use=] "push".
    11. -
    12. When the request has been completed, [=queue a global task=] on the [=networking - task source=] using |global| to [=resolve=] |promise| with |subscription| and terminate - these steps. +
    13. If |permission| is {{PermissionState/"denied"}}, [=queue a global task=] on the + [=user interaction task source=] using |global| to [=reject=] |promise| with a + {{"NotAllowedError"}} {{DOMException}} and terminate these steps. +
    14. +
    15. If |subscription| is non-null: +
        +
      1. If there is an error with |subscription|, then [=queue a global task=] on the + [=networking task source=] using |global| to [=reject=] |promise| with an + {{"AbortError"}} {{DOMException}} and terminate these steps. +
      2. +
      3. Compare the |options| argument with the `options` attribute of |subscription|. + The contents of {{BufferSource}} values are compared for equality rather than + [=ECMAScript/reference record|reference=]. +
      4. +
      5. If any attribute on |options| contains a different value to that stored for + |subscription|, then [=queue a global task=] on the [=networking task source=] + using |global| to [=reject=] |promise| with an {{"InvalidStateError"}} + {{DOMException}} and terminate these steps. +
      6. +
      7. [=Queue a global task=] on the [=networking task source=] using |global| to + [=resolve=] |promise| with |subscription| and terminate these steps. +
      8. +
      +
    16. +
    17. [=/Assert=]: |subscription| is null and |scope| is a [=/URL=]. +
    18. +
    19. Set |subscription| to the result of trying to [=create a push subscription=] with + |options|. If creating the subscription [=exception/throws=] an [=exception=], [=queue + a global task=] on the [=networking task source=] using |global| to [=reject=] + |promise| with a that [=exception=] and terminate these steps. +
    20. +
    21. Set |subscription|'s [=push subscription/scope=] to |scope|. +
    22. +
    23. [=Queue a global task=] on the [=networking task source=] using |global| to + [=resolve=] |promise| with a {{PushSubscription}} corresponding to |subscription|.
  • -
  • Let |subscription| be the result of trying to [=create a push subscription=] with - |options|. If creating the subscription [=exception/throws=] an [=exception=], [=queue a - global task=] on the [=networking task source=] using |global| to [=reject=] |promise| with - a that [=exception=] and terminate these these steps. -
  • -
  • Otherwise, [=queue a global task=] on the [=networking task source=] using |global| to - [=resolve=] |promise| with a {{PushSubscription}} providing the details of the new - |subscription|. +
  • Return |promise|.
  • @@ -1200,17 +1273,54 @@

    1. Let |promise| be a new promise.
    2. -
    3. Return |promise| and continue the following steps asynchronously. +
    4. Let |global| be [=this=]'s [=relevant global object=].
    5. -
    6. If the Service Worker is not subscribed, resolve |promise| with null. +
    7. Let |windowScope| be null.
    8. -
    9. Retrieve the push subscription associated with the Service Worker. +
    10. Let |registration| be null.
    11. -
    12. If there is an error, reject |promise| with a {{DOMException}} whose name is - {{"AbortError"}} and terminate these steps. +
    13. If [=this=] is a {{Window}} object, then set |windowScope| to the result of running the + [=basic URL parser=] given "`/`" and [=this=]'s associated Document's + [=Document/URL=]. +
    14. +
    15. Otherwise: +
        +
      1. [=/Assert=]: [=this=] is a {{ServiceWorkerRegistration}} object. +
      2. +
      3. Set |registration| to [=this=]'s associated [=service worker registration=]. +
      4. +
      +
    16. +
    17. Run these steps [=/in parallel=]: +
        +
      1. Let |subscription| be null. +
      2. +
      3. If |windowScope| is non-null: +
          +
        1. If there is a [=push subscription=] whose [=push subscription/scope=] is + |windowScope|, then set |subscription| to that [=push subscription=]. +
        2. +
        +
      4. +
      5. Otherwise: +
          +
        1. [=/Assert=]: |registration| is non-null. +
        2. +
        3. If |registration|'s [=associated push subscription=] is non-null, then set + |subscription| to |registration|'s [=associated push subscription=]. +
        4. +
        +
      6. +
      7. If |subscription| is null, then resolve |promise| with null. +
      8. +
      9. If there is an error with |subscription|, reject |promise| with a {{DOMException}} + whose name is {{"AbortError"}} and terminate these steps. +
      10. +
      11. Resolve |promise| with a {{PushSubscription}} corresponding to |subscription|. +
      12. +
    18. -
    19. When the request has been completed, resolve |promise| with a {{PushSubscription}} - providing the details of the retrieved push subscription. +
    20. Return |promise|.

    From b6747b268eff83ac6dc8723c58566108fb0c4b7d Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Wed, 6 Aug 2025 17:46:10 +0200 Subject: [PATCH 2/3] Address incorrect usage of "this" and make Receiving account for lack of service worker --- index.html | 96 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/index.html b/index.html index 9a7e299..acbe5fc 100644 --- a/index.html +++ b/index.html @@ -707,8 +707,9 @@

    A [=push subscription=] is considered to have a window-accessible scope when its [=push subscription/scope=] is - a [=/list=] of [=list/size=] 1 and [=push subscription/scope=][0] is the empty string. + "push subscription">window-accessible scope when its [=push subscription/scope=]'s + [=url/path=] is a [=/list=] of [=list/size=] 1 and [=push subscription/scope=]'s + [=url/path=][0] is the empty string.

    I.e., the [=url/path=] component of the [=/URL=] serializes as "`/`". @@ -1098,6 +1099,15 @@

    The pushManager attribute exposes a {{PushManager}}.

    +

    + On a {{Window}} object the {{PushManager}}'s [=PushManager/service worker registration=] is + null. +

    +

    + On a {{ServiceWorkerRegistration}} object the {{PushManager}}'s [=PushManager/service + worker registration=] is the service worker registration represented by the + {{ServiceWorkerRegistration}} object. +

    @@ -1116,6 +1126,10 @@

    Promise<PermissionState> permissionState(optional PushSubscriptionOptionsInit options = {}); }; +

    + A {{PushManager}} has an associated service worker + registration which is null or a service worker registration. +

    The supportedContentEncodings attribute exposes the sequence of supported content codings that can be used to encrypt the payload of a push message. A content @@ -1131,26 +1145,31 @@

    `subscribe()` method

    - The subscribe() method when invoked MUST run the following steps: + The subscribe() method steps are:

    1. Let |promise| be [=a new promise=].
    2. -
    3. Let |global| be [=this=]' [=relevant global object=]. +
    4. Let |global| be [=this=]'s [=relevant global object=].
    5. Let |scope| be null.
    6. Let |registration| be null.
    7. -
    8. If [=this=] is a {{Window}} object, then set |scope| to the result of running the - [=basic URL parser=] given "`/`" and |global|'s associated Document's - [=Document/URL=]. +
    9. If [=this=]'s [=PushManager/service worker registration=] is null: +
        +
      1. Set |scope| to the result of running the [=basic URL parser=] given "`/`" and + |global|'s associated Document's [=Document/URL=]. +
      2. + +
    10. Otherwise:
        -
      1. [=/Assert=]: [=this=] is a {{ServiceWorkerRegistration}} object. +
      2. [=/Assert=]: [=this=]'s [=PushManager/service worker registration=] is a [=service + worker registration=].
      3. -
      4. Set |registration| to [=this=]'s associated [=service worker registration=]. +
      5. Set |registration| to [=this=]'s [=PushManager/service worker registration=].
    11. @@ -1162,7 +1181,7 @@

      {{PushSubscriptionOptionsInit/userVisibleOnly}} is allowed after validating the {{PushSubscriptionOptionsInit/applicationServerKey}}, and vice versa). However, we don't believe this affects interoperability of implementations or web applications. -

      +

      1. If the |options| argument has a {{PushSubscriptionOptionsInit/userVisibleOnly}} @@ -1267,8 +1286,7 @@

      - The getSubscription method when invoked MUST run the - following steps: + The getSubscription() method steps are:

      1. Let |promise| be a new promise. @@ -1279,15 +1297,16 @@

      2. Let |registration| be null.
      3. -
      4. If [=this=] is a {{Window}} object, then set |windowScope| to the result of running the - [=basic URL parser=] given "`/`" and [=this=]'s associated Document's - [=Document/URL=]. +
      5. If [=this=]'s [=PushManager/service worker registration=] is null, then set + |windowScope| to the result of running the [=basic URL parser=] given "`/`" and [=this=]'s + associated Document's [=Document/URL=].
      6. Otherwise:
          -
        1. [=/Assert=]: [=this=] is a {{ServiceWorkerRegistration}} object. +
        2. [=/Assert=]: [=this=]'s [=PushManager/service worker registration=] is a [=service + worker registration=].
        3. -
        4. Set |registration| to [=this=]'s associated [=service worker registration=]. +
        5. Set |registration| to [=this=]'s [=PushManager/service worker registration=].
      7. @@ -1749,20 +1768,34 @@

        it MUST run the following steps.

          -
        1. Let |registration| be the service worker registration corresponding to the - push message. -
        2. -
        3. If |registration| is not found, abort these steps. +
        4. +

          + Let |subscription| be the active push subscription corresponding to the + push message. +

        5. -
        6. Let |subscription| be the active push subscription for |registration|. +
        7. +

          + Let |registration| be |subscription|'s [=push subscription/associated service worker + registration=]. +

        8. -
        9. Let |bytes| be null. +
        10. +

          + Let |bytes| be null. +

        11. -
        12. If the push message contains a payload: +
        13. +

          + If the push message contains a payload: +

            -
          1. Decrypt the push message's payload using the private key from the key pair - associated with |subscription| and the process described in [[RFC8291]]. Set |bytes| - to the resulting [=/byte sequence=]. +
          2. +

            + Decrypt the push message's payload using the private key from the key pair + associated with |subscription| and the process described in [[RFC8291]]. Set + |bytes| to the resulting [=/byte sequence=]. +

          3. @@ -1784,7 +1817,7 @@

            1. - Let |baseURL| be |registration|'s [=service worker registration/scope URL=]. + Let |baseURL| be |subscription|'s [=push subscription/scope=].

            2. @@ -1828,7 +1861,7 @@

            3. If |declarativeResult|'s [=declarative push message parser result/mutable=] - is true: + is true and |registration| is non-null:

              1. @@ -1862,6 +1895,11 @@

            4. +
            5. +

              + If |registration| is null, then abort these steps. +

              +
            6. Let |data| be a new {{PushMessageData}} object whose [=PushMessageData/bytes=] is From 1bc27048c029fdfff483ce534fe34f08624241ea Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Thu, 14 Aug 2025 12:21:29 +0200 Subject: [PATCH 3/3] account for scope corner case and slightly modernize pushManager getter --- index.html | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index acbe5fc..3039990 100644 --- a/index.html +++ b/index.html @@ -1097,7 +1097,12 @@

              ServiceWorkerRegistration includes PushManagerAttribute;

              - The pushManager attribute exposes a {{PushManager}}. + {{Window}} and {{ServiceWorkerRegistration}} objects have an associated {{PushManager}} + object. +

              +

              + The pushManager getter steps are to return [=this=]'s associated {{PushManager}} + object.

              On a {{Window}} object the {{PushManager}}'s [=PushManager/service worker registration=] is @@ -1184,10 +1189,14 @@

                +
              1. If |scope| is failure or is a [=/URL=] whose [=url/scheme=] is not "`https`", then + [=queue a global task=] on the [=networking task source=] using |global| to [=reject=] + |promise| with a {{"NotAllowedError"}} {{DOMException}}. +
              2. If the |options| argument has a {{PushSubscriptionOptionsInit/userVisibleOnly}} value set to `false` and the user agent requires it to be `true`, [=queue a global - task=] on the [=networking task source=] using |global| to [=reject=] |promise| - {{"NotAllowedError"}} {{DOMException}} + task=] on the [=networking task source=] using |global| to [=reject=] |promise| with a + {{"NotAllowedError"}} {{DOMException}}.
              3. If the |options| argument does not include a non-null value for the {{PushSubscriptionOptionsInit/applicationServerKey}} member, and the push