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

@casual-simulation/aux-records

Package Overview
Dependencies
Maintainers
2
Versions
196
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@casual-simulation/aux-records - npm Package Compare versions

Comparing version 3.1.14-alpha.3661157217 to 3.1.23-alpha.4227183169

RecordsHttpServer.d.ts

119

AuthController.d.ts

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

import { AddressType, AuthStore } from './AuthStore';
import { AddressType, AuthStore, AuthUser } from './AuthStore';
import { ServerError } from './Errors';
import { AuthMessenger } from './AuthMessenger';
import { RegexRule } from './Utils';
/**

@@ -62,2 +63,20 @@ * The number of miliseconds that a login request should be valid for before expiration.

listSessions(request: ListSessionsRequest): Promise<ListSessionsResult>;
/**
* Gets the information for a specific user.
* @param request The request.
*/
getUserInfo(request: GetUserInfoRequest): Promise<GetUserInfoResult>;
/**
* Attempts to update a user's metadata.
* @param request The request for the operation.
*/
updateUserInfo(request: UpdateUserInfoRequest): Promise<UpdateUserInfoResult>;
/**
* Lists the email rules that should be used.
*/
listEmailRules(): Promise<ListEmailRulesResult>;
/**
* Lists the SMS rules that should be used.
*/
listSmsRules(): Promise<ListSmsRulesResult>;
}

@@ -230,3 +249,3 @@ export interface LoginRequest {

/**
*
*The list of sessions.
*/

@@ -308,2 +327,98 @@ sessions: ListedSession[];

}
/**
* Defines an interface for requests to get a user's info.
*/
export interface GetUserInfoRequest {
/**
* The session key that should be used to authenticate the request.
*/
sessionKey: string;
/**
* The ID of the user whose info should be retrieved.
*/
userId: string;
}
export declare type GetUserInfoResult = GetUserInfoSuccess | GetUserInfoFailure;
export interface GetUserInfoSuccess {
success: true;
/**
* The ID of the user that was retrieved.
*/
userId: string;
/**
* The name of the user.
*/
name: string;
/**
* The URL of the avatar for the user.
*/
avatarUrl: string;
/**
* The URL of the avatar portrait for the user.
*/
avatarPortraitUrl: string;
/**
* The email address of the user.
*/
email: string;
/**
* The phone number of the user.
*/
phoneNumber: string;
}
export interface GetUserInfoFailure {
success: false;
errorCode: 'unacceptable_user_id' | ValidateSessionKeyFailure['errorCode'] | ServerError;
errorMessage: string;
}
/**
* Defines an interface for a request to update user info.
*/
export interface UpdateUserInfoRequest {
/**
* The session key that should be used to authenticate the request.
*/
sessionKey: string;
/**
* The ID of the user whose info should be updated.
*/
userId: string;
/**
* The new info for the user.
*/
update: Partial<Pick<AuthUser, 'name' | 'email' | 'phoneNumber' | 'avatarUrl' | 'avatarPortraitUrl'>>;
}
export declare type UpdateUserInfoResult = UpdateUserInfoSuccess | UpdateUserInfoFailure;
export interface UpdateUserInfoSuccess {
success: true;
/**
* The ID of the user that was retrieved.
*/
userId: string;
}
export interface UpdateUserInfoFailure {
success: false;
errorCode: 'unacceptable_user_id' | 'unacceptable_update' | ValidateSessionKeyFailure['errorCode'] | ServerError;
errorMessage: string;
}
export declare type ListEmailRulesResult = ListEmailRulesSuccess | ListEmailRulesFailure;
export interface ListEmailRulesSuccess {
success: true;
rules: RegexRule[];
}
export interface ListEmailRulesFailure {
success: false;
errorCode: ServerError;
errorMessage: string;
}
export declare type ListSmsRulesResult = ListSmsRulesSuccess | ListSmsRulesFailure;
export interface ListSmsRulesSuccess {
success: true;
rules: RegexRule[];
}
export interface ListSmsRulesFailure {
success: false;
errorCode: ServerError;
errorMessage: string;
}
//# sourceMappingURL=AuthController.d.ts.map

@@ -12,4 +12,5 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import { randomBytes } from 'tweetnacl';
import { hashPasswordWithSalt, verifyPasswordAgainstHashes, } from '@casual-simulation/crypto';
import { hashHighEntropyPasswordWithSalt, hashPasswordWithSalt, verifyPasswordAgainstHashes, } from '@casual-simulation/crypto';
import { fromByteArray } from 'base64-js';
import { cleanupObject, } from './Utils';
import { formatV1SessionKey, parseSessionKey, randomCode } from './AuthUtils';

@@ -271,3 +272,5 @@ /**

requestId: loginRequest.requestId,
secretHash: hashPasswordWithSalt(sessionSecret, sessionId),
// sessionSecret and sessionId are high-entropy (128 bits of random data)
// so we should use a hash that is optimized for high-entropy inputs.
secretHash: hashHighEntropyPasswordWithSalt(sessionSecret, sessionId),
grantedTimeMs: now,

@@ -653,3 +656,173 @@ revokeTimeMs: null,

}
/**
* Gets the information for a specific user.
* @param request The request.
*/
getUserInfo(request) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof request.userId !== 'string' || request.userId === '') {
return {
success: false,
errorCode: 'unacceptable_user_id',
errorMessage: 'The given userId is invalid. It must be a string.',
};
}
else if (typeof request.sessionKey !== 'string' ||
request.sessionKey === '') {
return {
success: false,
errorCode: 'unacceptable_session_key',
errorMessage: 'The given session key is invalid. It must be a string.',
};
}
try {
const keyResult = yield this.validateSessionKey(request.sessionKey);
if (keyResult.success === false) {
return keyResult;
}
else if (keyResult.userId !== request.userId) {
return {
success: false,
errorCode: 'invalid_key',
errorMessage: INVALID_KEY_ERROR_MESSAGE,
};
}
const result = yield this._store.findUser(request.userId);
if (!result) {
throw new Error('Unable to find user even though a valid session key was presented!');
}
return {
success: true,
userId: result.id,
name: result.name,
email: result.email,
phoneNumber: result.phoneNumber,
avatarPortraitUrl: result.avatarPortraitUrl,
avatarUrl: result.avatarUrl,
};
}
catch (err) {
console.error('[AuthController] Error ocurred while getting user info', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
});
}
/**
* Attempts to update a user's metadata.
* @param request The request for the operation.
*/
updateUserInfo(request) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof request.userId !== 'string' || request.userId === '') {
return {
success: false,
errorCode: 'unacceptable_user_id',
errorMessage: 'The given userId is invalid. It must be a string.',
};
}
else if (typeof request.sessionKey !== 'string' ||
request.sessionKey === '') {
return {
success: false,
errorCode: 'unacceptable_session_key',
errorMessage: 'The given session key is invalid. It must be a string.',
};
}
else if (typeof request.update !== 'object' ||
request.update === null ||
Array.isArray(request.update)) {
return {
success: false,
errorCode: 'unacceptable_update',
errorMessage: 'The given update is invalid. It must be an object.',
};
}
try {
const keyResult = yield this.validateSessionKey(request.sessionKey);
if (keyResult.success === false) {
return keyResult;
}
else if (keyResult.userId !== request.userId) {
return {
success: false,
errorCode: 'invalid_key',
errorMessage: INVALID_KEY_ERROR_MESSAGE,
};
}
const user = yield this._store.findUser(request.userId);
if (!user) {
throw new Error('Unable to find user even though a valid session key was presented!');
}
const cleaned = cleanupObject({
name: request.update.name,
avatarUrl: request.update.avatarUrl,
avatarPortraitUrl: request.update.avatarPortraitUrl,
email: request.update.email,
phoneNumber: request.update.phoneNumber,
});
yield this._store.saveUser(Object.assign(Object.assign({}, user), cleaned));
return {
success: true,
userId: user.id,
};
}
catch (err) {
console.error('[AuthController] Error ocurred while getting user info', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
});
}
/**
* Lists the email rules that should be used.
*/
listEmailRules() {
return __awaiter(this, void 0, void 0, function* () {
try {
const rules = yield this._store.listEmailRules();
return {
success: true,
rules,
};
}
catch (err) {
console.error('[AuthController] Error ocurred while listing email rules', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
});
}
/**
* Lists the SMS rules that should be used.
*/
listSmsRules() {
return __awaiter(this, void 0, void 0, function* () {
try {
const rules = yield this._store.listSmsRules();
return {
success: true,
rules,
};
}
catch (err) {
console.error('[AuthController] Error ocurred while listing email rules', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
});
}
}
//# sourceMappingURL=AuthController.js.map

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

import { RegexRule } from 'Utils';
import { ServerError } from './Errors';

@@ -82,2 +83,10 @@ /**

setCurrentLoginRequest(userId: string, requestId: string): Promise<void>;
/**
* Gets the list of email rules.
*/
listEmailRules(): Promise<RegexRule[]>;
/**
* Gets the list of SMS rules.
*/
listSmsRules(): Promise<RegexRule[]>;
}

@@ -84,0 +93,0 @@ export declare type AddressType = 'email' | 'phone';

4

FileRecordsController.d.ts

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

import { FileRecordsStore, AddFileFailure, MarkFileRecordAsUploadedFailure, EraseFileStoreResult } from './FileRecordsStore';
import { FileRecordsStore, AddFileFailure, MarkFileRecordAsUploadedFailure, EraseFileStoreResult, GetFileNameFromUrlResult } from './FileRecordsStore';
import { NotLoggedInError, ServerError } from './Errors';

@@ -20,2 +20,4 @@ import { RecordsController, ValidatePublicRecordKeyFailure } from './RecordsController';

markFileAsUploaded(recordName: string, fileName: string): Promise<FileUploadedResult>;
getFileNameFromUrl(fileUrl: string): Promise<GetFileNameFromUrlResult>;
getAllowedUploadHeaders(): string[];
}

@@ -22,0 +24,0 @@ /**

@@ -96,6 +96,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

catch (err) {
console.error('[FileRecordsController] An error occurred while recording a file:', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: err.toString(),
errorMessage: 'A server error occurred.',
};

@@ -142,6 +143,7 @@ }

catch (err) {
console.error('[FileRecordsController] An error occurred while erasing a file:', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: err.toString(),
errorMessage: 'A server error occurred.',
};

@@ -163,6 +165,7 @@ }

catch (err) {
console.error('[FileRecordsController] An error occurred while marking a file as uploaded:', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: err.toString(),
errorMessage: 'A server error occurred.',
};

@@ -172,3 +175,21 @@ }

}
getFileNameFromUrl(fileUrl) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield this._store.getFileNameFromUrl(fileUrl);
}
catch (err) {
console.error('[FileRecordsController] An error occurred while getting a file name:', err);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
});
}
getAllowedUploadHeaders() {
return this._store.getAllowedUploadHeaders();
}
}
//# sourceMappingURL=FileRecordsController.js.map

@@ -40,2 +40,11 @@ import { ServerError } from './Errors';

eraseFileRecord(recordName: string, fileName: string): Promise<EraseFileStoreResult>;
/**
* Attempts to get the record name and file name from the given URL.
* @param fileUrl The URL.
*/
getFileNameFromUrl(fileUrl: string): Promise<GetFileNameFromUrlResult>;
/**
* Gets the list of headers that should be allowed via CORS.
*/
getAllowedUploadHeaders(): string[];
}

@@ -158,2 +167,20 @@ export declare type GetFileRecordResult = GetFileRecordSuccess | GetFileRecordFailure;

}
export declare type GetFileNameFromUrlResult = GetFileNameFromUrlSuccess | GetFileNameFromUrlFailure;
export interface GetFileNameFromUrlSuccess {
success: true;
/**
* The name of the record that the URL references.
* Null if the URL contains no record name.
*/
recordName: string | null;
/**
* The name of the file that the URL references.
*/
fileName: string;
}
export interface GetFileNameFromUrlFailure {
success: false;
errorCode: ServerError | 'unacceptable_url';
errorMessage: string;
}
//# sourceMappingURL=FileRecordsStore.d.ts.map

@@ -14,2 +14,3 @@ export * from './RecordsController';

export * from './LivekitEvents';
export * from './RecordsHttpServer';
//# sourceMappingURL=index.d.ts.map

@@ -14,2 +14,3 @@ export * from './RecordsController';

export * from './LivekitEvents';
export * from './RecordsHttpServer';
//# sourceMappingURL=index.js.map

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

import { RegexRule } from './Utils';
import { AddressType, AuthLoginRequest, AuthSession, AuthStore, AuthUser, ListSessionsDataResult, SaveNewUserResult } from './AuthStore';

@@ -6,5 +7,9 @@ export declare class MemoryAuthStore implements AuthStore {

private _sessions;
private _emailRules;
private _smsRules;
get users(): AuthUser[];
get loginRequests(): AuthLoginRequest[];
get sessions(): AuthSession[];
get emailRules(): RegexRule[];
get smsRules(): RegexRule[];
saveUser(user: AuthUser): Promise<void>;

@@ -23,4 +28,6 @@ saveNewUser(user: AuthUser): Promise<SaveNewUserResult>;

listSessions(userId: string, expireTimeMs: number): Promise<ListSessionsDataResult>;
listEmailRules(): Promise<RegexRule[]>;
listSmsRules(): Promise<RegexRule[]>;
private _findUserIndex;
}
//# sourceMappingURL=MemoryAuthStore.d.ts.map

@@ -16,2 +16,4 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

this._sessions = [];
this._emailRules = [];
this._smsRules = [];
}

@@ -27,2 +29,8 @@ get users() {

}
get emailRules() {
return this._emailRules;
}
get smsRules() {
return this._smsRules;
}
saveUser(user) {

@@ -154,2 +162,12 @@ return __awaiter(this, void 0, void 0, function* () {

}
listEmailRules() {
return __awaiter(this, void 0, void 0, function* () {
return this._emailRules.slice();
});
}
listSmsRules() {
return __awaiter(this, void 0, void 0, function* () {
return this._smsRules.slice();
});
}
_findUserIndex(id) {

@@ -156,0 +174,0 @@ return this._users.findIndex((u) => u.id === id);

@@ -1,5 +0,7 @@

import { AddFileResult, EraseFileStoreResult, FileRecordsStore, GetFileRecordResult, MarkFileRecordAsUploadedResult, PresignFileUploadRequest, PresignFileUploadResult } from './FileRecordsStore';
import { AddFileResult, EraseFileStoreResult, FileRecordsStore, GetFileNameFromUrlResult, GetFileRecordResult, MarkFileRecordAsUploadedResult, PresignFileUploadRequest, PresignFileUploadResult } from './FileRecordsStore';
export declare class MemoryFileRecordsStore implements FileRecordsStore {
private _files;
private _fileUploadUrl;
presignFileUpload(request: PresignFileUploadRequest): Promise<PresignFileUploadResult>;
getFileNameFromUrl(fileUrl: string): Promise<GetFileNameFromUrlResult>;
getFileRecord(recordName: string, fileName: string): Promise<GetFileRecordResult>;

@@ -9,3 +11,4 @@ addFileRecord(recordName: string, fileName: string, publisherId: string, subjectId: string, sizeInBytes: number, description: string): Promise<AddFileResult>;

eraseFileRecord(recordName: string, fileName: string): Promise<EraseFileStoreResult>;
getAllowedUploadHeaders(): string[];
}
//# sourceMappingURL=MemoryFileRecordsStore.d.ts.map

@@ -13,6 +13,48 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

this._files = new Map();
this._fileUploadUrl = 'http://localhost:9191';
}
presignFileUpload(request) {
throw new Error('Method not implemented.');
return __awaiter(this, void 0, void 0, function* () {
return {
success: true,
uploadHeaders: Object.assign(Object.assign({}, request.headers), { 'record-name': request.recordName, 'content-type': request.fileMimeType }),
uploadMethod: 'POST',
uploadUrl: `${this._fileUploadUrl}/${request.recordName}/${request.fileName}`,
};
});
}
getFileNameFromUrl(fileUrl) {
return __awaiter(this, void 0, void 0, function* () {
if (fileUrl.startsWith(this._fileUploadUrl)) {
let recordNameAndFileName = fileUrl.slice(this._fileUploadUrl.length + 1);
let nextSlash = recordNameAndFileName.indexOf('/');
if (nextSlash < 0) {
return {
success: false,
errorCode: 'unacceptable_url',
errorMessage: 'The URL does not match an expected format.',
};
}
let recordName = recordNameAndFileName.slice(0, nextSlash);
let fileName = recordNameAndFileName.slice(nextSlash + 1);
if (recordName && fileName) {
return {
success: true,
recordName,
fileName,
};
}
return {
success: false,
errorCode: 'unacceptable_url',
errorMessage: 'The URL does not match an expected format.',
};
}
return {
success: false,
errorCode: 'unacceptable_url',
errorMessage: 'The URL does not match an expected format.',
};
});
}
getFileRecord(recordName, fileName) {

@@ -98,3 +140,6 @@ return __awaiter(this, void 0, void 0, function* () {

}
getAllowedUploadHeaders() {
return ['record-name', 'content-type'];
}
}
//# sourceMappingURL=MemoryFileRecordsStore.js.map
{
"name": "@casual-simulation/aux-records",
"version": "3.1.14-alpha.3661157217",
"version": "3.1.23-alpha.4227183169",
"description": "Helpers and managers used by the CasualOS records system.",

@@ -40,7 +40,7 @@ "keywords": [],

"dependencies": {
"@casual-simulation/crypto": "^3.1.14-alpha.3661157217",
"@casual-simulation/crypto": "^3.1.23-alpha.4227183169",
"livekit-server-sdk": "1.0.2",
"tweetnacl": "1.0.3"
},
"gitHead": "1a4688a5c80f36121fd0458346f944ea81fec2dd"
"gitHead": "b5a541f95bcfb41de2e2c2a04a5cff19d70ec09f"
}

@@ -11,3 +11,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import { toBase64String, fromBase64String } from './Utils';
import { hashPasswordWithSalt, } from '@casual-simulation/crypto';
import { hashHighEntropyPasswordWithSalt, hashPasswordWithSalt, } from '@casual-simulation/crypto';
import { randomBytes } from 'tweetnacl';

@@ -63,3 +63,3 @@ import { fromByteArray } from 'base64-js';

const salt = record.secretSalt;
const passwordHash = hashPasswordWithSalt(password, salt);
const passwordHash = hashHighEntropyPasswordWithSalt(password, salt);
yield this._store.addRecordKey({

@@ -81,3 +81,3 @@ recordName: name,

const salt = fromByteArray(randomBytes(16));
const passwordHash = hashPasswordWithSalt(password, salt);
const passwordHash = hashHighEntropyPasswordWithSalt(password, salt);
yield this._store.addRecord({

@@ -138,10 +138,11 @@ name,

}
const hash = hashPasswordWithSalt(password, record.secretSalt);
// Check v2 hashes first because they are much quicker to check
const hashV2 = hashHighEntropyPasswordWithSalt(password, record.secretSalt);
let valid = false;
let resultPolicy = DEFAULT_RECORD_KEY_POLICY;
if (record.secretHashes.some((h) => h === hash)) {
if (record.secretHashes.some((h) => h === hashV2)) {
valid = true;
}
else {
const key = yield this._store.getRecordKeyByRecordAndHash(name, hash);
const key = yield this._store.getRecordKeyByRecordAndHash(name, hashV2);
if (!!key) {

@@ -151,2 +152,16 @@ resultPolicy = key.policy;

}
else {
// Check v1 hashes
const hash = hashPasswordWithSalt(password, record.secretSalt);
if (record.secretHashes.some((h) => h === hash)) {
valid = true;
}
else {
const key = yield this._store.getRecordKeyByRecordAndHash(name, hash);
if (!!key) {
resultPolicy = key.policy;
valid = true;
}
}
}
}

@@ -153,0 +168,0 @@ if (resultPolicy !== policy) {

@@ -79,2 +79,31 @@ /**

}): 401 | 501 | 404 | 400 | 200 | 403 | 500;
/**
* Clones the given object into a new object that only has non-null and not-undefined properties.
* @param obj The object to cleanup.
*/
export declare function cleanupObject<T extends Object>(obj: T): Partial<T>;
/**
* Tries to parse the given JSON string into a JavaScript Value.
* @param json The JSON to parse.
*/
export declare function tryParseJson(json: string): JsonParseResult;
export declare type JsonParseResult = JsonParseSuccess | JsonParseFailure;
export interface JsonParseSuccess {
success: true;
value: any;
}
export interface JsonParseFailure {
success: false;
error: Error;
}
export interface RegexRule {
type: 'allow' | 'deny';
pattern: string;
}
/**
* Determines if the given value matches the given list of rules.
* @param value The value to test.
* @param rules The rules that the value should be tested against.
*/
export declare function isStringValid(value: string, rules: RegexRule[]): boolean;
//# sourceMappingURL=Utils.d.ts.map
import { fromByteArray, toByteArray } from 'base64-js';
import { padStart, sortBy } from 'lodash';
import { omitBy, padStart, sortBy } from 'lodash';
import { sha256, hmac } from 'hash.js';

@@ -237,2 +237,5 @@ /**

}
else if (response.errorCode === 'operation_not_found') {
return 404;
}
else if (response.errorCode === 'session_already_revoked') {

@@ -250,2 +253,5 @@ return 200;

}
else if (response.errorCode === 'invalid_origin') {
return 403;
}
else if (response.errorCode === 'session_expired') {

@@ -281,2 +287,5 @@ return 401;

}
else if (response.errorCode === 'unacceptable_request') {
return 400;
}
else if (response.errorCode === 'address_type_not_supported') {

@@ -297,2 +306,57 @@ return 501;

}
/**
* Clones the given object into a new object that only has non-null and not-undefined properties.
* @param obj The object to cleanup.
*/
export function cleanupObject(obj) {
return omitBy(obj, (o) => typeof o === 'undefined' || o === null);
}
/**
* Tries to parse the given JSON string into a JavaScript Value.
* @param json The JSON to parse.
*/
export function tryParseJson(json) {
try {
return {
success: true,
value: JSON.parse(json),
};
}
catch (err) {
return {
success: false,
error: err,
};
}
}
/**
* Determines if the given value matches the given list of rules.
* @param value The value to test.
* @param rules The rules that the value should be tested against.
*/
export function isStringValid(value, rules) {
if (rules.length <= 0) {
return true;
}
const regexRules = rules.map((r) => ({
type: r.type,
pattern: new RegExp(r.pattern, 'i'),
}));
let good = false;
for (let rule of regexRules) {
if (rule.type === 'allow') {
if (rule.pattern.test(value)) {
good = true;
break;
}
}
else if (rule.type === 'deny') {
if (rule.pattern.test(value)) {
good = false;
break;
}
}
}
return good;
}
//# sourceMappingURL=Utils.js.map

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

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