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

@microsoft/omnichannel-chat-sdk

Package Overview
Dependencies
Maintainers
4
Versions
347
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@microsoft/omnichannel-chat-sdk - npm Package Compare versions

Comparing version 0.1.1-main.55b031c to 0.1.1-main.67e5368

lib/api/createVoiceVideoCalling.d.ts

16

CHANGELOG.md

@@ -5,5 +5,21 @@ # Changelog

## [Unreleased]
### Added
- React Native sample app using Omnichannel Chat SDK with [react-native-gifted-chat](https://github.com/FaridSafi/react-native-gifted-chat)
- Escalation to Voice & View support (Web Only)
- React sample app using Omnichannel Chat SDK with [BotFramework-WebChat](https://github.com/microsoft/BotFramework-WebChat)
### Changed
- Uptake [@microsoft/ocsdk@0.1.1](https://www.npmjs.com/package/@microsoft/ocsdk/v/0.1.1)
- Uptake [@microsoft/omnichannel-ic3core@0.1.1](https://www.npmjs.com/package/@microsoft/omnichannel-ic3core/v/0.1.1)
- Uptake [jest@26.6.3](https://www.npmjs.com/package/jest/v/26.6.3)
- Uptake [ts-jest@26.5.1](https://www.npmjs.com/package/ts-jest/v/26.5.1)
### Fixed
- onAgentEndSession triggered on accept voice & video call
### Security
- Fix eslint errors
## [0.1.0] - 2020-10-26
### Added
- Initial release of Omnichannel Chat SDK v0.1.0

4

lib/core/IChatConfig.d.ts
export default interface IChatConfig {
DataMaskingInfo: any;
LiveChatConfigAuthSettings: any;
DataMaskingInfo: unknown;
LiveChatConfigAuthSettings: unknown;
LiveWSAndLiveChatEngJoin: any;
}
"use strict";
/* eslint-disable @typescript-eslint/no-explicit-any */
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IChatToken.js.map

@@ -8,8 +8,12 @@ import IChatConfig from "./core/IChatConfig";

import IFileMetadata from "@microsoft/omnichannel-ic3core/lib/model/IFileMetadata";
import ILiveChatContext from "./core/ILiveChatContext";
import IMessage from "@microsoft/omnichannel-ic3core/lib/model/IMessage";
import IOmnichannelConfig from "./core/IOmnichannelConfig";
import IRawMessage from "@microsoft/omnichannel-ic3core/lib/model/IRawMessage";
import IRawThread from "@microsoft/omnichannel-ic3core/lib/interfaces/IRawThread";
import IStartChatOptionalParams from "./core/IStartChatOptionalParams";
declare class OmnichannelChatSDK {
OCSDKProvider: any;
IC3SDKProvider: any;
private debug;
OCSDKProvider: unknown;
IC3SDKProvider: unknown;
OCClient: any;

@@ -27,8 +31,9 @@ IC3Client: any;

private conversation;
private debug;
private callingOption;
constructor(omnichannelConfig: IOmnichannelConfig, chatSDKConfig?: IChatSDKConfig);
setDebug(flag: boolean): void;
initialize(): Promise<IChatConfig>;
startChat(optionalParams?: IStartChatOptionalParams): Promise<any>;
endChat(): Promise<any>;
getCurrentLiveChatContext(): Promise<{}>;
startChat(optionalParams?: IStartChatOptionalParams): Promise<void>;
endChat(): Promise<void>;
getCurrentLiveChatContext(): Promise<ILiveChatContext | {}>;
/**

@@ -39,11 +44,12 @@ * Gets PreChat Survey.

getPreChatSurvey(parse?: boolean): Promise<any>;
getLiveChatConfig(cached?: boolean): Promise<any>;
getLiveChatConfig(cached?: boolean): Promise<IChatConfig>;
getChatToken(cached?: boolean): Promise<IChatToken>;
getMessages(): Promise<import("@microsoft/omnichannel-ic3core/lib/model/IMessage").default[] | undefined>;
getMessages(): Promise<IMessage[] | undefined>;
getDataMaskingRules(): Promise<any>;
sendMessage(message: IChatSDKMessage): Promise<void>;
onNewMessage(onNewMessageCallback: CallableFunction): void;
sendTypingEvent(): Promise<any>;
sendTypingEvent(): Promise<void>;
onTypingEvent(onTypingEventCallback: CallableFunction): Promise<void>;
onAgentEndSession(onAgentEndSessionCallback: (message: IRawThread) => void): Promise<void>;
uploadFileAttachment(fileInfo: IFileInfo): Promise<any>;
uploadFileAttachment(fileInfo: IFileInfo): Promise<IRawMessage>;
downloadFileAttachment(fileMetadata: IFileMetadata): Promise<Blob>;

@@ -53,3 +59,3 @@ emailLiveChatTranscript(body: IChatTranscriptBody): Promise<any>;

createChatAdapter(protocol?: string): Promise<unknown>;
setDebug(flag: boolean): void;
getVoiceVideoCalling(params?: any): Promise<any>;
private getIC3Client;

@@ -56,0 +62,0 @@ private getChatConfig;

"use strict";
/* eslint-disable @typescript-eslint/no-non-null-assertion */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

@@ -54,2 +55,5 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }

var SDKConfigValidators_1 = require("./validators/SDKConfigValidators");
var WebUtils_1 = require("./utils/WebUtils");
var createVoiceVideoCalling_1 = require("./api/createVoiceVideoCalling");
var CallingOptionsOptionSetNumber_1 = require("./core/CallingOptionsOptionSetNumber");
var OmnichannelChatSDK = /** @class */ (function () {

@@ -61,2 +65,4 @@ function OmnichannelChatSDK(omnichannelConfig, chatSDKConfig) {

this.conversation = null;
this.callingOption = CallingOptionsOptionSetNumber_1.default.NoCalling;
this.debug = false;
this.omnichannelConfig = omnichannelConfig;

@@ -70,6 +76,9 @@ this.chatSDKConfig = chatSDKConfig;

this.preChatSurvey = null;
this.debug = false;
OmnichannelConfigValidator_1.default(omnichannelConfig);
SDKConfigValidators_1.default(chatSDKConfig);
}
/* istanbul ignore next */
OmnichannelChatSDK.prototype.setDebug = function (flag) {
this.debug = flag;
};
OmnichannelChatSDK.prototype.initialize = function () {

@@ -85,3 +94,3 @@ return __awaiter(this, void 0, void 0, function () {

case 1:
_a.OCClient = _c.sent();
_a.OCClient = _c.sent(); // eslint-disable-line @typescript-eslint/no-explicit-any
_b = this;

@@ -283,2 +292,9 @@ return [4 /*yield*/, this.getIC3Client()];

};
OmnichannelChatSDK.prototype.getDataMaskingRules = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, this.dataMaskingRules];
});
});
};
OmnichannelChatSDK.prototype.sendMessage = function (message) {

@@ -295,3 +311,3 @@ return __awaiter(this, void 0, void 0, function () {

match = void 0;
while (match = regex.exec(content)) {
while (match = regex.exec(content)) { // eslint-disable-line no-cond-assign
replaceStr = match[0].replace(/./g, maskingCharacter);

@@ -392,3 +408,9 @@ content = content.replace(match[0], replaceStr);

return __generator(this, function (_b) {
(_a = this.conversation) === null || _a === void 0 ? void 0 : _a.registerOnThreadUpdate(onAgentEndSessionCallback);
(_a = this.conversation) === null || _a === void 0 ? void 0 : _a.registerOnThreadUpdate(function (message) {
var members = message.members;
// Agent ending conversation would have 1 member left in the chat thread
if (members.length === 1) {
onAgentEndSessionCallback(message);
}
});
return [2 /*return*/];

@@ -476,3 +498,2 @@ });

return __awaiter(this, void 0, void 0, function () {
var scriptElement;
var _this = this;

@@ -486,31 +507,104 @@ return __generator(this, function (_a) {

}
scriptElement = document.createElement('script');
scriptElement.setAttribute('src', libraries_1.default.getIC3AdapterCDNUrl());
document.head.appendChild(scriptElement);
return [2 /*return*/, new Promise(function (resolve, reject) {
scriptElement.addEventListener('load', function () {
_this.debug && console.debug('IC3Adapter loaded!');
var adapterConfig = {
chatToken: _this.chatToken,
userDisplayName: 'Customer',
userId: 'teamsvisitor',
sdkURL: libraries_1.default.getIC3ClientCDNUrl()
};
var adapter = new window.Microsoft.BotFramework.WebChat.IC3Adapter(adapterConfig);
resolve(adapter);
return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () {
var ic3AdapterCDNUrl;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
ic3AdapterCDNUrl = libraries_1.default.getIC3AdapterCDNUrl();
return [4 /*yield*/, WebUtils_1.loadScript(ic3AdapterCDNUrl, function () {
/* istanbul ignore next */
_this.debug && console.debug('IC3Adapter loaded!');
var adapterConfig = {
chatToken: _this.chatToken,
userDisplayName: 'Customer',
userId: 'teamsvisitor',
sdkURL: libraries_1.default.getIC3ClientCDNUrl()
};
var adapter = new window.Microsoft.BotFramework.WebChat.IC3Adapter(adapterConfig);
resolve(adapter);
}, function () {
reject('Failed to load IC3Adapter');
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
scriptElement.addEventListener('error', function () {
reject("Failed to load IC3Adapter");
});
})];
}); })];
});
});
};
/* istanbul ignore next */
OmnichannelChatSDK.prototype.setDebug = function (flag) {
this.debug = flag;
OmnichannelChatSDK.prototype.getVoiceVideoCalling = function (params) {
if (params === void 0) { params = {}; }
return __awaiter(this, void 0, void 0, function () {
var chatConfig, liveWSAndLiveChatEngJoin, msdyn_widgetsnippet, widgetSnippetSourceRegex, result;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (platform_1.default.isNode() || platform_1.default.isReactNative()) {
return [2 /*return*/, Promise.reject('VoiceVideoCalling is only supported on browser')];
}
if (this.callingOption.toString() === CallingOptionsOptionSetNumber_1.default.NoCalling.toString()) {
return [2 /*return*/, Promise.reject('Voice and video call is not enabled')];
}
return [4 /*yield*/, this.getChatConfig()];
case 1:
chatConfig = _a.sent();
liveWSAndLiveChatEngJoin = chatConfig.LiveWSAndLiveChatEngJoin;
msdyn_widgetsnippet = liveWSAndLiveChatEngJoin.msdyn_widgetsnippet;
widgetSnippetSourceRegex = new RegExp("src=\"(https:\\/\\/[\\w-.]+)[\\w-.\\/]+\"");
result = msdyn_widgetsnippet.match(widgetSnippetSourceRegex);
if (result && result.length) {
return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () {
var spoolSDKCDNUrl, LiveChatWidgetLibCDNUrl;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
spoolSDKCDNUrl = result[1] + "/livechatwidget/WebChatControl/lib/spool-sdk/sdk.bundle.js";
return [4 /*yield*/, WebUtils_1.loadScript(spoolSDKCDNUrl, function () {
/* istanbul ignore next */
_this.debug && console.debug(spoolSDKCDNUrl + " loaded!");
}, function () {
reject('Failed to load SpoolSDK');
})];
case 1:
_a.sent();
LiveChatWidgetLibCDNUrl = result[1] + "/livechatwidget/WebChatControl/lib/LiveChatWidgetLibs.min.js";
return [4 /*yield*/, WebUtils_1.loadScript(LiveChatWidgetLibCDNUrl, function () { return __awaiter(_this, void 0, void 0, function () {
var VoiceVideoCalling;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.debug && console.debug(LiveChatWidgetLibCDNUrl + " loaded!");
return [4 /*yield*/, createVoiceVideoCalling_1.default(params)];
case 1:
VoiceVideoCalling = _a.sent();
resolve(VoiceVideoCalling);
return [2 /*return*/];
}
});
}); }, function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
reject('Failed to load VoiceVideoCalling');
return [2 /*return*/];
});
}); })];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); })];
}
return [2 /*return*/];
}
});
});
};
OmnichannelChatSDK.prototype.getIC3Client = function () {
return __awaiter(this, void 0, void 0, function () {
var IC3Client, scriptElement;
var IC3Client;
var _this = this;

@@ -534,26 +628,43 @@ return __generator(this, function (_a) {

this.debug && console.debug('IC3Client');
scriptElement = document.createElement('script');
scriptElement.setAttribute('src', libraries_1.default.getIC3ClientCDNUrl());
document.head.appendChild(scriptElement);
return [2 /*return*/, new Promise(function (resolve) {
window.addEventListener("ic3:sdk:load", function () { return __awaiter(_this, void 0, void 0, function () {
var ic3sdk, IC3SDKProvider, IC3Client;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
ic3sdk = window.Microsoft.CRM.Omnichannel.IC3Client.SDK;
IC3SDKProvider = ic3sdk.SDKProvider;
this.IC3SDKProvider = IC3SDKProvider;
return [4 /*yield*/, IC3SDKProvider.getSDK({
hostType: HostType_1.default.IFrame,
protocolType: ProtocoleType_1.default.IC3V1SDK
})];
case 1:
IC3Client = _a.sent();
resolve(IC3Client);
return [2 /*return*/];
}
});
}); });
})];
// Use IC3Client if browser is detected
return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () {
var ic3ClientCDNUrl;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
ic3ClientCDNUrl = libraries_1.default.getIC3ClientCDNUrl();
window.addEventListener("ic3:sdk:load", function () { return __awaiter(_this, void 0, void 0, function () {
var ic3sdk, IC3SDKProvider, IC3Client;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
// Use FramedBridge from IC3Client
/* istanbul ignore next */
this.debug && console.debug('ic3:sdk:load');
ic3sdk = window.Microsoft.CRM.Omnichannel.IC3Client.SDK;
IC3SDKProvider = ic3sdk.SDKProvider;
this.IC3SDKProvider = IC3SDKProvider;
return [4 /*yield*/, IC3SDKProvider.getSDK({
hostType: HostType_1.default.IFrame,
protocolType: ProtocoleType_1.default.IC3V1SDK
})];
case 1:
IC3Client = _a.sent();
resolve(IC3Client);
return [2 /*return*/];
}
});
}); });
return [4 /*yield*/, WebUtils_1.loadScript(ic3ClientCDNUrl, function () {
_this.debug && console.debug('IC3Client loaded!');
}, function () {
reject('Failed to load IC3Adapter');
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); })];
}

@@ -565,3 +676,3 @@ });

return __awaiter(this, void 0, void 0, function () {
var liveChatConfig, dataMaskingConfig, authSettings, liveWSAndLiveChatEngJoin, setting, preChatSurvey, msdyn_prechatenabled, isPreChatEnabled, token, error_8;
var liveChatConfig, dataMaskingConfig, authSettings, liveWSAndLiveChatEngJoin, setting, preChatSurvey, msdyn_prechatenabled, msdyn_callingoptions, isPreChatEnabled, token, error_8;
return __generator(this, function (_a) {

@@ -582,3 +693,3 @@ switch (_a.label) {

}
preChatSurvey = liveWSAndLiveChatEngJoin.PreChatSurvey, msdyn_prechatenabled = liveWSAndLiveChatEngJoin.msdyn_prechatenabled;
preChatSurvey = liveWSAndLiveChatEngJoin.PreChatSurvey, msdyn_prechatenabled = liveWSAndLiveChatEngJoin.msdyn_prechatenabled, msdyn_callingoptions = liveWSAndLiveChatEngJoin.msdyn_callingoptions;
isPreChatEnabled = msdyn_prechatenabled === true || msdyn_prechatenabled == "true";

@@ -608,2 +719,3 @@ if (isPreChatEnabled && preChatSurvey && preChatSurvey.trim().length > 0) {

}
this.callingOption = msdyn_callingoptions;
this.liveChatConfig = liveChatConfig;

@@ -610,0 +722,0 @@ return [2 /*return*/, this.liveChatConfig];

@@ -1,2 +0,2 @@

export declare const isSystemMessage: (message: any) => any;
export declare const isCustomerMessage: (message: any) => any;
export declare const isSystemMessage: (message: any) => boolean;
export declare const isCustomerMessage: (message: any) => boolean;
{
"name": "@microsoft/omnichannel-chat-sdk",
"version": "0.1.1-main.55b031c",
"version": "0.1.1-main.67e5368",
"description": "Microsoft Omnichannel Chat SDK",
"files": [
"lib/**/*"
],
"main": "lib/index.js",

@@ -9,3 +12,4 @@ "types": "lib/index.d.ts",

"build:tsc": "tsc",
"test": "jest"
"test": "jest",
"lint": "eslint src --ext .ts"
},

@@ -30,10 +34,13 @@ "author": "Microsoft Corporation",

"@types/jest": "^26.0.10",
"jest": "^26.4.2",
"ts-jest": "^26.3.0",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"eslint": "^7.15.0",
"jest": "^26.6.3",
"ts-jest": "^26.5.1",
"typescript": "^3.9.5"
},
"dependencies": {
"@microsoft/ocsdk": "^0.1.0",
"@microsoft/omnichannel-ic3core": "^0.1.0"
"@microsoft/ocsdk": "^0.1.1",
"@microsoft/omnichannel-ic3core": "^0.1.1"
}
}
# Omnichannel Chat SDK
[![npm version](https://badge.fury.io/js/%40microsoft%2Fomnichannel-chat-sdk.svg)](https://badge.fury.io/js/%40microsoft%2Fomnichannel-chat-sdk)
![Release CI](https://github.com/microsoft/omnichannel-chat-sdk/workflows/Release%20CI/badge.svg)

@@ -24,2 +25,3 @@ Headless Chat SDK to build your own chat widget against Dynamics 365 Omnichannel Services.

| OmnichannelChatSDK.getLiveChatConfig() | Get live chat config | |
| OmnichannelChatSDK.getDataMaskingRules() | Get active data masking rules | |
| OmnichannelChatSDK.getCurrentLiveChatContext() | Get current live chat context information to reconnect to the same chat | |

@@ -37,3 +39,4 @@ | OmnichannelChatSDK.getChatToken() | Get chat token | |

| OmnichannelChatSDK.downloadFileAttachment() | Download file attachment | |
| OmnichannelChatSDK.createChatAdapter() | Get IC3Adapter (Web only) | |
| OmnichannelChatSDK.createChatAdapter() | Get IC3Adapter | **Web only** |
| OmnichannelChatSDK.getVoiceVideoCalling() | Get VoiceVideoCall SDK for Escalation to Voice & Video| **Web only** |

@@ -79,2 +82,7 @@ ### Import

### Get Data Masking Rules
```ts
const dataMaskingRules = await chatSDK.getDataMaskingRules();
```
### Get PreChat Survey

@@ -324,3 +332,96 @@ `Option 1`

### Escalation to Voice & Video
**NOTE**: Currently supported on web only
```ts
import OmnichannelChatSDK from '@microsoft/omnichannel-chat-sdk';
...
const chatSDK = new OmnichannelChatSDK.OmnichannelChatSDK(omnichannelConfig, chatSDKConfig);
await chatSDK.initialize();
let VoiceVideoCallingSDK;
try {
VoiceVideoCallingSDK = await chatSDK.getVoiceVideoCalling();
console.log("VoiceVideoCalling loaded");
} catch (e) {
console.log(`Failed to load VoiceVideoCalling: ${e}`);
}
await chatSDK.startChat();
const chatToken: any = await chatSDK.getChatToken();
try {
await VoiceVideoCallingSDK.initialize({
chatToken,
selfVideoHTMLElementId: 'selfVideo', // HTML element id where video stream of the agent will be rendered
remoteVideoHTMLElementId: 'remoteVideo', // HTML element id where video stream of the customer will be rendered
OCClient: chatSDK.OCClient
});
} catch (e) {
console.error("Failed to initialize VoiceVideoCalling!");
}
// Triggered when there's an incoming call
VoiceVideoCallingSDK.onCallAdded(() => {
...
});
// Triggered when local video stream is available (e.g.: Local video added succesfully in selfVideoHTMLElement)
VoiceVideoCallingSDK.onLocalVideoStreamAdded(() => {
...
});
// Triggered when local video stream is unavailable (e.g.: Customer turning off local video)
VoiceVideoCallingSDK.onLocalVideoStreamRemoved(() => {
...
});
// Triggered when remote video stream is available (e.g.: Remote video added succesfully in remoteVideoHTMLElement)
VoiceVideoCallingSDK.onRemoteVideoStreamAdded(() => {
...
});
// Triggered when remote video stream is unavailable (e.g.: Agent turning off remote video)
VoiceVideoCallingSDK.onRemoteVideoStreamRemoved(() => {
...
});
// Triggered when current call has ended or disconnected regardless the party
VoiceVideoCalling.onCallDisconnected(() => {
...
});
// Check if microphone is muted
const isMicrophoneMuted = VoiceVideoCallingSDK.isMicrophoneMuted();
// Check if remote video is available
const isRemoteVideoEnabled = VoiceVideoCallingSDK.isRemoteVideoEnabled();
// Check if local video is available
const isLocalVideoEnabled = VoiceVideoCallingSDK.isLocalVideoEnabled();
// Accepts incoming call
const acceptCallConfig = {
withVideo: true // Accept call with/without video stream
};
await VoiceVideoCallingSDK.acceptCall(acceptCallConfig);
// Rejects incoming call
await VoiceVideoCallingSDK.rejectCall();
// Ends/Stops current call
await VoiceVideoCallingSDK.stopCall();
// Mute/Unmute current call
await VoiceVideoCallingSDK.toggleMute()
// Display/Hide local video of current call
await VoiceVideoCallingSDK.toggleLocalVideo()
// Clean up VoiceVideoCallingSDK (e.g.: Usually called when customer ends chat session)
VoiceVideoCallingSDK.close();
```
## Feature Comparisons

@@ -333,3 +434,3 @@

| Chat Widget UI | Not provided | Basic chat client provided |
| Data Masking | Embedded | Requires `Attachment Middleware` implementation |
| Data Masking | Embedded | Requires `Data Masking Middleware` implementation |
| Send Typing indicator | Embedded | Requires `sendTypingIndicator` flag set to `true` |

@@ -346,3 +447,3 @@ | PreChat Survey | Requires Adaptive Cards renderer | Requires Adaptive Cards renderer

| Data Masking | Embedded | Embedded | X |
| Send Typing indicator | Embedded | WIP | X |
| Send Typing indicator | Embedded | Requires Implementation | X |
| PreChat Survey | Requires Adaptive Cards renderer | Requires Adaptive Cards renderer | X |

@@ -349,0 +450,0 @@ | Display Attachments | Requires implementation| Embedded | X |

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

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