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

@aerogear/apollo-voyager-conflicts

Package Overview
Dependencies
Maintainers
9
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@aerogear/apollo-voyager-conflicts - npm Package Compare versions

Comparing version 0.1.0-alpha to 0.2.7-SNAPSHOT

dist/api/ConflictLogger.d.ts

3

dist/api/ConflictHandler.d.ts
import { ObjectStateData } from './ObjectStateData';
import { ConflictResolution } from './ConflictResolution';
/**

@@ -8,2 +9,2 @@ * @param serverState server side data

*/
export declare type ConflictResolutionHandler = (serverState: ObjectStateData, clientState: ObjectStateData) => void;
export declare type ConflictResolutionHandler = (serverState: ObjectStateData, clientState: ObjectStateData, baseState?: ObjectStateData) => Promise<ConflictResolution> | ConflictResolution;

@@ -0,1 +1,4 @@

import { ConflictLogger } from './ConflictLogger';
import { ConflictResolution } from './ConflictResolution';
import { ConflictResolutionStrategy } from './ConflictResolutionStrategy';
import { ObjectStateData } from './ObjectStateData';

@@ -11,11 +14,28 @@ /**

*
* @param serverData the data currently on the server
* @param clientData the data the client wishes to perform some mutation with
* @param serverState the data currently on the server
* @param clientState the data the client wishes to perform some mutation with
*/
hasConflict(serverData: ObjectStateData, clientData: ObjectStateData): boolean;
hasConflict(serverState: ObjectStateData, clientState: ObjectStateData): boolean;
/**
*
* @param currentObjectState the object wish you would like to progress to its next state
* @param objectState the object wish you would like to progress to its next state
*/
nextState(currentObjectState: ObjectStateData): ObjectStateData;
nextState(objectState: ObjectStateData): ObjectStateData;
/**
*
* @param serverState the current state of the object on the server
* @param clientState the state of the object the client wishes to perform some mutation with
*/
resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData): ConflictResolution;
/**
*
* @param serverState the current state of the object on the server
* @param clientState the state of the object the client wishes to perform some mutation with
*/
resolveOnServer(strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData): Promise<ConflictResolution>;
/**
* Enable logging for conflict resolution package
* @param logger - logger implementation
*/
enableLogging(logger: ConflictLogger): void;
}

@@ -7,1 +7,2 @@ import { ConflictResolutionHandler } from '../api/ConflictHandler';

export declare const handleConflictOnClient: ConflictResolutionHandler;
export declare const resolveOnServer: ConflictResolutionHandler;

@@ -7,3 +7,3 @@ "use strict";

const debug_1 = __importDefault(require("debug"));
const ObjectConflictError_1 = require("../api/ObjectConflictError");
const ConflictResolution_1 = require("../api/ConflictResolution");
const constants_1 = require("../constants");

@@ -15,12 +15,12 @@ const logger = debug_1.default(constants_1.CONFLICT_LOGGER);

*/
exports.handleConflictOnClient = (serverState, clientState) => {
exports.handleConflictOnClient = function (serverState, clientState) {
logger(`Conflict detected.
Sending data to resolve conflict on client
Server: ${serverState} client: ${clientState}`);
throw new ObjectConflictError_1.ObjectConflictError({
clientData: clientState,
serverData: serverState,
resolvedOnServer: false
});
return new ConflictResolution_1.ConflictResolution(false, serverState, clientState);
};
exports.resolveOnServer = async function (strategy, serverState, clientState, baseState) {
const resolvedData = await strategy(serverState, clientState, baseState);
return new ConflictResolution_1.ConflictResolution(true, resolvedData, clientState, baseState);
};
//# sourceMappingURL=handleConflictOnClient.js.map

@@ -6,11 +6,11 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./api/ObjectConflictError"));
__export(require("./api/ConflictResolution"));
// State implementations
__export(require("./states/VersionedObjectState"));
__export(require("./states/HashObjectState"));
// Strategy implementations
__export(require("./strategies"));
// Default API state handler
var VersionedObjectState_1 = require("./states/VersionedObjectState");
exports.conflictHandler = VersionedObjectState_1.versionStateHandler;
// Conflict handlers
__export(require("./conflictHandlers/handleConflictOnClient"));
//# sourceMappingURL=index.js.map

@@ -0,21 +1,18 @@

import { ConflictLogger } from '../api/ConflictLogger';
import { ConflictResolution } from '../api/ConflictResolution';
import { ConflictResolutionStrategy } from '../api/ConflictResolutionStrategy';
import { ObjectState } from '../api/ObjectState';
import { ObjectStateData } from '../api/ObjectStateData';
/**
* Object state manager using a hash field on object
* Detects conflicts and allows moving to next state using the hash field of the object
*
* HashObjectState requires GraphQL types to contain hash field.
* For example:
*
* type User {
* id: ID!
* hash: String
* }
* Object state manager using a hashing method provided by user
*/
export declare class HashObjectState implements ObjectState {
private hash;
private logger;
private hash;
constructor(hashImpl: (object: any) => string);
hasConflict(serverData: ObjectStateData, clientData: ObjectStateData): boolean;
nextState(currentObjectState: ObjectStateData): any;
resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData): ConflictResolution;
resolveOnServer(strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData): Promise<ConflictResolution>;
enableLogging(logger: ConflictLogger): void;
}
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug = __importStar(require("debug"));
const constants_1 = require("../constants");
const ConflictResolution_1 = require("../api/ConflictResolution");
/**
* Object state manager using a hash field on object
* Detects conflicts and allows moving to next state using the hash field of the object
*
* HashObjectState requires GraphQL types to contain hash field.
* For example:
*
* type User {
* id: ID!
* hash: String
* }
* Object state manager using a hashing method provided by user
*/
class HashObjectState {
constructor(hashImpl) {
this.logger = debug.default(constants_1.CONFLICT_LOGGER);
this.hash = hashImpl;
}
hasConflict(serverData, clientData) {
if (serverData.hash && clientData.hash) {
if (serverData.hash !== clientData.hash) {
this.logger(`Conflict when saving data. current: ${serverData}, client: ${clientData}`);
return true;
if (this.hash(serverData) !== this.hash(clientData)) {
if (this.logger) {
this.logger.info(`Conflict when saving data.
current: ${JSON.stringify(serverData)},
client: ${JSON.stringify(clientData)}`);
}
return true;
}

@@ -39,8 +23,21 @@ return false;

nextState(currentObjectState) {
this.logger(`Moving object to next state, ${currentObjectState}`);
currentObjectState.hash = this.hash(currentObjectState);
// Hash can be calculated at any time and it is not added to object
return currentObjectState;
}
resolveOnClient(serverState, clientState) {
return new ConflictResolution_1.ConflictResolution(false, serverState, clientState);
}
async resolveOnServer(strategy, serverState, clientState) {
let resolvedState = strategy(serverState, clientState);
if (resolvedState instanceof Promise) {
resolvedState = await resolvedState;
}
resolvedState = this.nextState(resolvedState);
return new ConflictResolution_1.ConflictResolution(true, resolvedState, clientState);
}
enableLogging(logger) {
this.logger = logger;
}
}
exports.HashObjectState = HashObjectState;
//# sourceMappingURL=HashObjectState.js.map

@@ -0,1 +1,4 @@

import { ConflictLogger } from '../api/ConflictLogger';
import { ConflictResolution } from '../api/ConflictResolution';
import { ConflictResolutionStrategy } from '../api/ConflictResolutionStrategy';
import { ObjectState } from '../api/ObjectState';

@@ -19,2 +22,5 @@ import { ObjectStateData } from '../api/ObjectStateData';

nextState(currentObjectState: ObjectStateData): any;
resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData): ConflictResolution;
resolveOnServer(strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData): Promise<ConflictResolution>;
enableLogging(logger: ConflictLogger): void;
}

@@ -21,0 +27,0 @@ /**

"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug = __importStar(require("debug"));
const constants_1 = require("../constants");
const ConflictResolution_1 = require("../api/ConflictResolution");
/**

@@ -25,19 +17,43 @@ * Object state manager using a version field

class VersionedObjectState {
constructor() {
this.logger = debug.default(constants_1.CONFLICT_LOGGER);
}
hasConflict(serverData, clientData) {
if (serverData.version && clientData.version) {
if (serverData.version !== clientData.version) {
this.logger(`Conflict when saving data. current: ${serverData}, client: ${clientData}`);
if (this.logger) {
this.logger.info(`Conflict when saving data.
current: ${JSON.stringify(serverData)},
client: ${JSON.stringify(clientData)}`);
}
return true;
}
}
else if (this.logger) {
this.logger.info(`Supplied object is missing version field required to determine conflict
server: ${JSON.stringify(serverData)}
client: ${JSON.stringify(clientData)}`);
}
return false;
}
nextState(currentObjectState) {
this.logger(`Moving object to next state, ${currentObjectState}`);
if (this.logger) {
this.logger.info(`Moving object to next state,
${JSON.stringify(currentObjectState)}`);
}
currentObjectState.version = currentObjectState.version + 1;
return currentObjectState;
}
resolveOnClient(serverState, clientState) {
return new ConflictResolution_1.ConflictResolution(false, serverState, clientState);
}
async resolveOnServer(strategy, serverState, clientState) {
let resolvedState = strategy(serverState, clientState);
if (resolvedState instanceof Promise) {
resolvedState = await resolvedState;
}
resolvedState.version = serverState.version;
resolvedState = this.nextState(resolvedState);
return new ConflictResolution_1.ConflictResolution(true, resolvedState, clientState);
}
enableLogging(logger) {
this.logger = logger;
}
}

@@ -44,0 +60,0 @@ exports.VersionedObjectState = VersionedObjectState;

{
"name": "@aerogear/apollo-voyager-conflicts",
"version": "0.1.0-alpha",
"version": "0.2.7-SNAPSHOT",
"description": "A package to provide conflict handler framework for GraphQL server",

@@ -25,3 +25,2 @@ "main": "dist/index.js",

"@types/chai": "^4.1.3",
"@types/debug": "0.0.31",
"ava": "1.0.0-rc.2",

@@ -33,5 +32,2 @@ "chai": "^4.1.2",

},
"dependencies": {
"debug": "^4.1.0"
},
"publishConfig": {

@@ -38,0 +34,0 @@ "access": "public"

@@ -0,1 +1,4 @@

import { ConflictLogger } from './ConflictLogger'
import { ConflictResolution } from './ConflictResolution'
import { ConflictResolutionStrategy } from './ConflictResolutionStrategy'
import { ObjectStateData } from './ObjectStateData'

@@ -13,12 +16,32 @@

*
* @param serverData the data currently on the server
* @param clientData the data the client wishes to perform some mutation with
* @param serverState the data currently on the server
* @param clientState the data the client wishes to perform some mutation with
*/
hasConflict(serverData: ObjectStateData, clientData: ObjectStateData): boolean
hasConflict(serverState: ObjectStateData, clientState: ObjectStateData): boolean
/**
*
* @param currentObjectState the object wish you would like to progress to its next state
* @param objectState the object wish you would like to progress to its next state
*/
nextState(currentObjectState: ObjectStateData): ObjectStateData
nextState(objectState: ObjectStateData): ObjectStateData
/**
*
* @param serverState the current state of the object on the server
* @param clientState the state of the object the client wishes to perform some mutation with
*/
resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData): ConflictResolution
/**
*
* @param serverState the current state of the object on the server
* @param clientState the state of the object the client wishes to perform some mutation with
*/
resolveOnServer (strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData): Promise<ConflictResolution>
/**
* Enable logging for conflict resolution package
* @param logger - logger implementation
*/
enableLogging(logger: ConflictLogger): void
}
// Conflict api
export * from './api/ObjectState'
export * from './api/ObjectConflictError'
export * from './api/ConflictResolution'
export * from './api/ObjectStateData'
export * from './api/ConflictLogger'

@@ -9,7 +10,8 @@ // State implementations

export * from './states/HashObjectState'
// Strategy implementations
export * from './strategies'
// Default API state handler
export { versionStateHandler as conflictHandler }
from './states/VersionedObjectState'
// Conflict handlers
export * from './conflictHandlers/handleConflictOnClient'

@@ -1,21 +0,13 @@

import * as debug from 'debug'
import { ConflictLogger } from '../api/ConflictLogger'
import { ConflictResolution } from '../api/ConflictResolution'
import { ConflictResolutionStrategy } from '../api/ConflictResolutionStrategy'
import { ObjectState } from '../api/ObjectState'
import { ObjectStateData } from '../api/ObjectStateData'
import { CONFLICT_LOGGER } from '../constants'
/**
* Object state manager using a hash field on object
* Detects conflicts and allows moving to next state using the hash field of the object
*
* HashObjectState requires GraphQL types to contain hash field.
* For example:
*
* type User {
* id: ID!
* hash: String
* }
* Object state manager using a hashing method provided by user
*/
export class HashObjectState implements ObjectState {
private logger = debug.default(CONFLICT_LOGGER)
private hash: (object: any) => string
private logger: ConflictLogger | undefined

@@ -27,8 +19,9 @@ constructor(hashImpl: (object: any) => string) {

public hasConflict(serverData: ObjectStateData, clientData: ObjectStateData) {
if (serverData.hash && clientData.hash) {
if (serverData.hash !== clientData.hash) {
this.logger(`Conflict when saving data. current: ${serverData}, client: ${clientData}`)
return true
if (this.hash(serverData) !== this.hash(clientData)) {
if (this.logger) {
this.logger.info(`Conflict when saving data.
current: ${ JSON.stringify(serverData)},
client: ${JSON.stringify(clientData)}`)
}
return true
}

@@ -39,6 +32,26 @@ return false

public nextState(currentObjectState: ObjectStateData) {
this.logger(`Moving object to next state, ${currentObjectState}`)
currentObjectState.hash = this.hash(currentObjectState)
// Hash can be calculated at any time and it is not added to object
return currentObjectState
}
public resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData) {
return new ConflictResolution(false, serverState, clientState)
}
public async resolveOnServer(strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData) {
let resolvedState = strategy(serverState, clientState)
if (resolvedState instanceof Promise) {
resolvedState = await resolvedState
}
resolvedState = this.nextState(resolvedState)
return new ConflictResolution(true, resolvedState, clientState)
}
public enableLogging(logger: ConflictLogger): void {
this.logger = logger
}
}

@@ -1,5 +0,6 @@

import * as debug from 'debug'
import { ConflictLogger } from '../api/ConflictLogger'
import { ConflictResolution } from '../api/ConflictResolution'
import { ConflictResolutionStrategy } from '../api/ConflictResolutionStrategy'
import { ObjectState } from '../api/ObjectState'
import { ObjectStateData } from '../api/ObjectStateData'
import { CONFLICT_LOGGER } from '../constants'

@@ -19,3 +20,3 @@ /**

export class VersionedObjectState implements ObjectState {
private logger = debug.default(CONFLICT_LOGGER)
private logger: ConflictLogger | undefined

@@ -25,5 +26,14 @@ public hasConflict(serverData: ObjectStateData, clientData: ObjectStateData) {

if (serverData.version !== clientData.version) {
this.logger(`Conflict when saving data. current: ${serverData}, client: ${clientData}`)
if (this.logger) {
this.logger.info(`Conflict when saving data.
current: ${ JSON.stringify(serverData)},
client: ${JSON.stringify(clientData)}`)
}
return true
}
} else if (this.logger) {
this.logger.info(
`Supplied object is missing version field required to determine conflict
server: ${JSON.stringify(serverData)}
client: ${JSON.stringify(clientData)}`)
}

@@ -34,6 +44,30 @@ return false

public nextState(currentObjectState: ObjectStateData) {
this.logger(`Moving object to next state, ${currentObjectState}`)
if (this.logger) {
this.logger.info(`Moving object to next state,
${JSON.stringify(currentObjectState)}`)
}
currentObjectState.version = currentObjectState.version + 1
return currentObjectState
}
public resolveOnClient(serverState: ObjectStateData, clientState: ObjectStateData) {
return new ConflictResolution(false, serverState, clientState)
}
public async resolveOnServer(strategy: ConflictResolutionStrategy, serverState: ObjectStateData, clientState: ObjectStateData) {
let resolvedState = strategy(serverState, clientState)
if (resolvedState instanceof Promise) {
resolvedState = await resolvedState
}
resolvedState.version = serverState.version
resolvedState = this.nextState(resolvedState)
return new ConflictResolution(true, resolvedState, clientState)
}
public enableLogging(logger: ConflictLogger): void {
this.logger = logger
}
}

@@ -40,0 +74,0 @@

import test from 'ava'
import { VersionedObjectState } from '../src'
import { VersionedObjectState, ObjectStateData } from '../src'
import { ObjectConflictError } from '../src/api/ConflictResolution'
import { strategies } from '../src/strategies'

@@ -19,7 +21,213 @@ test('With conflict', (t) => {

test('Next state ', (t) => {
test('Missing version', (t) => {
const objectState = new VersionedObjectState()
const serverData = { name: 'AeroGear'}
const clientData = { name: 'AeroGear', version: 1 }
t.deepEqual(objectState.hasConflict(serverData, clientData), false)
})
test('Next state ', async (t) => {
const serverData = { name: 'AeroGear', version: 1 }
const objectState = new VersionedObjectState()
objectState.nextState(serverData)
t.deepEqual(serverData.version, 2)
const next = await objectState.nextState(serverData)
t.deepEqual(next.version, 2)
})
test('resolveOnClient returns the expected conflict payload for the client', (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'AeroGear Client', version: 1 }
const objectState = new VersionedObjectState()
const resolution = objectState.resolveOnClient(serverState, clientState)
const expected = {
payload: new ObjectConflictError({
serverState,
clientState,
resolvedOnServer: false
})
}
t.falsy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer works with the client wins strategy', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
const strategy = strategies.clientWins
const resolution = await objectState.resolveOnServer(strategy, serverState, clientState)
const expectedResolvedState = {
name: 'Client' ,
version: 3
}
const expected = {
resolvedState: expectedResolvedState,
payload: new ObjectConflictError({
serverState: expectedResolvedState,
clientState,
resolvedOnServer: true
})
}
t.truthy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.resolvedState, expected.resolvedState)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer works with the server wins strategy', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
const strategy = strategies.serverWins
const resolution = await objectState.resolveOnServer(strategy, serverState, clientState)
const expectedResolvedState = {
name: 'AeroGear' ,
version: 3
}
const expected = {
resolvedState: expectedResolvedState,
payload: new ObjectConflictError({
serverState: expectedResolvedState,
clientState,
resolvedOnServer: true
})
}
t.truthy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.resolvedState, expected.resolvedState)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer resolves the data using a custom handler', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
function customStrategy(serverState: ObjectStateData, clientState: ObjectStateData) {
return {
name: `${serverState.name} ${clientState.name}`
}
}
const resolution = await objectState.resolveOnServer(customStrategy, serverState, clientState)
const expectedResolvedState = {
name: 'AeroGear Client' ,
version: 3
}
const expected = {
resolvedState: expectedResolvedState,
payload: new ObjectConflictError({
serverState: expectedResolvedState,
clientState,
resolvedOnServer: true
})
}
t.truthy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.resolvedState, expected.resolvedState)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer applies the correct version number to resolvedState', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
function customStrategy(serverState: ObjectStateData, clientState: ObjectStateData) {
return {
name: `${serverState.name} ${clientState.name}`,
version: 50 // this gets overwritten with the correct version
}
}
const resolution = await objectState.resolveOnServer(customStrategy, serverState, clientState)
const expectedResolvedState = {
name: 'AeroGear Client' ,
version: 3
}
const expected = {
resolvedState: expectedResolvedState,
payload: new ObjectConflictError({
serverState: expectedResolvedState,
clientState,
resolvedOnServer: true
})
}
t.truthy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.resolvedState, expected.resolvedState)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer resolves the data using a custom async handler', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
function customStrategy(serverState: ObjectStateData, clientState: ObjectStateData) {
return new Promise((resolve, reject) => {
return resolve({
name: `${serverState.name} ${clientState.name}`
})
})
}
const resolution = await objectState.resolveOnServer(customStrategy, serverState, clientState)
const expectedResolvedState = {
name: 'AeroGear Client' ,
version: 3
}
const expected = {
resolvedState: expectedResolvedState,
payload: new ObjectConflictError({
serverState: expectedResolvedState,
clientState,
resolvedOnServer: true
})
}
t.truthy(resolution.resolvedState)
t.truthy(resolution.payload)
t.deepEqual(resolution.resolvedState, expected.resolvedState)
t.deepEqual(resolution.payload, expected.payload)
})
test('resolveOnServer throws if custom async strategy rejects', async (t) => {
const serverState = { name: 'AeroGear', version: 2 }
const clientState = { name: 'Client', version: 1 }
const objectState = new VersionedObjectState()
function customStrategy(serverState: ObjectStateData, clientState: ObjectStateData) {
return new Promise((resolve, reject) => {
return reject(new Error('an error occurred'))
})
}
await t.throwsAsync(async () => {
await objectState.resolveOnServer(customStrategy, serverState, clientState)
})
})

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