Socket
Socket
Sign inDemoInstall

@juzi/whatsapp-web.js

Package Overview
Dependencies
Maintainers
2
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@juzi/whatsapp-web.js - npm Package Compare versions

Comparing version 1.19.6 to 1.20.0

66

example.js

@@ -1,6 +0,10 @@

const { Client, Location, List, Buttons, LocalAuth} = require('./index');
const { Client, Location, List, Buttons, LocalAuth } = require('./index');
const client = new Client({
authStrategy: new LocalAuth(),
puppeteer: { headless: false }
// proxyAuthentication: { username: 'username', password: 'password' },
puppeteer: {
// args: ['--proxy-server=proxy-server-that-requires-authentication.example.com'],
headless: false
}
});

@@ -192,7 +196,7 @@

} else if (msg.body === '!buttons') {
let button = new Buttons('Button body',[{body:'bt1'},{body:'bt2'},{body:'bt3'}],'title','footer');
let button = new Buttons('Button body', [{ body: 'bt1' }, { body: 'bt2' }, { body: 'bt3' }], 'title', 'footer');
client.sendMessage(msg.from, button);
} else if (msg.body === '!list') {
let sections = [{title:'sectionTitle',rows:[{title:'ListItem1', description: 'desc'},{title:'ListItem2'}]}];
let list = new List('List body','btnText',sections,'Title','footer');
let sections = [{ title: 'sectionTitle', rows: [{ title: 'ListItem1', description: 'desc' }, { title: 'ListItem2' }] }];
let list = new List('List body', 'btnText', sections, 'Title', 'footer');
client.sendMessage(msg.from, list);

@@ -235,3 +239,3 @@ } else if (msg.body === '!reaction') {

if(ack == 3) {
if (ack == 3) {
// The message was read

@@ -259,3 +263,3 @@ }

client.on('change_state', state => {
console.log('CHANGE STATE', state );
console.log('CHANGE STATE', state);
});

@@ -280,1 +284,49 @@

client.on('contact_changed', async (message, oldId, newId, isContact) => {
/** The time the event occurred. */
const eventTime = (new Date(message.timestamp * 1000)).toLocaleString();
console.log(
`The contact ${oldId.slice(0, -5)}` +
`${!isContact ? ' that participates in group ' +
`${(await client.getChatById(message.to ?? message.from)).name} ` : ' '}` +
`changed their phone number\nat ${eventTime}.\n` +
`Their new phone number is ${newId.slice(0, -5)}.\n`);
/**
* Information about the {@name message}:
*
* 1. If a notification was emitted due to a group participant changing their phone number:
* {@name message.author} is a participant's id before the change.
* {@name message.recipients[0]} is a participant's id after the change (a new one).
*
* 1.1 If the contact who changed their number WAS in the current user's contact list at the time of the change:
* {@name message.to} is a group chat id the event was emitted in.
* {@name message.from} is a current user's id that got an notification message in the group.
* Also the {@name message.fromMe} is TRUE.
*
* 1.2 Otherwise:
* {@name message.from} is a group chat id the event was emitted in.
* {@name message.to} is @type {undefined}.
* Also {@name message.fromMe} is FALSE.
*
* 2. If a notification was emitted due to a contact changing their phone number:
* {@name message.templateParams} is an array of two user's ids:
* the old (before the change) and a new one, stored in alphabetical order.
* {@name message.from} is a current user's id that has a chat with a user,
* whos phone number was changed.
* {@name message.to} is a user's id (after the change), the current user has a chat with.
*/
});
client.on('group_admin_changed', (notification) => {
if (notification.type === 'promote') {
/**
* Emitted when a current user is promoted to an admin.
* {@link notification.author} is a user who performs the action of promoting/demoting the current user.
*/
console.log(`You were promoted by ${notification.author}`);
} else if (notification.type === 'demote')
/** Emitted when a current user is demoted to a regular user. */
console.log(`You were demoted by ${notification.author}`);
});

@@ -157,2 +157,8 @@

/** Sets the current user's profile picture */
setProfilePicture(media: MessageMedia): Promise<boolean>
/** Deletes the current user's profile picture */
deleteProfilePicture(): Promise<boolean>
/** Generic event */

@@ -202,2 +208,8 @@ on(event: string, listener: (...args: any) => void): this

/** Emitted when a current user is promoted to an admin or demoted to a regular user */
on(event: 'group_admin_changed', listener: (
/** GroupNotification with more information about the action */
notification: GroupNotification
) => void): this
/** Emitted when group settings are updated, such as subject, description or picture */

@@ -209,2 +221,14 @@ on(event: 'group_update', listener: (

/** Emitted when a contact or a group participant changed their phone number. */
on(event: 'contact_changed', listener: (
/** Message with more information about the event. */
message: Message,
/** Old user's id. */
oldId : String,
/** New user's id. */
newId : String,
/** Indicates if a contact or a group participant changed their phone number. */
isContact : Boolean
) => void): this
/** Emitted when media has been uploaded for a message sent by the client */

@@ -229,2 +253,8 @@ on(event: 'media_uploaded', listener: (

) => void): this
/** Emitted when a chat unread count changes */
on(event: 'unread_count', listener: (
/** The chat that was affected */
chat: Chat
) => void): this

@@ -260,2 +290,18 @@ /** Emitted when a new message is created, which may include the current user's own messages */

/** Emitted when a chat is removed */
on(event: 'chat_removed', listener: (
/** The chat that was removed */
chat: Chat
) => void): this
/** Emitted when a chat is archived/unarchived */
on(event: 'chat_archived', listener: (
/** The chat that was archived/unarchived */
chat: Chat,
/** State the chat is currently in */
currState: boolean,
/** State the chat was previously in */
prevState: boolean
) => void): this
/** Emitted when loading screen is appearing */

@@ -358,3 +404,5 @@ on(event: 'loading_screen', listener: (percent: string, message: string) => void): this

* @default 'ffmpeg' */
ffmpegPath?: string
ffmpegPath?: string,
/** Object with proxy autentication requirements @default: undefined */
proxyAuthentication?: {username: string, password: string} | undefined
}

@@ -516,4 +564,6 @@

MEDIA_UPLOADED = 'media_uploaded',
CONTACT_CHANGED = 'contact_changed',
GROUP_JOIN = 'group_join',
GROUP_LEAVE = 'group_leave',
GROUP_ADMIN_CHANGED = 'group_admin_changed',
GROUP_UPDATE = 'group_update',

@@ -656,2 +706,3 @@ QR_RECEIVED = 'qr',

* hasQuotedMsg: false,
* hasReaction: false,
* location: undefined,

@@ -686,2 +737,4 @@ * mentionedIds: []

hasQuotedMsg: boolean,
/** Indicates whether there are reactions to the message */
hasReaction: boolean,
/** Indicates the duration of the message in seconds */

@@ -788,2 +841,6 @@ duration: string,

getPayment: () => Promise<Payment>,
/**
* Gets the reactions associated with the given message
*/
getReactions: () => Promise<ReactionList[]>,
}

@@ -1041,2 +1098,4 @@

unreadCount: number,
/** Last message fo chat */
lastMessage: Message,

@@ -1185,2 +1244,6 @@ /** Archives this chat */

leave: () => Promise<void>;
/** Sets the group's picture.*/
setPicture: (media: MessageMedia) => Promise<boolean>;
/** Deletes the group's picture */
deletePicture: () => Promise<boolean>;
}

@@ -1389,4 +1452,11 @@

}
export type ReactionList = {
id: string,
aggregateEmoji: string,
hasReactionByMe: boolean,
senders: Array<Reaction>
}
}
export = WAWebJS

2

package.json
{
"name": "@juzi/whatsapp-web.js",
"version": "1.19.6",
"version": "1.20.0",
"description": "Library for interacting with the WhatsApp Web API ",

@@ -5,0 +5,0 @@ "main": "./index.js",

@@ -18,3 +18,3 @@ 'use strict';

const ContactFactory = require('./factories/ContactFactory');
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction, Chat } = require('./structures');
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');

@@ -38,2 +38,3 @@ const NoAuth = require('./authStrategies/NoAuth');

* @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
* @param {object} options.proxyAuthentication - Proxy Authentication object.
*

@@ -55,2 +56,4 @@ * @fires Client#qr

* @fires Client#change_state
* @fires Client#contact_changed
* @fires Client#group_admin_changed
*/

@@ -111,2 +114,6 @@ class Client extends EventEmitter {

}
if (this.options.proxyAuthentication !== undefined) {
await page.authenticate(this.options.proxyAuthentication);
}

@@ -329,2 +336,9 @@ await page.setUserAgent(this.options.userAgent);

this.emit(Events.GROUP_LEAVE, notification);
} else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
/**
* Emitted when a current user is promoted to an admin or demoted to a regular user.
* @event Client#group_admin_changed
* @param {GroupNotification} notification GroupNotification with more information about the action
*/
this.emit(Events.GROUP_ADMIN_CHANGED, notification);
} else {

@@ -389,2 +403,32 @@ /**

/**
* The event notification that is received when one of
* the group participants changes thier phone number.
*/
const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
/**
* The event notification that is received when one of
* the contacts changes thier phone number.
*/
const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
if (isParticipant || isContact) {
/** {@link GroupNotification} object does not provide enough information about this event, so a {@link Message} object is used. */
const message = new Message(this, msg);
const newId = isParticipant ? msg.recipients[0] : msg.to;
const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
/**
* Emitted when a contact or a group participant changes their phone number.
* @event Client#contact_changed
* @param {Message} message Message with more information about the event.
* @param {String} oldId The user's id (an old one) who changed their phone number
* and who triggered the notification.
* @param {String} newId The user's new id after the change.
* @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
*/
this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
}
});

@@ -421,2 +465,11 @@

await page.exposeFunction('onChatUnreadCountEvent', async (data) =>{
const chat = await this.getChatById(data.id);
/**
* Emitted when the chat unread count changes
*/
this.emit(Events.UNREAD_COUNT, chat);
});
await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => {

@@ -527,2 +580,22 @@

await page.exposeFunction('onRemoveChatEvent', (chat) => {
/**
* Emitted when a chat is removed
* @event Client#chat_removed
* @param {Chat} chat
*/
this.emit(Events.CHAT_REMOVED, new Chat(this, chat));
});
await page.exposeFunction('onArchiveChatEvent', (chat, currState, prevState) => {
/**
* Emitted when a chat is archived/unarchived
* @event Client#chat_archived
* @param {Chat} chat
* @param {boolean} currState
* @param {boolean} prevState
*/
this.emit(Events.CHAT_ARCHIVED, new Chat(this, chat), currState, prevState);
});
await page.evaluate(() => {

@@ -537,2 +610,4 @@ window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });

window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
window.Store.Chat.on('remove', async (chat) => { window.onRemoveChatEvent(await window.WWebJS.getChatModel(chat)); });
window.Store.Chat.on('change:archive', async (chat, currState, prevState) => { window.onArchiveChatEvent(await window.WWebJS.getChatModel(chat), currState, prevState); });
window.Store.Msg.on('add', (msg) => {

@@ -549,2 +624,4 @@ if (msg.isNewMsg) {

window.Store.Contact.on('change:name', (contact, newName, oldName) => {window.onContactNameChange(contact, newName, oldName);});
window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
{

@@ -1230,4 +1307,29 @@ const module = window.Store.createOrUpdateReactionsModule;

}
/**
* Sets the current user's profile picture.
* @param {MessageMedia} media
* @returns {Promise<boolean>} Returns true if the picture was properly updated.
*/
async setProfilePicture(media) {
const success = await this.pupPage.evaluate((chatid, media) => {
return window.WWebJS.setPicture(chatid, media);
}, this.info.wid._serialized, media);
return success;
}
/**
* Deletes the current user's profile picture.
* @returns {Promise<boolean>} Returns true if the picture was properly deleted.
*/
async deleteProfilePicture() {
const success = await this.pupPage.evaluate((chatid) => {
return window.WWebJS.deletePicture(chatid);
}, this.info.wid._serialized);
return success;
}
}
module.exports = Client;

@@ -78,2 +78,8 @@ 'use strict';

/**
* Last message fo chat
* @type {Message}
*/
this.lastMessage = data.lastMessage ? new Message(super.client, data.lastMessage) : undefined;
return super._patch(data);

@@ -80,0 +86,0 @@ }

@@ -217,2 +217,27 @@ 'use strict';

/**
* Deletes the group's picture.
* @returns {Promise<boolean>} Returns true if the picture was properly deleted. This can return false if the user does not have the necessary permissions.
*/
async deletePicture() {
const success = await this.client.pupPage.evaluate((chatid) => {
return window.WWebJS.deletePicture(chatid);
}, this.id._serialized);
return success;
}
/**
* Sets the group's picture.
* @param {MessageMedia} media
* @returns {Promise<boolean>} Returns true if the picture was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setPicture(media) {
const success = await this.client.pupPage.evaluate((chatid, media) => {
return window.WWebJS.setPicture(chatid, media);
}, this.id._serialized, media);
return success;
}
/**
* Gets the invite code for a specific group

@@ -219,0 +244,0 @@ * @returns {Promise<string>} Group's invite code

@@ -8,3 +8,4 @@ 'use strict';

const Payment = require('./Payment');
const { MessageTypes } = require('../util/Constants');
const Reaction = require('./Reaction');
const {MessageTypes} = require('../util/Constants');

@@ -92,4 +93,3 @@ /**

*/
this.deviceType = data.id.id.length > 21 ? 'android' : data.id.id.substring(0, 2) == '3A' ? 'ios' : 'web';
this.deviceType = typeof data.id.id === 'string' && data.id.id.length > 21 ? 'android' : typeof data.id.id === 'string' && data.id.id.substring(0, 2) === '3A' ? 'ios' : 'web';
/**

@@ -113,3 +113,3 @@ * Indicates if the message was forwarded

*/
this.isStatus = data.isStatusV3;
this.isStatus = data.isStatusV3 || data.id.remote === 'status@broadcast';

@@ -141,2 +141,8 @@ /**

/**
* Indicates whether there are reactions to the message
* @type {boolean}
*/
this.hasReaction = data.hasReaction ? true : false;
/**
* Indicates the duration of the message in seconds

@@ -443,11 +449,12 @@ * @type {string}

async delete(everyone) {
await this.client.pupPage.evaluate((msgId, everyone) => {
await this.client.pupPage.evaluate(async (msgId, everyone) => {
let msg = window.Store.Msg.get(msgId);
let chat = await window.Store.Chat.find(msg.id.remote);
const canRevoke = window.Store.MsgActionChecks.canSenderRevokeMsg(msg) || window.Store.MsgActionChecks.canAdminRevokeMsg(msg);
if (everyone && canRevoke) {
return window.Store.Cmd.sendRevokeMsgs(msg.chat, [msg], { type: msg.id.fromMe ? 'Sender' : 'Admin' });
return window.Store.Cmd.sendRevokeMsgs(chat, [msg], { clearMedia: true, type: msg.id.fromMe ? 'Sender' : 'Admin' });
}
return window.Store.Cmd.sendDeleteMsgs(msg.chat, [msg], true);
return window.Store.Cmd.sendDeleteMsgs(chat, [msg], true);
}, this.id._serialized, everyone);

@@ -460,7 +467,8 @@ }

async star() {
await this.client.pupPage.evaluate((msgId) => {
await this.client.pupPage.evaluate(async (msgId) => {
let msg = window.Store.Msg.get(msgId);
if (window.Store.MsgActionChecks.canStarMsg(msg)) {
return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
let chat = await window.Store.Chat.find(msg.id.remote);
return window.Store.Cmd.sendStarMsgs(chat, [msg], false);
}

@@ -474,7 +482,8 @@ }, this.id._serialized);

async unstar() {
await this.client.pupPage.evaluate((msgId) => {
await this.client.pupPage.evaluate(async (msgId) => {
let msg = window.Store.Msg.get(msgId);
if (window.Store.MsgActionChecks.canStarMsg(msg)) {
return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
let chat = await window.Store.Chat.find(msg.id.remote);
return window.Store.Cmd.sendUnstarMsgs(chat, [msg], false);
}

@@ -539,4 +548,42 @@ }, this.id._serialized);

}
/**
* Reaction List
* @typedef {Object} ReactionList
* @property {string} id Original emoji
* @property {string} aggregateEmoji aggregate emoji
* @property {boolean} hasReactionByMe Flag who sent the reaction
* @property {Array<Reaction>} senders Reaction senders, to this message
*/
/**
* Gets the reactions associated with the given message
* @return {Promise<ReactionList[]>}
*/
async getReactions() {
if (!this.hasReaction) {
return undefined;
}
const reactions = await this.client.pupPage.evaluate(async (msgId) => {
const msgReactions = await window.Store.Reactions.find(msgId);
if (!msgReactions || !msgReactions.reactions.length) return null;
return msgReactions.reactions.serialize();
}, this.id._serialized);
if (!reactions) {
return undefined;
}
return reactions.map(reaction => {
reaction.senders = reaction.senders.map(sender => {
sender.timestamp = Math.round(sender.timestamp / 1000);
return new Reaction(this.client, sender);
});
return reaction;
});
}
}
module.exports = Message;

@@ -16,3 +16,4 @@ 'use strict';

ffmpegPath: 'ffmpeg',
bypassCSP: false
bypassCSP: false,
proxyAuthentication: undefined
};

@@ -40,2 +41,4 @@

READY: 'ready',
CHAT_REMOVED: 'chat_removed',
CHAT_ARCHIVED: 'chat_archived',
MESSAGE_RECEIVED: 'message',

@@ -46,6 +49,9 @@ MESSAGE_CREATE: 'message_create',

MESSAGE_ACK: 'message_ack',
UNREAD_COUNT: 'unread_count',
MESSAGE_REACTION: 'message_reaction',
MEDIA_UPLOADED: 'media_uploaded',
CONTACT_CHANGED: 'contact_changed',
GROUP_JOIN: 'group_join',
GROUP_LEAVE: 'group_leave',
GROUP_ADMIN_CHANGED: 'group_admin_changed',
GROUP_UPDATE: 'group_update',

@@ -52,0 +58,0 @@ QR_RECEIVED: 'qr',

@@ -44,3 +44,3 @@ 'use strict';

window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
window.Store.GroupParticipants = window.mR.findModule('promoteParticipants')[1];
window.Store.GroupParticipants = window.mR.findModule('promoteParticipants')[0];
window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0];

@@ -58,2 +58,4 @@ window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;

window.Store.SocketWap = window.mR.findModule('wap')[0];
window.Store.SearchContext = window.mR.findModule('getSearchContext')[0].getSearchContext;
window.Store.DrawerManager = window.mR.findModule('DrawerManager')[0].DrawerManager;
window.Store.StickerTools = {

@@ -67,3 +69,4 @@ ...window.mR.findModule('toWebpSticker')[0],

...window.mR.findModule('setGroupDescription')[0],
...window.mR.findModule('sendExitGroup')[0]
...window.mR.findModule('sendExitGroup')[0],
...window.mR.findModule('sendSetPicture')[0]
};

@@ -248,7 +251,8 @@

const isMD = window.Store.MDBackend;
const newId = await window.Store.MsgKey.newId();
const newMsgId = new window.Store.MsgKey({
from: meUser,
to: chat.id,
id: window.Store.MsgKey.newId(),
id: newId,
participant: isMD && chat.id.isGroup() ? meUser : undefined,

@@ -278,2 +282,3 @@ selfDir: 'out',

...attOptions,
...(attOptions.toJSON ? attOptions.toJSON() : {}),
...quotedMsgOptions,

@@ -434,3 +439,11 @@ ...vcardOptions,

}
res.lastMessage = null;
if (res.msgs && res.msgs.length) {
const lastMessage = window.Store.Msg.get(chat.lastReceivedKey._serialized);
if (lastMessage) {
res.lastMessage = window.WWebJS.getMessageModel(lastMessage);
}
}
delete res.msgs;

@@ -634,2 +647,71 @@ delete res.msgUnsyncedButtonReplyMsgs;

};
window.WWebJS.cropAndResizeImage = async (media, options = {}) => {
if (!media.mimetype.includes('image'))
throw new Error('Media is not an image');
if (options.mimetype && !options.mimetype.includes('image'))
delete options.mimetype;
options = Object.assign({ size: 640, mimetype: media.mimetype, quality: .75, asDataUrl: false }, options);
const img = await new Promise ((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:${media.mimetype};base64,${media.data}`;
});
const sl = Math.min(img.width, img.height);
const sx = Math.floor((img.width - sl) / 2);
const sy = Math.floor((img.height - sl) / 2);
const canvas = document.createElement('canvas');
canvas.width = options.size;
canvas.height = options.size;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, sx, sy, sl, sl, 0, 0, options.size, options.size);
const dataUrl = canvas.toDataURL(options.mimetype, options.quality);
if (options.asDataUrl)
return dataUrl;
return Object.assign(media, {
mimetype: options.mimeType,
data: dataUrl.replace(`data:${options.mimeType};base64,`, '')
});
};
window.WWebJS.setPicture = async (chatid, media) => {
const thumbnail = await window.WWebJS.cropAndResizeImage(media, { asDataUrl: true, mimetype: 'image/jpeg', size: 96 });
const profilePic = await window.WWebJS.cropAndResizeImage(media, { asDataUrl: true, mimetype: 'image/jpeg', size: 640 });
const chatWid = window.Store.WidFactory.createWid(chatid);
try {
const collection = window.Store.ProfilePicThumb.get(chatid);
if (!collection.canSet()) return;
const res = await window.Store.GroupUtils.sendSetPicture(chatWid, thumbnail, profilePic);
return res ? res.status === 200 : false;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
};
window.WWebJS.deletePicture = async (chatid) => {
const chatWid = window.Store.WidFactory.createWid(chatid);
try {
const collection = window.Store.ProfilePicThumb.get(chatid);
if (!collection.canDelete()) return;
const res = await window.Store.GroupUtils.requestDeletePicture(chatWid);
return res ? res.status === 200 : false;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
};
};

@@ -53,3 +53,5 @@ 'use strict';

let msg = await window.Store.Msg.get(msgId);
await window.Store.Cmd.openChatAt(msg.chat, msg.chat.getSearchContext(msg));
let chat = await window.Store.Chat.find(msg.id.remote);
let searchContext = await window.Store.SearchContext(chat,msg);
await window.Store.Cmd.openChatAt(chat, searchContext);
}, msgId);

@@ -74,3 +76,3 @@ }

await this.pupPage.evaluate(async () => {
await window.Store.Cmd.closeDrawerRight();
await window.Store.DrawerManager.closeDrawerRight();
});

@@ -77,0 +79,0 @@ }

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