From 810e0130eb85840e24d7e59320f551e0aef0f7a5 Mon Sep 17 00:00:00 2001
From: BordedDev <>
Date: Sat, 1 Feb 2025 22:22:40 +0100
Subject: [PATCH] Initial commit

---
 README.md         | 121 ++++++++++++++++++++++++++++++++++++++++++++++
 example.html      |  42 ++++++++++++++++
 service-worker.js |  31 ++++++++++++
 3 files changed, 194 insertions(+)
 create mode 100644 README.md
 create mode 100644 example.html
 create mode 100644 service-worker.js

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c7649d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,121 @@
+# Notes from attempting to setup web push notifications
+
+## Files included
+
+- `service-worker.js` - Service worker file, it listens for push events and displays notifications
+- `example.html` - Main file, it will immediately ask for permission to send notifications and log the subscription object
+  to the console. You might have to refresh after giving permissions to see the subscription object.
+
+## Steps to setup
+
+### Browser setup
+
+1. run `npx serve`
+2. Open the browser and go to `http://localhost:3000`
+3. Open the console and you should see the subscription object
+
+### Server setup
+
+(Or you can use [web-push](https://www.npmjs.com/package/web-push) package)
+
+#### Generate VAPID keys
+
+1. Run `openssl ecparam -name prime256v1 -genkey -noout -out private.pem`
+    1. You might also want to generate a PKCS8 key for the private key
+        - Run `openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem`
+2. Run `openssl ec -in private.pem -pubout -out public.pem`
+
+Now you can your favourite http request library to send a POST request to the url returned by the
+`pushManager.subscribe` method.
+
+When you do you should include the following header (and no body!):
+
+```http
+TTL: 60
+Authorization: `Bearer ${jwtToken}`
+Crypto-Key: `p256ecdsa=${publicKey}`
+Topic: `An optional topic`
+Urgency: `An optional urgency`
+```
+
+The `jwtToken` is a JWT token that you can generate using the private key you generated earlier. The payload should
+include the `aud` field which should be the url returned by the `pushManager.subscribe` method.
+For example:
+
+```javascript
+const {default: fs} = await import("fs");
+const {default: crypto} = await import("crypto");
+const {default: jwt} = await import("jsonwebtoken");
+
+const privateKeyBuffer = fs.readFileSync('vapid_private.pem');
+const privateKey = crypto.createPrivateKey(privateKeyBuffer);
+
+const payload = {
+    aud: 'https://fcm.googleapis.com',
+    sub: 'mailto:example@example.com',
+};
+
+const jwtToken = jwt.sign(payload, privateKey, {algorithm: 'ES256', expiresIn: '1h'});
+console.log(jwtToken);
+```
+
+It has to use the ES256 algorithm and the `exp` field should be set to a timestamp in the future.
+
+The `publicKey` is the public key you generated earlier however it should be encoded as JWK and then base64 url encoded.
+You can use the following code to do that:
+
+```javascript
+const {default: fs} = await import("fs");
+const {default: crypto} = await import("crypto");
+
+const publicKeyBuffer = fs.readFileSync('public.pem');
+const publicKey = crypto.createPublicKey(publicKeyBuffer);
+
+const jwk = {
+    kty: 'EC',
+    crv: 'P-256',
+    x: publicKey.export({format: 'jwk'}).x,
+    y: publicKey.export({format: 'jwk'}).y,
+};
+
+const base64UrlEncode = (str) => {
+    return str.toString('base64')
+        .replace(/\+/g, '-')
+        .replace(/\//g, '_')
+        .replace(/=/g, '');
+};
+
+const base64UrlEncodedPublicKey = base64UrlEncode(publicKey.export({format: 'jwk'}).x) + '.' + base64UrlEncode(publicKey.export({format: 'jwk'}).y);
+
+console.log(base64UrlEncodedPublicKey);
+```
+
+This should be enough to get you started with web push notifications.
+
+### Notes
+
+- The `pushManager.subscribe` method may require depending on the browser a `userVisibleOnly` field or `applicationServerKey` field. The `userVisibleOnly`
+  field should be set to `true` and the `applicationServerKey` field should be set to the public key you generated earlier.
+
+- For sending a payload additional changes need to happen, you can't just send a payload with the request. 
+  - For it to work you will have to update the headers:
+    - Encoding the payload, it's complex and I haven't done it yet but you can look at the [Mozilla Blog](https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/) for more information.
+    - The `Crypto-Key` header should be set to 3 values:
+      - `p256ecdsa=${publicKey}` - The public key you generated earlier
+      - `dh=${publicHalfKey}` - The first half of the shared secret when the payload is encoded
+      - `keyid=p256dh` - The key id
+    - The `Encryption` header should be set to 2 values:
+      - `salt=${salt}` - The salt used to generate the shared secret
+      - `keyid=p256dh` - The key id
+    - The `Content-Encoding` header should be set to `aesgcm`
+
+For more information you can check the useful links below.
+
+### Useful links
+
+- [The Web Push Protocol ](https://web.dev/articles/push-notifications-web-push-protocol)
+- [Web Push Book](https://web-push-book.gauntface.com/)
+- [JWT.io](https://jwt.io/)
+- [WebPushDataTestPage](https://mozilla-services.github.io/WebPushDataTestPage/)
+- [Mozilla Blog](https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/)
+- [Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription)
\ No newline at end of file
diff --git a/example.html b/example.html
new file mode 100644
index 0000000..dfa9beb
--- /dev/null
+++ b/example.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Game Site</title>
+    <script>
+        this.onpush = (event) => {
+            console.log(event.data);
+            // From here we can write the data to IndexedDB, send it to any open
+            // windows, display a notification, etc.
+        };
+
+        navigator.serviceWorker
+            .register("service-worker.js")
+            .then((serviceWorkerRegistration) => {
+                serviceWorkerRegistration.pushManager.subscribe().then(
+                    (pushSubscription) => {
+                        const subscriptionObject = {
+                            endpoint: pushSubscription.endpoint,
+                            keys: {
+                                p256dh: pushSubscription.getKey('p256dh'),
+                                auth: pushSubscription.getKey('auth'),
+                            },
+                            encoding: PushManager.supportedContentEncodings,
+                            /* other app-specific data, such as user identity */
+                        };
+                        console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject);
+                        // The push subscription details needed by the application
+                        // server are now available, and can be sent to it using,
+                        // for example, the fetch() API.
+                    },
+                    (error) => {
+                        console.error(error);
+                    },
+                );
+            });
+
+    </script>
+</head>
+<body>
+</body>
+</html>
\ No newline at end of file
diff --git a/service-worker.js b/service-worker.js
new file mode 100644
index 0000000..72acde4
--- /dev/null
+++ b/service-worker.js
@@ -0,0 +1,31 @@
+
+self.addEventListener("install", (event) => {
+  console.log("Service worker installed");
+});
+
+self.addEventListener("push",  (event) => {
+  if (!(self.Notification && self.Notification.permission === "granted")) {
+    return;
+  }
+  console.log("Received a push message", event);
+
+  const data = event.data?.json() ?? {};
+  const title = data.title || "Something Has Happened";
+  const message =
+    data.message || "Here's something you might want to check out.";
+  const icon = "images/new-notification.png";
+
+  event.waitUntil(self.registration.showNotification(title, {
+    body: message,
+    tag: "simple-push-demo-notification",
+    icon,
+  }));
+
+});
+
+self.addEventListener("notificationclick", (event) => {
+    console.log("Notification click Received.", event);
+    event.notification.close();
+    event.waitUntil(clients.openWindow(
+      "https://molodetz.nl",));
+});
\ No newline at end of file