Socket
Socket
Sign inDemoInstall

canvas-client

Package Overview
Dependencies
1
Maintainers
3
Versions
50
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.3.8 to 2.0.0

lib-esm/utils/parselink.d.ts

13

lib-esm/canvas.d.ts

@@ -1,8 +0,12 @@

import { CanvasAccount, CanvasCourse, CanvasSection, CanvasEnrollment, CanvasEnrollmentPayload, CanvasCoursePayload, CanvasSectionPayload, CanvasGradingStandard, CanvasID, SpecialUserID, SpecialSectionID, SISSectionID, SISUserID, SpecialCourseID, SISTermID, SpecialTermID, CanvasEnrollmentTerm, CanvasCourseParams, CanvasEnrollmentParams, CanvasCourseSettings, CanvasCourseSettingsUpdate, CanvasUserUpdatePayload, CanvasCourseListFilters, CanvasEnrollmentTermPayload, CanvasEnrollmentTermParams, CanvasCourseUsersParams, CanvasUser, CanvasAssignment, CanvasAssignmentNew, CanvasAssignmentSubmission, CanvasAssignmentSubmissionNew, UserDisplay } from './interfaces';
import { ExternalTool, ExternalToolPayload } from './interfaces/externaltool';
import { type CanvasAccount, CanvasCourse, type CanvasSection, type CanvasEnrollment, type CanvasEnrollmentPayload, type CanvasCoursePayload, CanvasSectionPayload, type CanvasGradingStandard, type CanvasID, type SpecialUserID, type SpecialSectionID, type SISSectionID, type SISUserID, type SpecialCourseID, type SISTermID, type SpecialTermID, CanvasEnrollmentTerm, type CanvasCourseParams, type CanvasEnrollmentParams, type CanvasCourseSettings, type CanvasCourseSettingsUpdate, type CanvasUserUpdatePayload, type CanvasCourseListFilters, type CanvasEnrollmentTermPayload, type CanvasEnrollmentTermParams, type CanvasCourseUsersParams, CanvasUser, CanvasAssignment, type CanvasAssignmentNew, type CanvasAssignmentSubmission, type CanvasAssignmentSubmissionNew, type UserDisplay } from './interfaces';
import { type ExternalTool, type ExternalToolPayload } from './interfaces/externaltool';
export declare class CanvasConnector {
protected canvasUrl: string;
private service;
protected token?: string | undefined;
private rateLimit;
constructor(canvasUrl: string, token?: string, options?: CanvasAPIOptions);
private restUrl;
constructor(canvasUrl: string, token?: string | undefined, options?: CanvasAPIOptions);
send(method: 'get' | 'post' | 'put' | 'delete' | 'head', url: string, payload?: any): Promise<Response & {
data?: any;
}>;
tasks(): number;

@@ -51,2 +55,3 @@ get(url: string, params?: any): Promise<any>;

getGradeableStudents(courseId: CanvasID, assignmentId: CanvasID): Promise<UserDisplay[]>;
getAssignmentSubmissions(courseId: CanvasID, assignmentId: CanvasID): Promise<CanvasAssignmentSubmission[]>;
getAssignmentSubmission(courseId: CanvasID, assignmentId: CanvasID, userId: CanvasID): Promise<CanvasAssignmentSubmission>;

@@ -53,0 +58,0 @@ updateAssignmentSubmission(submission: CanvasAssignmentSubmissionNew): Promise<CanvasAssignmentSubmission>;

@@ -1,11 +0,6 @@

import { HttpsAgent } from 'agentkeepalive';
import Axios from 'axios';
import flatten from 'lodash/flatten';
import range from 'lodash/range';
import { pLimit } from 'txstate-utils';
import parselinkheader from 'parse-link-header';
import qs from 'qs';
import { pLimit, omit, isNotEmpty } from 'txstate-utils';
import { CanvasCourse, CanvasSectionPayload, CanvasEnrollmentTerm, CanvasCourseIncludes, CanvasUser, CanvasAssignment } from './interfaces';
import { throwUnlessValidId, throwUnlessValidUserId } from './utils/utils';
import { GraphQLError } from './utils/errors';
import { parseLinkHeader } from './utils/parselink';
export class CanvasConnector {

@@ -15,11 +10,29 @@ constructor(canvasUrl, token, options = {}) {

this.canvasUrl = canvasUrl;
this.token = token;
this.rateLimit = pLimit(10);
const maxConnections = (_a = options.maxConnections) !== null && _a !== void 0 ? _a : 10;
this.rateLimit = pLimit(maxConnections);
this.service = Axios.create({
baseURL: canvasUrl + '/api/v1',
timeout: 20000,
httpsAgent: new HttpsAgent({ maxSockets: maxConnections }),
headers: Object.assign(Object.assign({}, (token ? { Authorization: 'Bearer ' + token } : {})), { 'Content-Type': 'application/json' })
this.restUrl = new URL('/api/v1', canvasUrl);
}
async send(method, url, payload = {}) {
let finalUrl;
try {
finalUrl = new URL(url); // URL was complete, use it
}
catch (_a) {
finalUrl = new URL('/api/v1/' + url.replace(/^\//, ''), this.restUrl);
}
const isPostOrPut = ['post', 'put'].includes(method);
if (!isPostOrPut && isNotEmpty(payload))
finalUrl.search = new URLSearchParams(payload).toString();
const resp = await fetch(finalUrl, {
method,
headers: Object.assign(Object.assign({}, (this.token ? { Authorization: 'Bearer ' + this.token } : {})), { 'Content-Type': 'application/json' }),
body: isPostOrPut && payload ? JSON.stringify(payload) : undefined
});
if (!resp.ok)
throw new Error(await resp.text() || resp.statusText);
if (method !== 'head')
resp.data = await resp.json();
return resp;
}

@@ -30,28 +43,26 @@ tasks() {

async get(url, params = {}) {
const res = await this.rateLimit(async () => await this.service.get(url, { params }));
const res = await this.rateLimit(async () => await this.send('get', url, params));
return res.data;
}
async getall(url, params = {}, returnObjKey) {
var _a, _b, _c, _d;
const res = await this.rateLimit(async () => await this.service.get(url, { params: Object.assign(Object.assign({}, params), { page: 1, per_page: 1000 }) }));
var _a, _b, _c, _d, _e;
const res = await this.rateLimit(async () => await this.send('get', url, Object.assign(Object.assign({}, params), { page: 1, per_page: 1000 })));
const ret = (returnObjKey ? (_a = res.data) === null || _a === void 0 ? void 0 : _a[returnObjKey] : res.data);
let links = parselinkheader(res.headers.link);
const lasturl = (_b = links === null || links === void 0 ? void 0 : links.last) === null || _b === void 0 ? void 0 : _b.url;
if (lasturl) {
const lastparams = qs.parse(lasturl.slice(lasturl.lastIndexOf('?') + 1));
const page = parseInt(lastparams === null || lastparams === void 0 ? void 0 : lastparams.page);
let links = parseLinkHeader(res.headers.get('link'));
const page = Number((_c = (_b = links === null || links === void 0 ? void 0 : links.last) === null || _b === void 0 ? void 0 : _b.page) !== null && _c !== void 0 ? _c : 0);
if (page > 0) {
if (page > 1) {
const alldata = await Promise.all(range(2, page + 1).map(async (p) => {
const alldata = await Promise.all(Array.from({ length: page - 1 }, (_, i) => i + 2).map(async (p) => {
var _a;
const res = await this.rateLimit(async () => await this.service.get(url, { params: Object.assign(Object.assign({}, lastparams), { page: p }) }));
const res = await this.rateLimit(async () => await this.send('get', url, Object.assign(Object.assign({}, omit(links.last, 'page', 'rel', 'url')), { page: p })));
return (returnObjKey ? (_a = res.data) === null || _a === void 0 ? void 0 : _a[returnObjKey] : res.data) || [];
}));
ret.push(...flatten(alldata));
ret.push(...alldata.flat());
}
}
else if ((_c = links === null || links === void 0 ? void 0 : links.next) === null || _c === void 0 ? void 0 : _c.url) {
while ((_d = links === null || links === void 0 ? void 0 : links.next) === null || _d === void 0 ? void 0 : _d.url) {
const res = await this.rateLimit(async () => await this.service.get(links.next.url));
else if ((_d = links === null || links === void 0 ? void 0 : links.next) === null || _d === void 0 ? void 0 : _d.url) {
while ((_e = links.next) === null || _e === void 0 ? void 0 : _e.url) {
const res = await this.rateLimit(async () => await this.send('get', links.next.url));
ret.push(...res.data);
links = parselinkheader(res.headers.link);
links = parseLinkHeader(res.headers.get('link'));
}

@@ -62,11 +73,11 @@ }

async delete(url, params = {}) {
const res = await this.rateLimit(async () => await this.service.delete(url, { params }));
const res = await this.rateLimit(async () => await this.send('delete', url, params));
return res.data;
}
async put(url, payload) {
const res = await this.rateLimit(async () => await this.service.put(url, payload));
const res = await this.rateLimit(async () => await this.send('put', url, payload));
return res.data;
}
async post(url, payload) {
const res = await this.rateLimit(async () => await this.service.post(url, payload));
const res = await this.rateLimit(async () => await this.send('post', url, payload));
return res.data;

@@ -77,4 +88,4 @@ }

try {
const res = await this.service.head(url);
return res.status >= 200 && res.status < 400;
const res = await this.send('head', url);
return res.ok;
}

@@ -98,3 +109,3 @@ catch (e) {

this.defaultCourseTimeZone = 'America/Chicago';
if (!canvasUrl || !canvasUrl.length)
if (!(canvasUrl === null || canvasUrl === void 0 ? void 0 : canvasUrl.length))
throw new Error('Instantiated a canvas client with no URL.');

@@ -178,3 +189,3 @@ if (tokens && !tokens.length)

courseSectionIds && await Promise.all(courseSectionIds.map(id => this.removeSISFromSection(id)));
return this.delete(`/courses/${courseId}`);
return await this.delete(`/courses/${courseId}`);
}

@@ -208,2 +219,5 @@ async concludeCourse(courseId) {

// ASSIGNMENT SUBMISSIONS (GRADES)
async getAssignmentSubmissions(courseId, assignmentId) {
return await this.getall(`/courses/${courseId}/assignments/${assignmentId}/submissions`);
}
async getAssignmentSubmission(courseId, assignmentId, userId) {

@@ -238,3 +252,3 @@ return await this.get(`/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`);

return await Promise.all(courseIds.map(id => this.courseSections(id)))
.then(flatten);
.then(c => c.flat());
}

@@ -252,3 +266,3 @@ async createSection(courseId, sectionPayload) {

const canvasSectionId = await this.removeSISFromSection(id).then(res => res.id);
return this.delete(`/sections/${canvasSectionId}`);
return await this.delete(`/sections/${canvasSectionId}`);
}

@@ -317,3 +331,3 @@ async deleteSectionBySIS(sisId) {

}));
return await Promise.all(flatten(enrollments).map(async (enrollment) => await this.deactivateEnrollmentFromSection(enrollment)));
return await Promise.all(enrollments.flat().map(async (enrollment) => await this.deactivateEnrollmentFromSection(enrollment)));
}

@@ -320,0 +334,0 @@ async deleteEnrollmentFromSection(enrollment) {

@@ -1,3 +0,3 @@

import { CanvasID } from '../interfaces';
import { type CanvasID } from '../interfaces';
export declare function throwUnlessValidId(id: CanvasID | string, prefix: string): void;
export declare function throwUnlessValidUserId(id: CanvasID | string): void;

@@ -1,8 +0,12 @@

import { CanvasAccount, CanvasCourse, CanvasSection, CanvasEnrollment, CanvasEnrollmentPayload, CanvasCoursePayload, CanvasSectionPayload, CanvasGradingStandard, CanvasID, SpecialUserID, SpecialSectionID, SISSectionID, SISUserID, SpecialCourseID, SISTermID, SpecialTermID, CanvasEnrollmentTerm, CanvasCourseParams, CanvasEnrollmentParams, CanvasCourseSettings, CanvasCourseSettingsUpdate, CanvasUserUpdatePayload, CanvasCourseListFilters, CanvasEnrollmentTermPayload, CanvasEnrollmentTermParams, CanvasCourseUsersParams, CanvasUser, CanvasAssignment, CanvasAssignmentNew, CanvasAssignmentSubmission, CanvasAssignmentSubmissionNew, UserDisplay } from './interfaces';
import { ExternalTool, ExternalToolPayload } from './interfaces/externaltool';
import { type CanvasAccount, CanvasCourse, type CanvasSection, type CanvasEnrollment, type CanvasEnrollmentPayload, type CanvasCoursePayload, CanvasSectionPayload, type CanvasGradingStandard, type CanvasID, type SpecialUserID, type SpecialSectionID, type SISSectionID, type SISUserID, type SpecialCourseID, type SISTermID, type SpecialTermID, CanvasEnrollmentTerm, type CanvasCourseParams, type CanvasEnrollmentParams, type CanvasCourseSettings, type CanvasCourseSettingsUpdate, type CanvasUserUpdatePayload, type CanvasCourseListFilters, type CanvasEnrollmentTermPayload, type CanvasEnrollmentTermParams, type CanvasCourseUsersParams, CanvasUser, CanvasAssignment, type CanvasAssignmentNew, type CanvasAssignmentSubmission, type CanvasAssignmentSubmissionNew, type UserDisplay } from './interfaces';
import { type ExternalTool, type ExternalToolPayload } from './interfaces/externaltool';
export declare class CanvasConnector {
protected canvasUrl: string;
private service;
protected token?: string | undefined;
private rateLimit;
constructor(canvasUrl: string, token?: string, options?: CanvasAPIOptions);
private restUrl;
constructor(canvasUrl: string, token?: string | undefined, options?: CanvasAPIOptions);
send(method: 'get' | 'post' | 'put' | 'delete' | 'head', url: string, payload?: any): Promise<Response & {
data?: any;
}>;
tasks(): number;

@@ -51,2 +55,3 @@ get(url: string, params?: any): Promise<any>;

getGradeableStudents(courseId: CanvasID, assignmentId: CanvasID): Promise<UserDisplay[]>;
getAssignmentSubmissions(courseId: CanvasID, assignmentId: CanvasID): Promise<CanvasAssignmentSubmission[]>;
getAssignmentSubmission(courseId: CanvasID, assignmentId: CanvasID, userId: CanvasID): Promise<CanvasAssignmentSubmission>;

@@ -53,0 +58,0 @@ updateAssignmentSubmission(submission: CanvasAssignmentSubmissionNew): Promise<CanvasAssignmentSubmission>;

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanvasAPI = exports.CanvasConnector = void 0;
const agentkeepalive_1 = require("agentkeepalive");
const axios_1 = __importDefault(require("axios"));
const flatten_1 = __importDefault(require("lodash/flatten"));
const range_1 = __importDefault(require("lodash/range"));
const txstate_utils_1 = require("txstate-utils");
const parse_link_header_1 = __importDefault(require("parse-link-header"));
const qs_1 = __importDefault(require("qs"));
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils/utils");
const errors_1 = require("./utils/errors");
const parselink_1 = require("./utils/parselink");
class CanvasConnector {

@@ -21,11 +13,29 @@ constructor(canvasUrl, token, options = {}) {

this.canvasUrl = canvasUrl;
this.token = token;
this.rateLimit = (0, txstate_utils_1.pLimit)(10);
const maxConnections = (_a = options.maxConnections) !== null && _a !== void 0 ? _a : 10;
this.rateLimit = (0, txstate_utils_1.pLimit)(maxConnections);
this.service = axios_1.default.create({
baseURL: canvasUrl + '/api/v1',
timeout: 20000,
httpsAgent: new agentkeepalive_1.HttpsAgent({ maxSockets: maxConnections }),
headers: Object.assign(Object.assign({}, (token ? { Authorization: 'Bearer ' + token } : {})), { 'Content-Type': 'application/json' })
this.restUrl = new URL('/api/v1', canvasUrl);
}
async send(method, url, payload = {}) {
let finalUrl;
try {
finalUrl = new URL(url); // URL was complete, use it
}
catch (_a) {
finalUrl = new URL('/api/v1/' + url.replace(/^\//, ''), this.restUrl);
}
const isPostOrPut = ['post', 'put'].includes(method);
if (!isPostOrPut && (0, txstate_utils_1.isNotEmpty)(payload))
finalUrl.search = new URLSearchParams(payload).toString();
const resp = await fetch(finalUrl, {
method,
headers: Object.assign(Object.assign({}, (this.token ? { Authorization: 'Bearer ' + this.token } : {})), { 'Content-Type': 'application/json' }),
body: isPostOrPut && payload ? JSON.stringify(payload) : undefined
});
if (!resp.ok)
throw new Error(await resp.text() || resp.statusText);
if (method !== 'head')
resp.data = await resp.json();
return resp;
}

@@ -36,28 +46,26 @@ tasks() {

async get(url, params = {}) {
const res = await this.rateLimit(async () => await this.service.get(url, { params }));
const res = await this.rateLimit(async () => await this.send('get', url, params));
return res.data;
}
async getall(url, params = {}, returnObjKey) {
var _a, _b, _c, _d;
const res = await this.rateLimit(async () => await this.service.get(url, { params: Object.assign(Object.assign({}, params), { page: 1, per_page: 1000 }) }));
var _a, _b, _c, _d, _e;
const res = await this.rateLimit(async () => await this.send('get', url, Object.assign(Object.assign({}, params), { page: 1, per_page: 1000 })));
const ret = (returnObjKey ? (_a = res.data) === null || _a === void 0 ? void 0 : _a[returnObjKey] : res.data);
let links = (0, parse_link_header_1.default)(res.headers.link);
const lasturl = (_b = links === null || links === void 0 ? void 0 : links.last) === null || _b === void 0 ? void 0 : _b.url;
if (lasturl) {
const lastparams = qs_1.default.parse(lasturl.slice(lasturl.lastIndexOf('?') + 1));
const page = parseInt(lastparams === null || lastparams === void 0 ? void 0 : lastparams.page);
let links = (0, parselink_1.parseLinkHeader)(res.headers.get('link'));
const page = Number((_c = (_b = links === null || links === void 0 ? void 0 : links.last) === null || _b === void 0 ? void 0 : _b.page) !== null && _c !== void 0 ? _c : 0);
if (page > 0) {
if (page > 1) {
const alldata = await Promise.all((0, range_1.default)(2, page + 1).map(async (p) => {
const alldata = await Promise.all(Array.from({ length: page - 1 }, (_, i) => i + 2).map(async (p) => {
var _a;
const res = await this.rateLimit(async () => await this.service.get(url, { params: Object.assign(Object.assign({}, lastparams), { page: p }) }));
const res = await this.rateLimit(async () => await this.send('get', url, Object.assign(Object.assign({}, (0, txstate_utils_1.omit)(links.last, 'page', 'rel', 'url')), { page: p })));
return (returnObjKey ? (_a = res.data) === null || _a === void 0 ? void 0 : _a[returnObjKey] : res.data) || [];
}));
ret.push(...(0, flatten_1.default)(alldata));
ret.push(...alldata.flat());
}
}
else if ((_c = links === null || links === void 0 ? void 0 : links.next) === null || _c === void 0 ? void 0 : _c.url) {
while ((_d = links === null || links === void 0 ? void 0 : links.next) === null || _d === void 0 ? void 0 : _d.url) {
const res = await this.rateLimit(async () => await this.service.get(links.next.url));
else if ((_d = links === null || links === void 0 ? void 0 : links.next) === null || _d === void 0 ? void 0 : _d.url) {
while ((_e = links.next) === null || _e === void 0 ? void 0 : _e.url) {
const res = await this.rateLimit(async () => await this.send('get', links.next.url));
ret.push(...res.data);
links = (0, parse_link_header_1.default)(res.headers.link);
links = (0, parselink_1.parseLinkHeader)(res.headers.get('link'));
}

@@ -68,11 +76,11 @@ }

async delete(url, params = {}) {
const res = await this.rateLimit(async () => await this.service.delete(url, { params }));
const res = await this.rateLimit(async () => await this.send('delete', url, params));
return res.data;
}
async put(url, payload) {
const res = await this.rateLimit(async () => await this.service.put(url, payload));
const res = await this.rateLimit(async () => await this.send('put', url, payload));
return res.data;
}
async post(url, payload) {
const res = await this.rateLimit(async () => await this.service.post(url, payload));
const res = await this.rateLimit(async () => await this.send('post', url, payload));
return res.data;

@@ -83,4 +91,4 @@ }

try {
const res = await this.service.head(url);
return res.status >= 200 && res.status < 400;
const res = await this.send('head', url);
return res.ok;
}

@@ -105,3 +113,3 @@ catch (e) {

this.defaultCourseTimeZone = 'America/Chicago';
if (!canvasUrl || !canvasUrl.length)
if (!(canvasUrl === null || canvasUrl === void 0 ? void 0 : canvasUrl.length))
throw new Error('Instantiated a canvas client with no URL.');

@@ -185,3 +193,3 @@ if (tokens && !tokens.length)

courseSectionIds && await Promise.all(courseSectionIds.map(id => this.removeSISFromSection(id)));
return this.delete(`/courses/${courseId}`);
return await this.delete(`/courses/${courseId}`);
}

@@ -215,2 +223,5 @@ async concludeCourse(courseId) {

// ASSIGNMENT SUBMISSIONS (GRADES)
async getAssignmentSubmissions(courseId, assignmentId) {
return await this.getall(`/courses/${courseId}/assignments/${assignmentId}/submissions`);
}
async getAssignmentSubmission(courseId, assignmentId, userId) {

@@ -245,3 +256,3 @@ return await this.get(`/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`);

return await Promise.all(courseIds.map(id => this.courseSections(id)))
.then(flatten_1.default);
.then(c => c.flat());
}

@@ -259,3 +270,3 @@ async createSection(courseId, sectionPayload) {

const canvasSectionId = await this.removeSISFromSection(id).then(res => res.id);
return this.delete(`/sections/${canvasSectionId}`);
return await this.delete(`/sections/${canvasSectionId}`);
}

@@ -324,3 +335,3 @@ async deleteSectionBySIS(sisId) {

}));
return await Promise.all((0, flatten_1.default)(enrollments).map(async (enrollment) => await this.deactivateEnrollmentFromSection(enrollment)));
return await Promise.all(enrollments.flat().map(async (enrollment) => await this.deactivateEnrollmentFromSection(enrollment)));
}

@@ -327,0 +338,0 @@ async deleteEnrollmentFromSection(enrollment) {

@@ -1,3 +0,3 @@

import { CanvasID } from '../interfaces';
import { type CanvasID } from '../interfaces';
export declare function throwUnlessValidId(id: CanvasID | string, prefix: string): void;
export declare function throwUnlessValidUserId(id: CanvasID | string): void;
{
"name": "canvas-client",
"version": "1.3.8",
"version": "2.0.0",
"description": "Typescript library to make accessing the Canvas API more convenient.",

@@ -31,10 +31,7 @@ "main": "lib/index.js",

"@types/chai": "^4.2.14",
"@types/lodash": "^4.14.167",
"@types/luxon": "^3.2.0",
"@types/mocha": "^10.0.1",
"@types/parse-link-header": "^2.0.1",
"@types/qs": "^6.9.5",
"chai": "^4.3.7",
"dotenv": "^16.0.3",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-config-standard-with-typescript": "^39.0.0",
"luxon": "^3.2.1",

@@ -46,8 +43,3 @@ "mocha": "^10.2.0",

"dependencies": {
"agentkeepalive": "^4.1.3",
"axios": "^1.3.4",
"lodash": "^4.17.20",
"txstate-utils": "^1.8.7",
"parse-link-header": "^2.0.0",
"qs": "^6.9.4"
"txstate-utils": "^1.8.15"
},

@@ -54,0 +46,0 @@ "files": [

@@ -6,1 +6,4 @@ # canvas-client

Duplicate `.env.example`, rename it to `.env` and fill in good values for each of the variables. This will allow you to run `npm test` successfully.
# 2.0
The breaking change for 2.0 is node 18+ is required since I got rid of axios in favor of the native fetch api.
SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc