Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

push.js

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

push.js - npm Package Compare versions

Comparing version 0.0.11 to 0.0.12

README.md

92

karma.conf.js

@@ -5,64 +5,72 @@ // Karma configuration

module.exports = function(config) {
config.set({
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
coverageReporter: {
// specify a common output directory
dir: 'coverage',
reporters: [{
type: 'lcov',
subdir: '.'
}]
},
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
plugins: [
'karma-jasmine',
'karma-firefox-launcher',
'karma-mocha-reporter',
'karma-coverage'
],
plugins: [
'karma-jasmine',
'karma-firefox-launcher',
'karma-mocha-reporter',
'karma-coverage'
],
// list of files / patterns to load in the browser
files: [
'push.js',
'push_tests.js'
],
// list of files / patterns to load in the browser
files: [
'push.js',
'push_tests.js'
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'push.js': ['coverage']
},
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'push.js': ['coverage']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha', 'coverage'],
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha', 'coverage'],
// web server port
port: 9876,
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Firefox'],
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Firefox'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true
});
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true
});
};
{
"name": "push.js",
"version": "0.0.11",
"version": "0.0.12",
"description": "A compact, cross-browser solution for the Javascript Notifications API",

@@ -34,4 +34,5 @@ "main": "push.js",

"karma-mocha-reporter": "^1.3.0",
"uglify-js": "^2.6.2"
"uglify-js": "^2.6.2",
"coveralls": "^2.11.15"
}
}

@@ -11,3 +11,3 @@ var

TEST_SW = 'customServiceWorker.js',
TEST_SW_DEFAULT = "sw.js"
TEST_SW_DEFAULT = "serviceWorker.js"
NOOP = function () {}; // NO OPerator (empty function)

@@ -59,3 +59,2 @@

it('should update permission value if permission is granted and execute callback', function (done) {

@@ -73,2 +72,11 @@ spyOn(window.Notification, 'requestPermission').and.callFake(function (cb) {

it('should not request permission if permission is already granted', function () {
spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.GRANTED);
spyOn(window.Notification, 'requestPermission').and.callFake(function (cb) {
cb(Push.Permission.GRANTED);
});
Push.Permission.request();
Push.create(TEST_TITLE);
expect(window.Notification.requestPermission).not.toHaveBeenCalled();
});
});

@@ -130,3 +138,3 @@

it('should use "sw.js" as a service worker path if one is not specified', function(done) {
it('should use "serviceWorker.js" as a service worker path if one is not specified', function(done) {
var promise = Push.create(TEST_TITLE);

@@ -206,2 +214,16 @@ promise.then(function(wrapper) {

it('should close notifications on close callback', function (done) {
var promise;
promise = Push.create(TEST_TITLE, {
onClose: callback
});
expect(Push.count()).toBe(1);
promise.then(function(wrapper) {
var notification = wrapper.get();
notification.dispatchEvent(new Event('close'));
expect(Push.count()).toBe(0);
done();
});
});
it('should close notifications using wrapper', function (done) {

@@ -208,0 +230,0 @@ var promise;

@@ -16,3 +16,3 @@ /**

*
* Copyright (c) 2015 Tyler Nickerson
* Copyright (c) 2015-2017 Tyler Nickerson
*

@@ -145,2 +145,3 @@ * Permission is hereby granted, free of charge, to any person obtaining a copy

key;
for (key in notifications) {

@@ -161,2 +162,26 @@ if (notifications.hasOwnProperty(key)) {

prepareNotification = function (id, options) {
var wrapper;
/* Wrapper used to get/close notification later on */
wrapper = {
get: function () {
return notifications[id];
},
close: function () {
closeNotification(id);
}
};
/* Autoclose timeout */
if (options.timeout) {
setTimeout(function () {
wrapper.close();
}, options.timeout);
}
return wrapper;
},
/**

@@ -166,6 +191,4 @@ * Callback function for the 'create' method

*/
createCallback = function (title, options) {
createCallback = function (title, options, resolve) {
var notification,
wrapper,
id,
onClose;

@@ -177,7 +200,15 @@

/* Set the last service worker path for testing */
self.lastWorkerPath = options.serviceWorker || 'sw.js';
self.lastWorkerPath = options.serviceWorker || 'serviceWorker.js';
/* onClose event handler */
onClose = function (id) {
/* A bit redundant, but covers the cases when close() isn't explicitly called */
removeNotification(id);
if (isFunction(options.onClose)) {
options.onClose.call(this, notification);
}
};
/* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
if (w.Notification) {
try {

@@ -195,13 +226,48 @@ notification = new w.Notification(

if (w.navigator) {
w.navigator.serviceWorker.register(options.serviceWorker || 'sw.js');
/* Register ServiceWorker using lastWorkerPath */
w.navigator.serviceWorker.register(self.lastWorkerPath);
w.navigator.serviceWorker.ready.then(function(registration) {
var localData = {
id: currentId,
link: options.link,
origin: document.location.href,
onClick: (isFunction(options.onClick)) ? options.onClick.toString() : '',
onClose: (isFunction(options.onClose)) ? options.onClose.toString() : ''
};
if (typeof options.data !== 'undefined' && options.data !== null)
localData = Object.assign(localData, options.data);
/* Show the notification */
registration.showNotification(
title,
{
icon: options.icon,
body: options.body,
vibrate: options.vibrate,
tag: options.tag,
data: localData,
requireInteraction: options.requireInteraction
}
);
).then(function() {
var id;
/* Find the most recent notification and add it to the global array */
registration.getNotifications().then(function(notifications) {
id = addNotification(notifications[notifications.length - 1]);
/* Send an empty message so the ServiceWorker knows who the client is */
registration.active.postMessage('');
/* Listen for close requests from the ServiceWorker */
navigator.serviceWorker.addEventListener('message', function (event) {
var data = JSON.parse(event.data);
if (data.action === 'close' && Number.isInteger(data.id))
removeNotification(data.id);
});
resolve(prepareNotification(id, options));
});
});
});

@@ -250,46 +316,29 @@ }

/* Add it to the global array */
id = addNotification(notification);
if (typeof(notification) !== 'undefined') {
var id = addNotification(notification),
wrapper = prepareNotification(id, options);
/* Wrapper used to get/close notification later on */
wrapper = {
get: function () {
return notification;
},
/* Notification callbacks */
if (isFunction(options.onShow))
notification.addEventListener('show', options.onShow);
close: function () {
closeNotification(id);
}
};
if (isFunction(options.onError))
notification.addEventListener('error', options.onError);
/* Autoclose timeout */
if (options.timeout) {
setTimeout(function () {
wrapper.close();
}, options.timeout);
}
if (isFunction(options.onClick))
notification.addEventListener('click', options.onClick);
/* Notification callbacks */
if (isFunction(options.onShow))
notification.addEventListener('show', options.onShow);
notification.addEventListener('close', function() {
onClose(id);
});
if (isFunction(options.onError))
notification.addEventListener('error', options.onError);
notification.addEventListener('cancel', function() {
onClose(id);
});
if (isFunction(options.onClick))
notification.addEventListener('click', options.onClick);
onClose = function () {
/* A bit redundant, but covers the cases when close() isn't explicitly called */
removeNotification(id);
if (isFunction(options.onClose)) {
options.onClose.call(this);
}
/* Return the wrapper so the user can call close() */
resolve(wrapper);
}
notification.addEventListener('close', onClose);
notification.addEventListener('cancel', onClose);
/* Return the wrapper so the user can call close() */
return wrapper;
resolve({}); // By default, pass an empty wrapper
},

@@ -322,2 +371,3 @@

self.Permission.request = function (onGranted, onDenied) {
var existing = self.Permission.get();

@@ -348,4 +398,8 @@ /* Return if Push not supported */

/* Permissions already set */
if (existing !== self.Permission.DEFAULT) {
callback(existing);
}
/* Safari 6+, Chrome 23+ */
if (w.Notification && w.Notification.requestPermission) {
else if (w.Notification && w.Notification.requestPermission) {
Notification.requestPermission(callback);

@@ -394,3 +448,3 @@ }

} else if (navigator.mozNotification) {
permission = Permissions.GRANTED;
permission = Permission.GRANTED;

@@ -445,5 +499,6 @@ /* IE9+ */

* @param {Array} options
* @return {void}
* @return {Promise}
*/
self.create = function (title, options) {
var promiseCallback;

@@ -462,6 +517,6 @@ /* Fail if the browser is not supported */

if (!self.Permission.has()) {
return new Promise(function(resolve, reject) {
promiseCallback = function(resolve, reject) {
self.Permission.request(function() {
try {
resolve(createCallback(title, options));
createCallback(title, options, resolve);
} catch (e) {

@@ -473,13 +528,14 @@ reject(e);

});
});
};
} else {
return new Promise(function(resolve, reject) {
promiseCallback = function(resolve, reject) {
try {
resolve(createCallback(title, options));
createCallback(title, options, resolve);
} catch (e) {
reject(e);
}
});
};
}
return new Promise(promiseCallback);
};

@@ -494,5 +550,6 @@

key;
for (key in notifications) {
for (key in notifications)
count++;
}
return count;

@@ -532,8 +589,7 @@ },

self.clear = function () {
var i,
success = true;
for (key in notifications) {
var didClose = closeNotification(key);
success = success && didClose;
}
var success = true;
for (key in notifications)
success = success && closeNotification(key);
return success;

@@ -540,0 +596,0 @@ };

@@ -16,3 +16,3 @@ /**

*
* Copyright (c) 2015 Tyler Nickerson
* Copyright (c) 2015-2017 Tyler Nickerson
*

@@ -39,2 +39,2 @@ * Permission is hereby granted, free of charge, to any person obtaining a copy

*/
(function(global,factory){"use strict";if(typeof define==="function"&&define.amd){define(function(){return new(factory(global,global.document))})}else if(typeof module!=="undefined"&&module.exports){module.exports=new(factory(global,global.document))}else{global.Push=new(factory(global,global.document))}})(typeof window!=="undefined"?window:this,function(w,d){var Push=function(){var self=this,isUndefined=function(obj){return obj===undefined},isString=function(obj){return String(obj)===obj},isFunction=function(obj){return obj&&{}.toString.call(obj)==="[object Function]"},currentId=0,incompatibilityErrorMessage="PushError: push.js is incompatible with browser.",hasPermission=false,notifications={},lastWorkerPath=null,closeNotification=function(id){var errored=false,notification=notifications[id];if(typeof notification!=="undefined"){if(notification.close){notification.close()}else if(notification.cancel){notification.cancel()}else if(w.external&&w.external.msIsSiteMode){w.external.msSiteModeClearIconOverlay()}else{errored=true;throw new Error("Unable to close notification: unknown interface")}if(!errored){return removeNotification(id)}}return false},addNotification=function(notification){var id=currentId;notifications[id]=notification;currentId++;return id},removeNotification=function(id){var dict={},success=false,key;for(key in notifications){if(notifications.hasOwnProperty(key)){if(key!=id){dict[key]=notifications[key]}else{success=true}}}notifications=dict;return success},createCallback=function(title,options){var notification,wrapper,id,onClose;options=options||{};self.lastWorkerPath=options.serviceWorker||"sw.js";if(w.Notification){try{notification=new w.Notification(title,{icon:isString(options.icon)||isUndefined(options.icon)?options.icon:options.icon.x32,body:options.body,tag:options.tag,requireInteraction:options.requireInteraction})}catch(e){if(w.navigator){w.navigator.serviceWorker.register(options.serviceWorker||"sw.js");w.navigator.serviceWorker.ready.then(function(registration){registration.showNotification(title,{body:options.body,vibrate:options.vibrate,tag:options.tag,requireInteraction:options.requireInteraction})})}}}else if(w.webkitNotifications){notification=w.webkitNotifications.createNotification(options.icon,title,options.body);notification.show()}else if(navigator.mozNotification){notification=navigator.mozNotification.createNotification(title,options.body,options.icon);notification.show()}else if(w.external&&w.external.msIsSiteMode()){w.external.msSiteModeClearIconOverlay();w.external.msSiteModeSetIconOverlay(isString(options.icon)||isUndefined(options.icon)?options.icon:options.icon.x16,title);w.external.msSiteModeActivate();notification={}}else{throw new Error("Unable to create notification: unknown interface")}id=addNotification(notification);wrapper={get:function(){return notification},close:function(){closeNotification(id)}};if(options.timeout){setTimeout(function(){wrapper.close()},options.timeout)}if(isFunction(options.onShow))notification.addEventListener("show",options.onShow);if(isFunction(options.onError))notification.addEventListener("error",options.onError);if(isFunction(options.onClick))notification.addEventListener("click",options.onClick);onClose=function(){removeNotification(id);if(isFunction(options.onClose)){options.onClose.call(this)}};notification.addEventListener("close",onClose);notification.addEventListener("cancel",onClose);return wrapper},Permission={DEFAULT:"default",GRANTED:"granted",DENIED:"denied"},Permissions=[Permission.GRANTED,Permission.DEFAULT,Permission.DENIED];self.Permission=Permission;self.Permission.request=function(onGranted,onDenied){if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}callback=function(result){switch(result){case self.Permission.GRANTED:hasPermission=true;if(onGranted)onGranted();break;case self.Permission.DENIED:hasPermission=false;if(onDenied)onDenied();break}};if(w.Notification&&w.Notification.requestPermission){Notification.requestPermission(callback)}else if(w.webkitNotifications&&w.webkitNotifications.checkPermission){w.webkitNotifications.requestPermission(callback)}else{throw new Error(incompatibilityErrorMessage)}};self.Permission.has=function(){return hasPermission};self.Permission.get=function(){var permission;if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}if(w.Notification&&w.Notification.permissionLevel){permission=w.Notification.permissionLevel}else if(w.webkitNotifications&&w.webkitNotifications.checkPermission){permission=Permissions[w.webkitNotifications.checkPermission()]}else if(w.Notification&&w.Notification.permission){permission=w.Notification.permission}else if(navigator.mozNotification){permission=Permissions.GRANTED}else if(w.external&&w.external.msIsSiteMode()!==undefined){permission=w.external.msIsSiteMode()?Permission.GRANTED:Permission.DEFAULT}else{throw new Error(incompatibilityErrorMessage)}return permission};self.isSupported=function(){var isSupported=false;try{isSupported=!!(w.Notification||w.webkitNotifications||navigator.mozNotification||w.external&&w.external.msIsSiteMode()!==undefined)}catch(e){}return isSupported}();self.create=function(title,options){if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}if(!isString(title)){throw new Error("PushError: Title of notification must be a string")}if(!self.Permission.has()){return new Promise(function(resolve,reject){self.Permission.request(function(){try{resolve(createCallback(title,options))}catch(e){reject(e)}},function(){reject("Permission request declined")})})}else{return new Promise(function(resolve,reject){try{resolve(createCallback(title,options))}catch(e){reject(e)}})}};self.count=function(){var count=0,key;for(key in notifications){count++}return count},self.__lastWorkerPath=function(){return self.lastWorkerPath},self.close=function(tag){var key;for(key in notifications){notification=notifications[key];if(notification.tag===tag){return closeNotification(key)}}};self.clear=function(){var i,success=true;for(key in notifications){var didClose=closeNotification(key);success=success&&didClose}return success}};return Push});
(function(global,factory){"use strict";if(typeof define==="function"&&define.amd){define(function(){return new(factory(global,global.document))})}else if(typeof module!=="undefined"&&module.exports){module.exports=new(factory(global,global.document))}else{global.Push=new(factory(global,global.document))}})(typeof window!=="undefined"?window:this,function(w,d){var Push=function(){var self=this,isUndefined=function(obj){return obj===undefined},isString=function(obj){return String(obj)===obj},isFunction=function(obj){return obj&&{}.toString.call(obj)==="[object Function]"},currentId=0,incompatibilityErrorMessage="PushError: push.js is incompatible with browser.",hasPermission=false,notifications={},lastWorkerPath=null,closeNotification=function(id){var errored=false,notification=notifications[id];if(typeof notification!=="undefined"){if(notification.close){notification.close()}else if(notification.cancel){notification.cancel()}else if(w.external&&w.external.msIsSiteMode){w.external.msSiteModeClearIconOverlay()}else{errored=true;throw new Error("Unable to close notification: unknown interface")}if(!errored){return removeNotification(id)}}return false},addNotification=function(notification){var id=currentId;notifications[id]=notification;currentId++;return id},removeNotification=function(id){var dict={},success=false,key;for(key in notifications){if(notifications.hasOwnProperty(key)){if(key!=id){dict[key]=notifications[key]}else{success=true}}}notifications=dict;return success},prepareNotification=function(id,options){var wrapper;wrapper={get:function(){return notifications[id]},close:function(){closeNotification(id)}};if(options.timeout){setTimeout(function(){wrapper.close()},options.timeout)}return wrapper},createCallback=function(title,options,resolve){var notification,onClose;options=options||{};self.lastWorkerPath=options.serviceWorker||"serviceWorker.js";onClose=function(id){removeNotification(id);if(isFunction(options.onClose)){options.onClose.call(this,notification)}};if(w.Notification){try{notification=new w.Notification(title,{icon:isString(options.icon)||isUndefined(options.icon)?options.icon:options.icon.x32,body:options.body,tag:options.tag,requireInteraction:options.requireInteraction})}catch(e){if(w.navigator){w.navigator.serviceWorker.register(self.lastWorkerPath);w.navigator.serviceWorker.ready.then(function(registration){var localData={id:currentId,link:options.link,origin:document.location.href,onClick:isFunction(options.onClick)?options.onClick.toString():"",onClose:isFunction(options.onClose)?options.onClose.toString():""};if(typeof options.data!=="undefined"&&options.data!==null)localData=Object.assign(localData,options.data);registration.showNotification(title,{icon:options.icon,body:options.body,vibrate:options.vibrate,tag:options.tag,data:localData,requireInteraction:options.requireInteraction}).then(function(){var id;registration.getNotifications().then(function(notifications){id=addNotification(notifications[notifications.length-1]);registration.active.postMessage("");navigator.serviceWorker.addEventListener("message",function(event){var data=JSON.parse(event.data);if(data.action==="close"&&Number.isInteger(data.id))removeNotification(data.id)});resolve(prepareNotification(id,options))})})})}}}else if(w.webkitNotifications){notification=w.webkitNotifications.createNotification(options.icon,title,options.body);notification.show()}else if(navigator.mozNotification){notification=navigator.mozNotification.createNotification(title,options.body,options.icon);notification.show()}else if(w.external&&w.external.msIsSiteMode()){w.external.msSiteModeClearIconOverlay();w.external.msSiteModeSetIconOverlay(isString(options.icon)||isUndefined(options.icon)?options.icon:options.icon.x16,title);w.external.msSiteModeActivate();notification={}}else{throw new Error("Unable to create notification: unknown interface")}if(typeof notification!=="undefined"){var id=addNotification(notification),wrapper=prepareNotification(id,options);if(isFunction(options.onShow))notification.addEventListener("show",options.onShow);if(isFunction(options.onError))notification.addEventListener("error",options.onError);if(isFunction(options.onClick))notification.addEventListener("click",options.onClick);notification.addEventListener("close",function(){onClose(id)});notification.addEventListener("cancel",function(){onClose(id)});resolve(wrapper)}resolve({})},Permission={DEFAULT:"default",GRANTED:"granted",DENIED:"denied"},Permissions=[Permission.GRANTED,Permission.DEFAULT,Permission.DENIED];self.Permission=Permission;self.Permission.request=function(onGranted,onDenied){var existing=self.Permission.get();if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}callback=function(result){switch(result){case self.Permission.GRANTED:hasPermission=true;if(onGranted)onGranted();break;case self.Permission.DENIED:hasPermission=false;if(onDenied)onDenied();break}};if(existing!==self.Permission.DEFAULT){callback(existing)}else if(w.Notification&&w.Notification.requestPermission){Notification.requestPermission(callback)}else if(w.webkitNotifications&&w.webkitNotifications.checkPermission){w.webkitNotifications.requestPermission(callback)}else{throw new Error(incompatibilityErrorMessage)}};self.Permission.has=function(){return hasPermission};self.Permission.get=function(){var permission;if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}if(w.Notification&&w.Notification.permissionLevel){permission=w.Notification.permissionLevel}else if(w.webkitNotifications&&w.webkitNotifications.checkPermission){permission=Permissions[w.webkitNotifications.checkPermission()]}else if(w.Notification&&w.Notification.permission){permission=w.Notification.permission}else if(navigator.mozNotification){permission=Permission.GRANTED}else if(w.external&&w.external.msIsSiteMode()!==undefined){permission=w.external.msIsSiteMode()?Permission.GRANTED:Permission.DEFAULT}else{throw new Error(incompatibilityErrorMessage)}return permission};self.isSupported=function(){var isSupported=false;try{isSupported=!!(w.Notification||w.webkitNotifications||navigator.mozNotification||w.external&&w.external.msIsSiteMode()!==undefined)}catch(e){}return isSupported}();self.create=function(title,options){var promiseCallback;if(!self.isSupported){throw new Error(incompatibilityErrorMessage)}if(!isString(title)){throw new Error("PushError: Title of notification must be a string")}if(!self.Permission.has()){promiseCallback=function(resolve,reject){self.Permission.request(function(){try{createCallback(title,options,resolve)}catch(e){reject(e)}},function(){reject("Permission request declined")})}}else{promiseCallback=function(resolve,reject){try{createCallback(title,options,resolve)}catch(e){reject(e)}}}return new Promise(promiseCallback)};self.count=function(){var count=0,key;for(key in notifications)count++;return count},self.__lastWorkerPath=function(){return self.lastWorkerPath},self.close=function(tag){var key;for(key in notifications){notification=notifications[key];if(notification.tag===tag){return closeNotification(key)}}};self.clear=function(){var success=true;for(key in notifications)success=success&&closeNotification(key);return success}};return Push});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc