├── .gitignore
├── README.md
├── handler.js
├── package.json
├── serverless.yml
├── static
├── index.html
├── index.js
└── service-worker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # package directories
2 | node_modules
3 | jspm_packages
4 |
5 | # Serverless directories
6 | .serverless
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # serverless-web-push
2 |
3 | Serverless implementation of the [Web API (Push Payload) Demo](https://serviceworke.rs/push-payload_demo.html) using **Serverless framework** and **AWS Lambda**
4 |
5 | ## Requirements
6 |
7 | - Node.js
8 | - AWS Account
9 |
10 | ## Setup
11 |
12 | - Install and authenticate [aws-cli](https://aws.amazon.com/cli/)
13 | - Install and configure [Serverless](https://serverless.com/)
14 |
15 | ## Deployment
16 |
17 | - Clone this repo
18 | - Move to this repo's folder
19 | - Execute
20 | ```
21 | sls deploy
22 | ```
23 | - Open `index.html` path
24 |
25 | ## Demo
26 |
27 | This lambda can currently be seen in action here: [DEMO](https://x79n1d1ajl.execute-api.us-east-1.amazonaws.com/dev/index.html)
--------------------------------------------------------------------------------
/handler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const mime = require('mime-types')
5 | const webPush = require('web-push');
6 |
7 | let subscriptions = []
8 |
9 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
10 | console.log("You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " +
11 | "environment variables. You can use the following ones:");
12 | console.log(webPush.generateVAPIDKeys());
13 | }
14 |
15 | webPush.setVapidDetails(
16 | process.env.DOMAIN,
17 | process.env.VAPID_PUBLIC_KEY,
18 | process.env.VAPID_PRIVATE_KEY
19 | );
20 |
21 | function response(statusCode, body, file) {
22 | let payload = {
23 | statusCode,
24 | body: typeof (body) === 'string' ? body : JSON.stringify(body, null, 2),
25 | }
26 | if (file) {
27 | payload.headers = { 'content-type': mime.contentType(file) }
28 | }
29 | console.log('RESPOND', payload)
30 | return payload
31 | }
32 |
33 | module.exports.vapidPublicKey = async () => {
34 | return response(200, process.env.VAPID_PUBLIC_KEY);
35 | }
36 |
37 | module.exports.register = async (event, context) => {
38 | // Save the registered users subscriptions (event.body)
39 | subscriptions.push(JSON.parse(event.body))
40 | return response(201, event);
41 | }
42 |
43 | function send(subscriptions, payload, options, delay) {
44 | console.log('send', subscriptions, payload, options, delay)
45 |
46 | return new Promise((success) => {
47 | setTimeout(() => {
48 |
49 | Promise.all(subscriptions.map((each_subscription) => {
50 | return webPush.sendNotification(each_subscription, payload, options)
51 | }))
52 | .then(function () {
53 | success(response(201, {}))
54 | }).catch(function (error) {
55 | console.log('ERROR>', error);
56 | success(response(500, { error: error }))
57 | })
58 |
59 | }, 1000 * parseInt(delay))
60 | })
61 | }
62 |
63 | module.exports.sendNotification = async (event) => {
64 | console.log('register event', JSON.stringify(event, null, 2))
65 | let body = JSON.parse(event.body)
66 | const subscription = body.subscription;
67 | const payload = body.payload;
68 | const delay = body.delay;
69 | const options = {
70 | TTL: body.ttl | 5
71 | };
72 |
73 | return await send([subscription], payload, options, delay)
74 | }
75 |
76 | module.exports.registerOrSendToAll = async (event) => {
77 | // these two functions (register and SendtoAll) are in the same
78 | // handler, so that they share the same memory and we don't have
79 | // to setup a database for storing the subscriptions
80 | // this works for this test, but subscriptions will be deleted
81 | // when the lambda cointainer dies
82 | if (event.resource === '/register') {
83 | subscriptions.push(JSON.parse(event.body).subscription)
84 | return response(201, event);
85 | } else {
86 | console.log('register event', JSON.stringify(event, null, 2))
87 | let body = JSON.parse(event.body)
88 | console.log('got body', body)
89 | const payload = body.payload;
90 | const delay = body.delay;
91 | const options = {
92 | TTL: body.ttl | 5
93 | };
94 | return await send(subscriptions, payload, options, delay)
95 | }
96 |
97 | }
98 |
99 | module.exports.statics = async (event) => {
100 | // Serve static files from lambda (only for simplicity of this example)
101 | var file = fs.readFileSync(`./static${event.resource}`)
102 | return await response(200, file.toString(), event.resource.split('/')[1])
103 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "mime-types": "^2.1.21",
4 | "web-push": "^3.3.3"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: serverless-web-push # NOTE: update this with your service name
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs8.10
6 | stage: dev
7 | region: us-east-1
8 | environment:
9 | DOMAIN: https://remittir.com/ # no need to change this domain for testing it
10 | VAPID_PUBLIC_KEY: BLpTsaEAy-BGQnkZ1DeFYYNS6EH1gWP-cP49n9NmbWtjkSVMJQjj-wVI0tapfsK7Ju9r0VQz7jpE9kf8BETAdns
11 | VAPID_PRIVATE_KEY: LtWhacMtRs63fhABUUMLOynMRTKTffIf7oQuRpwChFc
12 |
13 | functions:
14 | vapidPublicKey:
15 | handler: handler.vapidPublicKey
16 | events:
17 | - http:
18 | path: vapidPublicKey
19 | method: get
20 |
21 | sendNotification:
22 | handler: handler.sendNotification
23 | events:
24 | - http:
25 | path: sendNotification
26 | method: post
27 |
28 | registerOrSendToAll:
29 | handler: handler.registerOrSendToAll
30 | events:
31 | - http:
32 | path: register
33 | method: post
34 | - http:
35 | path: sendToAll
36 | method: post
37 |
38 | statics:
39 | handler: handler.statics
40 | events:
41 | - http:
42 | path: index.html
43 | method: get
44 | - http:
45 | path: index.js
46 | method: get
47 | - http:
48 | path: service-worker.js
49 | method: get
50 |
51 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | telepathy
10 |
11 |
12 |
13 |
14 |
17 |
18 |
Send notification to this subscription
19 |
20 |
21 |
22 |
Send notification to all subscriptions
23 | (Open this same URL in many computers to really see it in action)
24 |
25 |
26 |
27 |
28 |
Source Code:
29 |
Github
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/static/index.js:
--------------------------------------------------------------------------------
1 | navigator.serviceWorker.register('service-worker.js');
2 |
3 | /**
4 | * urlBase64ToUint8Array
5 | *
6 | * @param {string} base64String a public vavid key
7 | */
8 | function urlBase64ToUint8Array(base64String) {
9 | var padding = '='.repeat((4 - base64String.length % 4) % 4);
10 | var base64 = (base64String + padding)
11 | .replace(/\-/g, '+')
12 | .replace(/_/g, '/');
13 |
14 | var rawData = window.atob(base64);
15 | var outputArray = new Uint8Array(rawData.length);
16 |
17 | for (var i = 0; i < rawData.length; ++i) {
18 | outputArray[i] = rawData.charCodeAt(i);
19 | }
20 | return outputArray;
21 | }
22 |
23 | navigator.serviceWorker.ready
24 | .then(function (registration) {
25 | return registration.pushManager.getSubscription()
26 | .then(async function (subscription) {
27 | if (subscription) {
28 | console.log('got subscription!', subscription)
29 | return subscription;
30 | }
31 | const response = await fetch('./vapidPublicKey');
32 | const vapidPublicKey = await response.text();
33 | console.log('decoding:', vapidPublicKey)
34 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
35 | console.log('got vapidPublicKey', vapidPublicKey, convertedVapidKey)
36 |
37 | return registration.pushManager.subscribe({
38 | userVisibleOnly: true,
39 | applicationServerKey: convertedVapidKey
40 | });
41 | });
42 |
43 | }).then(function (subscription) {
44 | console.log('register!', subscription)
45 | fetch('./register', {
46 | method: 'post',
47 | headers: {
48 | 'Content-type': 'application/json'
49 | },
50 | body: JSON.stringify({
51 | subscription: subscription
52 | }),
53 | });
54 |
55 | document.getElementById('doIt').onclick = function () {
56 | const payload = 'Hola!'
57 | const delay = '5'
58 | const ttl = '5'
59 | fetch('./sendNotification', {
60 | method: 'post',
61 | headers: {
62 | 'Content-type': 'application/json'
63 | },
64 | body: JSON.stringify({
65 | subscription: subscription,
66 | payload: payload,
67 | delay: delay,
68 | ttl: ttl,
69 | }),
70 | });
71 | };
72 |
73 | document.getElementById('sendToAll').onclick = function () {
74 | fetch('./sendToAll', {
75 | method: 'post',
76 | headers: {
77 | 'Content-type': 'application/json'
78 | },
79 | body: JSON.stringify({
80 | payload: 'SEND TEXT TO ALL',
81 | delay: 0,
82 | }),
83 | });
84 | };
85 |
86 | });
--------------------------------------------------------------------------------
/static/service-worker.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('push', function (event) {
2 | const payload = event.data ? event.data.text() : 'no payload';
3 | event.waitUntil(
4 | self.registration.showNotification('ServiceWorker Cookbook', {
5 | body: payload,
6 | })
7 | );
8 | });
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | agent-base@^4.1.0:
6 | version "4.2.1"
7 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
8 | integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
9 | dependencies:
10 | es6-promisify "^5.0.0"
11 |
12 | asn1.js@^5.0.0:
13 | version "5.0.1"
14 | resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.0.1.tgz#7668b56416953f0ce3421adbb3893ace59c96f59"
15 | integrity sha512-aO8EaEgbgqq77IEw+1jfx5c9zTbzvkfuRBuZsSsPnTHMkmd5AI4J6OtITLZFa381jReeaQL67J0GBTUu0+ZTVw==
16 | dependencies:
17 | bn.js "^4.0.0"
18 | inherits "^2.0.1"
19 | minimalistic-assert "^1.0.0"
20 |
21 | bn.js@^4.0.0:
22 | version "4.11.8"
23 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
24 | integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
25 |
26 | buffer-equal-constant-time@1.0.1:
27 | version "1.0.1"
28 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
29 | integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
30 |
31 | debug@^3.1.0:
32 | version "3.2.6"
33 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
34 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
35 | dependencies:
36 | ms "^2.1.1"
37 |
38 | ecdsa-sig-formatter@1.0.10:
39 | version "1.0.10"
40 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3"
41 | integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=
42 | dependencies:
43 | safe-buffer "^5.0.1"
44 |
45 | es6-promise@^4.0.3:
46 | version "4.2.5"
47 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
48 | integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
49 |
50 | es6-promisify@^5.0.0:
51 | version "5.0.0"
52 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
53 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
54 | dependencies:
55 | es6-promise "^4.0.3"
56 |
57 | http_ece@1.0.5:
58 | version "1.0.5"
59 | resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.0.5.tgz#b60660faaf14215102d1493ea720dcd92b53372f"
60 | integrity sha1-tgZg+q8UIVEC0Uk+pyDc2StTNy8=
61 | dependencies:
62 | urlsafe-base64 "~1.0.0"
63 |
64 | https-proxy-agent@^2.2.1:
65 | version "2.2.1"
66 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
67 | integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
68 | dependencies:
69 | agent-base "^4.1.0"
70 | debug "^3.1.0"
71 |
72 | inherits@^2.0.1:
73 | version "2.0.3"
74 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
75 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
76 |
77 | jwa@^1.1.5:
78 | version "1.1.6"
79 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6"
80 | integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==
81 | dependencies:
82 | buffer-equal-constant-time "1.0.1"
83 | ecdsa-sig-formatter "1.0.10"
84 | safe-buffer "^5.0.1"
85 |
86 | jws@^3.1.3:
87 | version "3.1.5"
88 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f"
89 | integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==
90 | dependencies:
91 | jwa "^1.1.5"
92 | safe-buffer "^5.0.1"
93 |
94 | mime-db@~1.37.0:
95 | version "1.37.0"
96 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
97 | integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
98 |
99 | mime-types@^2.1.21:
100 | version "2.1.21"
101 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
102 | integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
103 | dependencies:
104 | mime-db "~1.37.0"
105 |
106 | minimalistic-assert@^1.0.0:
107 | version "1.0.1"
108 | resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
109 | integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
110 |
111 | minimist@^1.2.0:
112 | version "1.2.0"
113 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
114 | integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
115 |
116 | ms@^2.1.1:
117 | version "2.1.1"
118 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
119 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
120 |
121 | safe-buffer@^5.0.1:
122 | version "5.1.2"
123 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
124 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
125 |
126 | urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
127 | version "1.0.0"
128 | resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
129 | integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
130 |
131 | web-push@^3.3.3:
132 | version "3.3.3"
133 | resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.3.3.tgz#8dc7c578dd1243ceb5a8377389424e87ea9b15cc"
134 | integrity sha512-Om4CNZpyzHP3AtGZpbBavCO7I9oCS9CFY2VDfTj/cFx2gm+mAtyK2OlKd6qu9pwCdZTyYanUiyhT0JSrs0ypHQ==
135 | dependencies:
136 | asn1.js "^5.0.0"
137 | http_ece "1.0.5"
138 | https-proxy-agent "^2.2.1"
139 | jws "^3.1.3"
140 | minimist "^1.2.0"
141 | urlsafe-base64 "^1.0.0"
142 |
--------------------------------------------------------------------------------