├── .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 | --------------------------------------------------------------------------------