Socket
Socket
Sign inDemoInstall

@uppy/provider-views

Package Overview
Dependencies
Maintainers
6
Versions
108
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@uppy/provider-views - npm Package Compare versions

Comparing version 3.3.1 to 3.4.0

8

CHANGELOG.md
# @uppy/provider-views
## 3.4.0
Released: 2023-07-13
Included in: Uppy v3.12.0
- @uppy/provider-views: add support for remote file paths (Mikael Finstad / #4537)
- @uppy/box,@uppy/companion,@uppy/dropbox,@uppy/google-drive,@uppy/onedrive,@uppy/provider-views: Load Google Drive / OneDrive lists 5-10x faster & always load all files (Merlijn Vos / #4513)
## 3.3.1

@@ -4,0 +12,0 @@

10

lib/Breadcrumbs.js

@@ -19,3 +19,3 @@ import { h, Fragment } from 'preact';

breadcrumbsIcon,
directories
breadcrumbs
} = props;

@@ -26,8 +26,8 @@ return h("div", {

className: "uppy-Provider-breadcrumbsIcon"
}, breadcrumbsIcon), directories.map((directory, i) => h(Breadcrumb, {
}, breadcrumbsIcon), breadcrumbs.map((directory, i) => h(Breadcrumb, {
key: directory.id,
getFolder: () => getFolder(directory.id),
title: i === 0 ? title : directory.title,
isLast: i + 1 === directories.length
getFolder: () => getFolder(directory.requestPath),
title: i === 0 ? title : directory.name,
isLast: i + 1 === breadcrumbs.length
})));
});

@@ -62,3 +62,4 @@ import { h } from 'preact';

src: itemIconString,
alt: alt
alt: alt,
loading: "lazy"
});

@@ -65,0 +66,0 @@ }

@@ -8,3 +8,3 @@ import User from "./User.js";

getFolder: props.getFolder,
directories: props.directories,
breadcrumbs: props.breadcrumbs,
breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),

@@ -11,0 +11,0 @@ title: props.title

@@ -15,3 +15,3 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

const packageJson = {
"version": "3.3.1"
"version": "3.4.0"
};

@@ -36,6 +36,16 @@ function getOrigin() {

function formatBreadcrumbs(breadcrumbs) {
return breadcrumbs.slice(1).map(directory => directory.name).join('/');
}
function prependPath(path, component) {
if (!path) return component;
return `${path}/${component}`;
}
/**
* Class to easily generate generic views for Provider plugins
*/
var _updateFilesAndFolders = /*#__PURE__*/_classPrivateFieldLooseKey("updateFilesAndFolders");
var _list = /*#__PURE__*/_classPrivateFieldLooseKey("list");
var _listFilesAndFolders = /*#__PURE__*/_classPrivateFieldLooseKey("listFilesAndFolders");
var _recursivelyListAllFiles = /*#__PURE__*/_classPrivateFieldLooseKey("recursivelyListAllFiles");
export default class ProviderView extends View {

@@ -49,5 +59,11 @@ /**

// set default options
Object.defineProperty(this, _updateFilesAndFolders, {
value: _updateFilesAndFolders2
Object.defineProperty(this, _recursivelyListAllFiles, {
value: _recursivelyListAllFiles2
});
Object.defineProperty(this, _listFilesAndFolders, {
value: _listFilesAndFolders2
});
Object.defineProperty(this, _list, {
value: _list2
});
const defaultOptions = {

@@ -57,3 +73,4 @@ viewType: 'list',

showFilter: true,
showBreadcrumbs: true
showBreadcrumbs: true,
loadAllFiles: false
};

@@ -85,3 +102,3 @@

folders: [],
directories: [],
breadcrumbs: [],
filterInput: '',

@@ -98,35 +115,69 @@ isSearchVisible: false,

/**
* Based on folder ID, fetch a new folder and update it to state
* Select a folder based on its id: fetches the folder and then updates state with its contents
* TODO rename to something better like selectFolder or navigateToFolder (breaking change?)
*
* @param {string} id Folder id
* @param {string} requestPath
* the path we need to use when sending list request to companion (for some providers it's different from ID)
* @param {string} name used in the UI and to build the absDirPath
* @returns {Promise} Folders/files in folder
*/
async getFolder(id, name) {
async getFolder(requestPath, name) {
const controller = new AbortController();
const cancelRequest = () => {
controller.abort();
this.clearSelection();
};
this.plugin.uppy.on('dashboard:close-panel', cancelRequest);
this.plugin.uppy.on('cancel-all', cancelRequest);
this.setLoading(true);
try {
const res = await this.provider.list(id);
const folders = [];
const files = [];
let updatedDirectories;
const state = this.plugin.getPluginState();
const index = state.directories.findIndex(dir => id === dir.id);
this.lastCheckbox = undefined;
let {
breadcrumbs
} = this.plugin.getPluginState();
const index = breadcrumbs.findIndex(dir => requestPath === dir.requestPath);
if (index !== -1) {
updatedDirectories = state.directories.slice(0, index + 1);
// means we navigated back to a known directory (already in the stack), so cut the stack off there
breadcrumbs = breadcrumbs.slice(0, index + 1);
} else {
updatedDirectories = state.directories.concat([{
id,
title: name
}]);
// we have navigated into a new (unknown) folder, add it to the stack
breadcrumbs = [...breadcrumbs, {
requestPath,
name
}];
}
this.username = res.username || this.username;
_classPrivateFieldLooseBase(this, _updateFilesAndFolders)[_updateFilesAndFolders](res, files, folders);
let files = [];
let folders = [];
do {
const {
files: newFiles,
folders: newFolders
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({
requestPath,
breadcrumbs,
signal: controller.signal
});
files = files.concat(newFiles);
folders = folders.concat(newFolders);
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', {
numFiles: files.length + folders.length
}));
} while (this.opts.loadAllFiles && this.nextPagePath);
this.plugin.setPluginState({
directories: updatedDirectories,
folders,
files,
breadcrumbs,
filterInput: ''
});
this.lastCheckbox = undefined;
} catch (err) {
var _err$cause;
if (((_err$cause = err.cause) == null ? void 0 : _err$cause.name) === 'AbortError') {
// Expected, user clicked “cancel”
return;
}
this.handleError(err);
} finally {
this.setLoading(false);
this.plugin.uppy.off('dashboard:close-panel', cancelRequest);
this.plugin.uppy.off('cancel-all', cancelRequest);
}

@@ -162,3 +213,3 @@ }

folders: [],
directories: [],
breadcrumbs: [],
filterInput: ''

@@ -226,12 +277,24 @@ };

async handleScroll(event) {
const path = this.nextPagePath || null;
if (this.shouldHandleScroll(event) && path) {
const requestPath = this.nextPagePath || null;
if (this.shouldHandleScroll(event) && requestPath) {
this.isHandlingScroll = true;
try {
const response = await this.provider.list(path);
const {
files,
folders
folders,
breadcrumbs
} = this.plugin.getPluginState();
_classPrivateFieldLooseBase(this, _updateFilesAndFolders)[_updateFilesAndFolders](response, files, folders);
const {
files: newFiles,
folders: newFolders
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({
requestPath,
breadcrumbs
});
const combinedFiles = files.concat(newFiles);
const combinedFolders = folders.concat(newFolders);
this.plugin.setPluginState({
folders: combinedFolders,
files: combinedFiles
});
} catch (error) {

@@ -244,17 +307,2 @@ this.handleError(error);

}
async recursivelyListAllFiles(path, queue, onFiles) {
let curPath = path;
while (curPath) {
const res = await this.provider.list(curPath);
curPath = res.nextPagePath;
const files = res.items.filter(item => !item.isFolder);
const folders = res.items.filter(item => item.isFolder);
onFiles(files);
// recursively queue call to self for each folder
const promises = folders.map(async folder => queue.add(async () => this.recursivelyListAllFiles(folder.requestPath, queue, onFiles)));
await Promise.all(promises); // in case we get an error
}
}
async donePicking() {

@@ -268,8 +316,13 @@ this.setLoading(true);

const newFiles = [];
for (const file of currentSelection) {
if (file.isFolder) {
const {
requestPath,
name
} = file;
for (const selectedItem of currentSelection) {
const {
requestPath
} = selectedItem;
const withRelDirPath = newItem => ({
...newItem,
// calculate the file's path relative to the user's selected item's path
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, '')
});
if (selectedItem.isFolder) {
let isEmpty = true;

@@ -289,3 +342,3 @@ let numNewFiles = 0;

if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
newFiles.push(newFile);
newFiles.push(withRelDirPath(newFile));
numNewFiles++;

@@ -299,3 +352,9 @@ this.setLoading(this.plugin.uppy.i18n('addedNumFiles', {

};
await this.recursivelyListAllFiles(requestPath, queue, onFiles);
await _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({
requestPath,
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name),
relDirPath: selectedItem.name,
queue,
onFiles
});
await queue.onIdle();

@@ -307,3 +366,3 @@ let message;

message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: name
folder: selectedItem.name
});

@@ -316,3 +375,3 @@ } else {

smart_count: numNewFiles,
folder: name
folder: selectedItem.name
});

@@ -322,3 +381,3 @@ }

} else {
newFiles.push(file);
newFiles.push(withRelDirPath(selectedItem));
}

@@ -381,3 +440,3 @@ }

getFolder: this.getFolder,
directories: this.plugin.getPluginState().directories,
breadcrumbs: this.plugin.getPluginState().breadcrumbs,
pluginIcon: this.plugin.icon,

@@ -448,5 +507,43 @@ title: this.plugin.title,

}
function _updateFilesAndFolders2(res, files, folders) {
this.nextPagePath = res.nextPagePath;
res.items.forEach(item => {
async function _list2(_ref) {
let {
requestPath,
absDirPath,
signal
} = _ref;
const {
username,
nextPagePath,
items
} = await this.provider.list(requestPath, {
signal
});
this.username = username || this.username;
return {
items: items.map(item => ({
...item,
absDirPath
})),
nextPagePath
};
}
async function _listFilesAndFolders2(_ref2) {
let {
requestPath,
breadcrumbs,
signal
} = _ref2;
const absDirPath = formatBreadcrumbs(breadcrumbs);
const {
items,
nextPagePath
} = await _classPrivateFieldLooseBase(this, _list)[_list]({
requestPath,
absDirPath,
signal
});
this.nextPagePath = nextPagePath;
const files = [];
const folders = [];
items.forEach(item => {
if (item.isFolder) {

@@ -458,7 +555,37 @@ folders.push(item);

});
this.plugin.setPluginState({
folders,
files
});
return {
files,
folders
};
}
async function _recursivelyListAllFiles2(_ref3) {
let {
requestPath,
absDirPath,
relDirPath,
queue,
onFiles
} = _ref3;
let curPath = requestPath;
while (curPath) {
const res = await _classPrivateFieldLooseBase(this, _list)[_list]({
requestPath: curPath,
absDirPath
});
curPath = res.nextPagePath;
const files = res.items.filter(item => !item.isFolder);
const folders = res.items.filter(item => item.isFolder);
onFiles(files);
// recursively queue call to self for each folder
const promises = folders.map(async folder => queue.add(async () => _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({
requestPath: folder.requestPath,
absDirPath: prependPath(absDirPath, folder.name),
relDirPath: prependPath(relDirPath, folder.name),
queue,
onFiles
})));
await Promise.all(promises); // in case we get an error
}
}
ProviderView.VERSION = packageJson.version;

@@ -10,3 +10,3 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

const packageJson = {
"version": "3.3.1"
"version": "3.4.0"
};

@@ -56,3 +56,3 @@ /**

folders: [],
directories: [],
breadcrumbs: [],
filterInput: '',

@@ -59,0 +59,0 @@ currentSelection: [],

@@ -174,2 +174,7 @@ import getFileType from '@uppy/utils/lib/getFileType';

}
// add relativePath similar to non-remote files: https://github.com/transloadit/uppy/pull/4486#issuecomment-1579203717
if (file.relDirPath != null) tagFile.meta.relativePath = file.relDirPath ? `${file.relDirPath}/${tagFile.name}` : null;
// and absolutePath (with leading slash) https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
if (file.absDirPath != null) tagFile.meta.absolutePath = file.absDirPath ? `/${file.absDirPath}/${tagFile.name}` : `/${tagFile.name}`;
return tagFile;

@@ -176,0 +181,0 @@ }

{
"name": "@uppy/provider-views",
"description": "View library for Uppy remote provider plugins.",
"version": "3.3.1",
"version": "3.4.0",
"license": "MIT",

@@ -23,3 +23,3 @@ "main": "lib/index.js",

"dependencies": {
"@uppy/utils": "^5.4.0",
"@uppy/utils": "^5.4.1",
"classnames": "^2.2.6",

@@ -31,4 +31,4 @@ "nanoid": "^4.0.0",

"peerDependencies": {
"@uppy/core": "^3.2.1"
"@uppy/core": "^3.3.1"
}
}

@@ -29,3 +29,3 @@ # @uppy/provider-views

this.provider.fetchPreAuthToken(),
this.view.getFolder('root', '/'),
this.view.getFolder('root'),
])

@@ -32,0 +32,0 @@ }

@@ -21,3 +21,3 @@ import { h, Fragment } from 'preact'

export default (props) => {
const { getFolder, title, breadcrumbsIcon, directories } = props
const { getFolder, title, breadcrumbsIcon, breadcrumbs } = props

@@ -28,8 +28,8 @@ return (

{
directories.map((directory, i) => (
breadcrumbs.map((directory, i) => (
<Breadcrumb
key={directory.id}
getFolder={() => getFolder(directory.id)}
title={i === 0 ? title : directory.title}
isLast={i + 1 === directories.length}
getFolder={() => getFolder(directory.requestPath)}
title={i === 0 ? title : directory.name}
isLast={i + 1 === breadcrumbs.length}
/>

@@ -36,0 +36,0 @@ ))

@@ -41,5 +41,5 @@ import { h } from 'preact'

const { alt } = props
return <img src={itemIconString} alt={alt} />
return <img src={itemIconString} alt={alt} loading="lazy" />
}
}
}

@@ -9,3 +9,3 @@ import User from './User.jsx'

getFolder: props.getFolder,
directories: props.directories,
breadcrumbs: props.breadcrumbs,
breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),

@@ -12,0 +12,0 @@ title: props.title,

@@ -35,2 +35,11 @@ import { h } from 'preact'

function formatBreadcrumbs (breadcrumbs) {
return breadcrumbs.slice(1).map((directory) => directory.name).join('/')
}
function prependPath (path, component) {
if (!path) return component
return `${path}/${component}`
}
/**

@@ -54,2 +63,3 @@ * Class to easily generate generic views for Provider plugins

showBreadcrumbs: true,
loadAllFiles: false,
}

@@ -78,3 +88,3 @@

folders: [],
directories: [],
breadcrumbs: [],
filterInput: '',

@@ -91,5 +101,26 @@ isSearchVisible: false,

#updateFilesAndFolders (res, files, folders) {
this.nextPagePath = res.nextPagePath
res.items.forEach((item) => {
async #list ({ requestPath, absDirPath, signal }) {
const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal })
this.username = username || this.username
return {
items: items.map((item) => ({
...item,
absDirPath,
})),
nextPagePath,
}
}
async #listFilesAndFolders ({ requestPath, breadcrumbs, signal }) {
const absDirPath = formatBreadcrumbs(breadcrumbs)
const { items, nextPagePath } = await this.#list({ requestPath, absDirPath, signal })
this.nextPagePath = nextPagePath
const files = []
const folders = []
items.forEach((item) => {
if (item.isFolder) {

@@ -102,36 +133,66 @@ folders.push(item)

this.plugin.setPluginState({ folders, files })
return { files, folders }
}
/**
* Based on folder ID, fetch a new folder and update it to state
* Select a folder based on its id: fetches the folder and then updates state with its contents
* TODO rename to something better like selectFolder or navigateToFolder (breaking change?)
*
* @param {string} id Folder id
* @param {string} requestPath
* the path we need to use when sending list request to companion (for some providers it's different from ID)
* @param {string} name used in the UI and to build the absDirPath
* @returns {Promise} Folders/files in folder
*/
async getFolder (id, name) {
async getFolder (requestPath, name) {
const controller = new AbortController()
const cancelRequest = () => {
controller.abort()
this.clearSelection()
}
this.plugin.uppy.on('dashboard:close-panel', cancelRequest)
this.plugin.uppy.on('cancel-all', cancelRequest)
this.setLoading(true)
try {
const res = await this.provider.list(id)
const folders = []
const files = []
let updatedDirectories
this.lastCheckbox = undefined
const state = this.plugin.getPluginState()
const index = state.directories.findIndex((dir) => id === dir.id)
let { breadcrumbs } = this.plugin.getPluginState()
const index = breadcrumbs.findIndex((dir) => requestPath === dir.requestPath)
if (index !== -1) {
updatedDirectories = state.directories.slice(0, index + 1)
// means we navigated back to a known directory (already in the stack), so cut the stack off there
breadcrumbs = breadcrumbs.slice(0, index + 1)
} else {
updatedDirectories = state.directories.concat([{ id, title: name }])
// we have navigated into a new (unknown) folder, add it to the stack
breadcrumbs = [...breadcrumbs, { requestPath, name }]
}
this.username = res.username || this.username
this.#updateFilesAndFolders(res, files, folders)
this.plugin.setPluginState({ directories: updatedDirectories, filterInput: '' })
this.lastCheckbox = undefined
let files = []
let folders = []
do {
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
requestPath, breadcrumbs, signal: controller.signal,
})
files = files.concat(newFiles)
folders = folders.concat(newFolders)
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: files.length + folders.length }))
} while (
this.opts.loadAllFiles && this.nextPagePath
)
this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' })
} catch (err) {
if (err.cause?.name === 'AbortError') {
// Expected, user clicked “cancel”
return
}
this.handleError(err)
} finally {
this.setLoading(false)
this.plugin.uppy.off('dashboard:close-panel', cancelRequest)
this.plugin.uppy.off('cancel-all', cancelRequest)
}

@@ -169,3 +230,3 @@ }

folders: [],
directories: [],
breadcrumbs: [],
filterInput: '',

@@ -229,12 +290,18 @@ }

async handleScroll (event) {
const path = this.nextPagePath || null
const requestPath = this.nextPagePath || null
if (this.shouldHandleScroll(event) && path) {
if (this.shouldHandleScroll(event) && requestPath) {
this.isHandlingScroll = true
try {
const response = await this.provider.list(path)
const { files, folders } = this.plugin.getPluginState()
const { files, folders, breadcrumbs } = this.plugin.getPluginState()
this.#updateFilesAndFolders(response, files, folders)
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
requestPath, breadcrumbs,
})
const combinedFiles = files.concat(newFiles)
const combinedFolders = folders.concat(newFolders)
this.plugin.setPluginState({ folders: combinedFolders, files: combinedFiles })
} catch (error) {

@@ -248,7 +315,7 @@ this.handleError(error)

async recursivelyListAllFiles (path, queue, onFiles) {
let curPath = path
async #recursivelyListAllFiles ({ requestPath, absDirPath, relDirPath, queue, onFiles }) {
let curPath = requestPath
while (curPath) {
const res = await this.provider.list(curPath)
const res = await this.#list({ requestPath: curPath, absDirPath })
curPath = res.nextPagePath

@@ -263,3 +330,9 @@

const promises = folders.map(async (folder) => queue.add(async () => (
this.recursivelyListAllFiles(folder.requestPath, queue, onFiles)
this.#recursivelyListAllFiles({
requestPath: folder.requestPath,
absDirPath: prependPath(absDirPath, folder.name),
relDirPath: prependPath(relDirPath, folder.name),
queue,
onFiles,
})
)))

@@ -278,5 +351,13 @@ await Promise.all(promises) // in case we get an error

for (const file of currentSelection) {
if (file.isFolder) {
const { requestPath, name } = file
for (const selectedItem of currentSelection) {
const { requestPath } = selectedItem
const withRelDirPath = (newItem) => ({
...newItem,
// calculate the file's path relative to the user's selected item's path
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, ''),
})
if (selectedItem.isFolder) {
let isEmpty = true

@@ -296,3 +377,3 @@ let numNewFiles = 0

if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
newFiles.push(newFile)
newFiles.push(withRelDirPath(newFile))
numNewFiles++

@@ -305,3 +386,9 @@ this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { numFiles: numNewFiles }))

await this.recursivelyListAllFiles(requestPath, queue, onFiles)
await this.#recursivelyListAllFiles({
requestPath,
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name),
relDirPath: selectedItem.name,
queue,
onFiles,
})
await queue.onIdle()

@@ -314,3 +401,3 @@

message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: name,
folder: selectedItem.name,
})

@@ -322,3 +409,3 @@ } else {

message = this.plugin.uppy.i18n('folderAdded', {
smart_count: numNewFiles, folder: name,
smart_count: numNewFiles, folder: selectedItem.name,
})

@@ -329,3 +416,3 @@ }

} else {
newFiles.push(file)
newFiles.push(withRelDirPath(selectedItem))
}

@@ -368,3 +455,3 @@ }

getFolder: this.getFolder,
directories: this.plugin.getPluginState().directories,
breadcrumbs: this.plugin.getPluginState().breadcrumbs,
pluginIcon: this.plugin.icon,

@@ -371,0 +458,0 @@ title: this.plugin.title,

@@ -49,3 +49,3 @@ import { h } from 'preact'

folders: [],
directories: [],
breadcrumbs: [],
filterInput: '',

@@ -52,0 +52,0 @@ currentSelection: [],

@@ -95,2 +95,7 @@ import getFileType from '@uppy/utils/lib/getFileType'

// add relativePath similar to non-remote files: https://github.com/transloadit/uppy/pull/4486#issuecomment-1579203717
if (file.relDirPath != null) tagFile.meta.relativePath = file.relDirPath ? `${file.relDirPath}/${tagFile.name}` : null
// and absolutePath (with leading slash) https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
if (file.absDirPath != null) tagFile.meta.absolutePath = file.absDirPath ? `/${file.absDirPath}/${tagFile.name}` : `/${tagFile.name}`
return tagFile

@@ -97,0 +102,0 @@ }

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

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