
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
@funcmaticjs/funcmatic
Advanced tools
Middleware framework for AWS Lambda using ES2017 async functions (inspired by Koa.js)
Funcmatic helps you develop more complex serverless functions that respond to web requests. What Express is for building Node.js web servers, Funcmatic is for building Node.js web functions with AWS API Gateway and Lambda.
Funcmatic is able to be so lightweight because it:
Because Funcmatic only focuses on helping you organize the internal logic of your function, it works great with other serverless frameworks that help with packaging, configuration, and deployment (e.g. Serverless Framework, AWS SAM CLI).
Funcmatic requires node v8.10 or higher.
$ npm install @funcmaticjs/funcmatic
const func = require('@funcmaticjs/funcmatic')
func.request(async ctx => {
// Response formatted according to API Gateway's Lambda Proxy Integration
ctx.response = {
statusCode: 200,
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify({ hello: "world" })
}
})
module.exports = {
lambdaHandler: func.handler() // async (event, context) => { ... }
}
Checkout some of the commented examples below to get a feel for what Funcmatic functions look like:
AWS Lambda gives a single entrypoint to execute all of our function's logic.
// Standard AWS Lambda Handler
module.exports.lambdaHandler = async function(event, context) {
// ... all your function code
return "some success message"
// or
// throw new Error("some error type");
}
When creating more complex functions we often want particular logic to be executed only in particular cirumstances. For example, we might want to fetch environment variables or create a database connection only when a function is cold started.
Funcmatic gives you multiple entrypoints so that you can predictably trigger specific logic for different stages of your function's lifecycle.
The purpose of the env handler is to fetch all necessary configuration values and set them in the ctx.env object. The ctx.env object will persist in memory across subsequent invocations of this function.
The env handler is the first of the four handlers to be invoked. It is only invoked during a cold start.
Logic that might be executed by your env handler:
Sample code:
// The async function you pass to "func.env" will be
// called during the "env" lifecycle stage.
func.env(async (ctx) => {
// A typical thing to do in our env handler is to
// fetch environment variables.
let vars = await fetchEnvVarsFromSomewhere()
// It's Funcmatic convention to set these
// values in the ctx.env object which persists
// over invocations of this function.
// We can access ctx.env.DB_CONNECTION_URI on
// subsequent invocations of this function.
ctx.env.DB_CONNECTION_URI = vars.DB_CONNECTION_URI
// Once all the config values our function
// needs is set in ctx.env we can return from
// this handler.
return
})
The purpose of the start handler is to perform all the necessary initialization of your function before core business logic can be executed. Oftentimes, this initialization is expensive and we want it to run only once when the function is cold started. A common use case of the start handler is connecting to a database.
The start handler is executed immediately after the env handler. As such, it has access to all the configuration values stored in ctx.env by the env handler. Like the env handler it only is invoked during a cold start.
Logic that might be executed by your start handler:
Sample code:
// The async function you pass to "func.start" will be
// called during the "start" lifecycle stage.
func.start(async (ctx) => {
// Our env handler set DB_CONNECTION_URI in ctx.env
// so we just read it out here.
let uri = ctx.env.DB_CONNECTION_URI
// We use this value to create a database connection
// which takes a lot of time.
// Since we want this db connection to stay
// open across multiple invocations ("cached")
// we set it in "ctx.env"
ctx.env.db = await connectToSomeDB(uri)
// This is all the initialization our function needs
// so we return
return
})
The purpose of the request handler is to to perform the majority of our function's business logic and return the response to the client. Ideally, all configuration and initialization were completed by our env* and start handlers and our request handler can just focus on the logic that makes this function unique and valuable.
The request handler will often make use of the ctx.event and ctx.context objects which store the unadulterated namesake objects provided by AWS Lambda.
Unlike the env and start handlers which are only executed on cold start, the request handler is executed every time the function is invoked. During a cold start it will run immediately after the env and start handlers. During a warm start it will be the first handler to be executed.
Logic that might be executed by your request handler:
Sample code:
// The async function you pass to "func.request"
// will be called during the "request" lifecycle stage.
func.request(async (ctx) => {
// We fetch the HTTP query param "name" from the
// AWS API Gateway Lambda Proxy Integration event
let name = ctx.event.queryStringParams.name || '*'
// We can use the open db connection that was
// "cached" in ctx.env by our start handler to
// execute a query based on the "name" param.
let db = ctx.env.db
let data = await db.query({ name })
// We return the data to client by setting
// "ctx.response" to be an object with structure
// expected by API Gateway's Lambda Proxy
// Integration.
ctx.response = {
statusCode: 200,
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(data),
isBase64Encoded: false
}
// Note that we don't actually return the response
// in the request handler. "ctx.response" is what
// the client will receive.
return
})
The purpose of the error handler is to deal with uncaught errors that interrupted the execution of our env, start, or request handler. In other words, it is the error handler of last resort in our function. ctx.error is where you can access the uncaught error object.
Our error handler will not be executed if there are no uncaught errors.
Logic that might be executed by your error handler:
Sample code:
// The async function you pass to "func.error"
// will be called during the "error" lifecycle stage.
func.error(async (ctx) => {
// The uncaught error is available in ctx.error
let error = ctx.error
// Let's log the error the console so it
// shows up in Cloudwatch Logs.
// "ctx.logger" is the Funcmatic default logger
ctx.logger.error(error)
// We return an response to the user with our error message
let message = `Sorry there was an error! (${ctx.error.message})`
ctx.respose = {
statusCode: 500, // Internal server error
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify({ message }),
isBase64Encoded: false
}
return
})
The teardown handler is a pseudo handler because it is never called when our function is invoked by AWS Lambda.
So why do we have a teardown handler?
It helps when unit testing our function to clean up any resources that our function might be hanging on to after invocation. For example, if we are caching a database connection, our unit test needs close the connection before ending the test and moving on to another.
Logic that might be executed by your teardown handler:
Sample code:
// The async function you pass to "func.teardown"
// will be called when you manually invoke
// the teardown lifecycle handler
// i.e. calling "func.invokeTeardown()"
func.teardown(async (ctx) => {
// If we have a db connection, close it
if (ctx.env.db) {
await ctx.env.db.close()
}
// Reset all configuration
ctx.env = { }
// We have nothing more to cleanup so we return
return
})
One of the primary benefits of using Funcmatic is being able to package common logic into middleware and reuse it our functions.
Funcmatic's middleware design is based on Koa.js. Since each lifecycle handler has its own entrypoint of execution (i.e. env, start, request, error), this means that each lifecycle handler can be configured with its own middleware stack.
A middleware stack is series of nested Javascript async functions.
The first (i.e. topmost) function is invoked by the Funcmatic framework directly. It is the first middleware function's responsibility to pass control to the second middleware function by calling await next(). next is a special callback async function created by Funcmatic and available all middleware functions.
The second function invokes the third, third invokes the fourth, and so on until we reach the last function in the stack which is typically where your function-specific logic lives.
Since this last function is at the bottom of the stack, it does not need to call next() since there is no next function to pass control off to. It can simply return to end its own execution.
Now execution flows back up the stack in the reverse direction. The N-1 function was waiting on the last function to complete execution await next().
The N-1 function can now execute its own logic and then end its own execution and pass control to the N-2 function by calling return and so on until the second function returns and passes control back to the first function which was waiting for it via await next().
Middleware functions are just async javascript functions that take two arguments: ctx and next.
ctx: The context object which many middleware functions with read data from and also write data to. Since middleware functions don't directly pass arguments or return data directly to each other, ctx is the only way for as a side effect. See ctx documentation below for more details.next: an async function created and passed in by Funcmatic. All middleware functions must call next once and only once so that execution can continue to the next middleware function in the stack.A middleware function has the following structure:
async (ctx, next) => {
// All "downstream" middleware logic higher in the stack will have been executed
// before this middleware
/* This middleware's "downstream" logic ... */
await next() // invoke the next middleware function in the stack
/* This middleware's "upstream" logic ... */
return // any return value will be ignored
// The next "upsteram" middleware logic higher in the stack will be invoked
// after this middleware finishes execution
}
Since the last function (often our function's specific logic) does not have to invoke await next() it can have the structure below:
async (ctx) => { // Note we don't need to accept the "next" parameter
// All "downstream" middleware logic has executed by this time
/* This end-user function's logic ... */
return // any return value will be ignored
// All "upstream" middleware logic will now begin execution
}
There are two useful terms to describe what happens in a specific middleware function:
await next() and pass control to the lower function in the stack.awaits next() and control returns as a result of the lower function completing execution via return.Downstream and Upstream are relative terms. In Funcmatic we think of the user initiating a request to our API being the topmost and our function-specific logic being bottommost. Therefore execution first makes its way downstream from the user, through our middleware stack, to our function specific logic. And then back upstream through our middleware stack and ultimately return to the user.
Here are some simple examples of middleware functions.
queryStringParameters normalizerOne annoying thing about AWS's event object is that if the user's HTTP request has no query parameters the event.queryStringParameters object will be null instead of {}. This means that everywhere in our code we have to check if event.queryStringParameters is first null before we check if our.
Instead of putting these checks everywhere, let's write a middleware function that will do it once and set
// 1. define our middleware function
const queryStringParametersNormalizer = async (ctx, next) => {
// Because we want all "downstream" logic to not
// have to check "event.queryStringParameters == null"
// we put this logic BEFORE "await next()"
let event = ctx.event
if (!event.queryStringParameters) {
event.queryStringParameters = { }
}
await next()
return
}
// 2. add it to the request middleware stack
// when the "request" lifecycle handler is invoked
// by Funcmatic
func.request(queryStringParametersNormalizer)
It's always CORS! To be honest, I still do not understand how to properly configure API Gateway's built in CORS support. Rather than leave it up to AWS, let's not leave things to chance and just set the CORS header Access-Control-Allow-Origin to * ourselves. This will allow our API can be called from any website domain.
// We can define and add our middleware function
// at the same time.
func.request(async (ctx, next) => {
await next()
// Note that this logic happens "upstream" when control
// is flowing back up the middleware stack to the user.
// We assume that some downstream logic has put the
// response to be returned to the user in the
// context object i.e. "ctx.response".
// This middleware is just adding
// and additional header key-value to it.
let response = ctx.response
if (!response.headers["Access-Control-Allow-Origin"]) {
response.headers["Access-Control-Allow-Origin"] = "*"
}
return
})
Given the distributed nature of serverless, logging and monitoring are common ways to apply middleware.
If the middleware function below is the topmost function of the request middleware stack it will log the elapsed execution time of the entire request middleware stack.
// Ideally, this middleware function will the first
// function added to the 'request' middleware stack
// so that we account for all of the nested
// middleware functions in the stack.
func.request(async (ctx, next) => {
// We have to capture the initial request time
// in the "downstream" logic otherwise we won't
// account for the execution that happens downstream.
let t = Date.now()
let id = ctx.context.awsRequestId
await next()
// This logic happens "upstream" because elapsed
// time needs to account for all downstream AND upstream logic
//
// We use Funcmatic's built in JSON-formatted logger
// "ctx.logger" to log the
// * id: AWS Lambda request id
// * t: The time of the request in ms since epoch
// * elapsed: How long the request took in ms
ctx.logger.info({ id, t, elapsed: (Date.now() - t) })
return
})
Middleware Plugins are Javascript classes that define one or more lifecycle methods.
class MyMiddlewarePlugin {
// Lifecycle middleware methods must be use
// the exact names below to be recognized
// by Funcmatic
env(ctx, next) {
/* Downstream logic ... */
await next()
/* Upstream logic ... */
return
}
start(ctx, next) { }
request(ctx, next) { }
error(ctx, next) { }
teardown(ctx, next) { }
}
// Adds the individual lifecycle methods
// defined above to the appropriate
// lifecycle middleware stacks
func.use(new MyMiddlewarePlugin())
It is recommended that you create and use middleware as plugins rather than as individual functions.
Why use plugins rather than individual functions?
func.use(...) will add multiple methods to their correct lifecycle middleware stack.The response-plugin creates a response object and sets it in the ctx.response. This response object makes it format HTTP responses according to AWS's Lambda Proxy Integration format.
$> npm install -save '@funcmaticjs/response-plugin'
There are three steps you must take in your code:
ResponsePlugin class via requireResponsePluginfunc.use with the instanceHere is the code below:
let func = require('@funcmaticjs/func')
let ResponsePlugin = require('@funcmaticjs/response-plugin')
func.use(new ResponsePlugin())
/* ... */
There are already some handy middleware plugins that have been created and ready to use in your functions:
process.env variables to ctx.envctx.envctx.envctx)The context object (ctx) is the shared state between AWS Lambda, the Funcmatic framework, middleware, and your function's unique code. It is the interface in which information is passed between each of these layers.
ctx.eventInitialized to be the event created by AWS Lambda when your function is invoked. Funcmatic is primarily designed to be build HTTP APIs, this will most likely be an event in API Gateway Lambda Proxy Integration event format.
Here is an example of what the ctx.event object could look like.
Note that some middleware, such as EventPlugin, may alter the original AWS event.
Some notable properties of the ctx.event object are:
ctx.contextInitialized to be the AWS Lambda Context object created by AWS Lambda when your function is invoked.
Unlike the AWS event above, the format of this context object remains consistent and independent of what service invoked your function (e.g. API Gateway, S3).
Some notable properties of the ctx.context object are:
awsRequestId
functionName
functionVersion
invokedFunctionArn
callbackWaitsForEmptyEventLoop
false.
request handler completes, AWS Lambda will immediately return the value in ctx.response even if the Node.js event loop is not empty.true. Which means that AWS Lambda will wait for the Node.js event loop to be empty before returning the response.
ctx.context.callbackWaitsForEmptyEventLoop = truectx.responseThis is what needs to be set by your function before it completes execution. This is the value that will be returned to AWS Lambda and ultimately back to the requesting client.
Assumming that you are using API Gateway and it's Lambda Proxy Integration then the response must be an object with the following structure:
{
"statusCode": httpStatusCode, /* e.g. 200 */
"headers": {
"headerName": "headerValue",
...
},
"multiValueHeaders": {
"headerName": ["headerValue", "headerValue2" ]
},
"body": "...",
"isBase64Encoded": true|false
}
If your function is returning a JSON object:
{
"statusCode": 200, // Internal Server Error
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": "{\"hello\":\"world\"}",
"isBase64Encoded": false
}
If your function is returning binary data (e.g. an jpeg image):
{
"statusCode": 200, // Internal Server Error
"headers": {
"Content-Type": "image/jpeg"
},
"body": "/9j/2wBDAAMCAgICAgMCAgIDAw...", // Base64 encoded image data
"isBase64Encoded": true
}
If you are returning an HTTP error:
{
"statusCode": 500, // Internal Server Error
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": "{\"errorMessage\":\"My error message\"}",
"isBase64Encoded": false
}
The ResponsePlugin is intended to help abstract the specifics of API Lambda Proxy format. We can produce the equivalent response in the examples above by:
async (ctx) => {
ctx.response.json({ hello: 'world'})
ctx.response.blob("image.jpeg")
ctx.response.httperror(500, 'My error message')
}
ctx.envThis is intended to contain all the configuration values that your function needs. When your function is first cold started, Funcmatic will initialize ctx.env to an empty object {}. But unlike event, context, and state, the values you choose to store in ctx.env will be preserved across invocations.
The single responsibility of your function's env handler is to fetch configuration values and populate them in ctx.env. The benefits of this is that it isolates the complexity of where your config is stored (e.g. process.env, AWS Parameter Store, API Gateway Stage Variables), to a single handler (env) and beyond that point the rest of your function's logic only needs to interact with the ctx.env object.
ctx.stateThis is initialized to an empty object {} upon every invocation of your function whether it is a cold or warm start. It borrows its purpose from Express as the recommended place to pass data between middleware and your function's logic. The Funcmatic framework does not do anything directly with this object except initialize it to {}.
For example, if you use the MongoDBPlugin, it will create a connection to a MongoDB server and sets the connection in ctx.state.mongodb.
ctx.state.mongodb = await connectToMongoDB()
Then your own function's code can access the connection:
async (ctx) => {
let userid = ctx.event.queryStringParameters['userid']
let db = ctx.state.mongodb
let user = await db.findOne({ userid })
/* ... */
}
ctx.coldstartThis is a literal boolean value (true or false).
true: This current invocation of your function is a cold start meaning that the event and start handlers will be invoked as part of this invocation.false: This current invocation is NOT a cold start (i.e. a warm start) and therefore the event and start handlers will not be invoked as part of this invocation.ctx.loggerFuncmatic provides a default JSON logger ctx.logger. See Logging using ctx.logger section for more detailed info.
ctx.funcA reference of this currently executing Funcmatic function. Most middleware and your function will not need to reference this.
Funcmatic helps you use structured JSON logging in your function. Yan Cui has a great blog post explaining why it's better use structured logs (i.e. JSON) rather than lines of text with console.log(You need to use structured logging with AWS Lambda).
By default, Funcmatic is configured to set ctx.logger to its own very simple JSON logger (i.e. instance of ConsoleLogger). It is recommended to use ctx.logger rather than console.log in your function and any custom middleware. Of course, console.log will still work.
The default logger supports all the standard Log4J Log Levels: trace (10), debug (20), info (30), warn (40), error (50), fatal (60), off (70).
There are three ways you can log messages using this
If you were to log the following:
ctx.logger.info("hello")
It would log the following JSON to console:
{
msg: "hello",
time: 1560116027570, // Epoch ms
level: 30, // Level as a number
level_name: "info" // Level as a string
}
Above you can see that the log fields time, level, and level_name are automatically added by Funcmatic. There are other automatically added fields which you can see in the section below.
If you were to log the following:
ctx.logger.debug({ hello: "world", req: { /* JSON object */ } })
It would log the following JSON console:
{
hello: "world",
req: { /* JSON object */ },
time: 1560116027570,
level: 20,
level_name: "debug"
}
Note that the default logger uses JSON.stringify so you can log nested objects as long as they are serializable into JSON.
There is a special case of logging a Javascript Error object:
try {
/* Some logic that throws */
} catch (err) {
ctx.logger.error(err)
}
This would log the following JSON:
{
msg: "The error message" // err.message
err: "The stack trace" // err.stack,
time: 1560116027570,
level: 50,
level_name: "error"
}
Which follows the same convention that Bunyan uses to log Error objects.
It is also possible to log a JSON object followed by a simple string:
ctx.logger.trace({ hello: "world" }, "hello world")
This would log the following JSON:
{
msg: "hello world",
hello: "world",
time: 1560116027570,
level: 10,
level_name: "trace"
}
Note that you could accomplish the same log line by logging a single object:
ctx.logger.trace({ msg: "hello world", hello: "world" })
In the examples above we saw that the fields time, level, and level_name where automatically included by Funcmatic. Below are all the fields automatically included:
time
Date.now()level
level_name
src
ctx.loggerAsyncFunction:[anonymous]: Logged from an anonymous function. e.g. func.request(async (ctx { /* ... */ })AsyncFunction:myHandler: Logged from a function with the name myHandler.MyPlugin:request: Logged from a middleware plugin of class MyPlugin and method named request.funcmatic: Logged from the core Funcmatic framework i.e. func.js.lifecycle
env, start, request, error, teardown, systemsystem if being logged from Funcmatic framework when it is executing logic that lives "in between" handlers.By default, the log level of ctx.logger is set to info. This means that all log levels equal or higher in severity (info, warn, error, fatal) will actually get logged to the console. Log lines with a lower level of severity (trace, debug) will be silienced.
You can set the log level by calling ctx.logger.level:
ctx.logger.level("debug")
Now all log lines at a debug level and higher will be output to the console.
You can get the current level of ctx.logger by calling ctx.logger.level() with no arguments:
ctx.logger.level()
// returns the level name: "info"
You can adjust the default log level that ctx.logger is initialized with in a couple ways.
When an Funcmatic instance is initialized it will first see if the LOG_LEVEL environment variable is set. If so, it will initialize ctx.logger with the log level found from process.env["LOG_LEVEL"].
For example, if you use dotenv, your .env file might look like:
# .env file
LOG_LEVEL=debug
If you don't have control over the environment variables
const { Func } = require("@funcmaticjs/funcmatic")
let func = new Func({ LOG_LEVEL: 'debug' })
You can add properties to the logger which will be included in all following log lines. For example, if a user is authenticated, you might want to set a bound field of user so that all subsequent lines include user: [id] without having to manually log it yourself with every call to ctx.logger.
To add bound fields you can call ctx.logger.state and pass in the fields (and values) you want to always be logged:
ctx.logger.state({ user: 123, email: "user@example.com" })
Now, whenever you log a message for the life of this specific invocation, you will see these fields. For example,
ctx.logger.info("hello world")
Will log the following line:
{
msg: "hello world",
user: 123,
email: "user@example.com"
/* ... Funcmatic default fields */
}
To see the current fields and values that are bound you can call ctx.logger.state with no arguments:
// Previously: ctx.logger.state({ user: 123, email: "user@example.com" })
let bound = ctx.logger.state()
/*
bound = {
user: 123,
email: "user@example.com"
}
*/
If you want clear all the bound fields in the logger, you can call ctx.logger.state with the boolean value false.
ctx.logger.state(false) // clears all bound fields
let bound = ctx.logger.state()
/*
bound = { }
*/
You can also replace the bound fields wholesale by calling ctx.logger.state and passing in options of { replace: true }:
// Previously: ctx.logger.state({ user: 123, email: "user@example.com" })
ctx.logger.state({ completely: "new" }, { replace: true })
let bound = ctx.logger.state()
/*
bound = {
completely: "new"
}
Calling ctx.logger.state({ completely: "new" }) would
have just done an update:
bound = {
completely: "new",
user: 123,
email: "user@example.com"
}
*/
Note that some middleware (e.g. CorrelationPlugin) will add bound fields for you.
The one downside aboutstructured logging is that it is more difficult for humans to read it output. When you are actively developing and testing your function, it
We wrote a simple log prettifier that can be used with Funcmatic to turn logs that would like
[ Image of regular JSON output ]
into this ...
[ Image of prettified logs ]
const { Func } = require("@funcmaticjs/funcmatic")
const prettify = require("@funcmaticjs/pretty-logs")
const func = new Func({ prettify })
You can create your own prettify function. It ultimately needs the following interface:
function (obj) { // obj is the JSON log line
/* ... do some prettifying here */
return "someBeautifulString"
}
Check out @funcmaticjs/pretty-logs for an example.
Funcmatic was designed to support unit testing. Examples below will be in jest.
describe('Func', () => {
let func = null
let ctx = { }
beforeEach(async () => {
func = require("../lib/func.js")
ctx = func.initCtx(ctx)
})
afterEach(async () => {
func.teardown()
})
it ("should test a cold start invocation", async () => {
ctx.event.httpMethod = "POST"
ctx.event.headers[""] = "test"
ctx.event.body = JSON.stringify({hello: "world"})
await func.invoke(ctx)
expect(ctx.response).toMatchObject({
statusCode: ,
headers: {
}
})
let body = JSON.parse(ctx.response.body)
expect(body).toMatchObject({
})
})
it ("should test a warm start invocation", async () => {
await func.invoke(ctx) // cold start
await func.invoke(ctx)
})
it ("should test env hander", async () => {
await func.invokeEnv(ctx)
expect(ctx.env).toMatchObject({
})
})
it ("should test the start handler", async () => {
await func.invokeStart(ctx)
expect(ctx.state).toMatchObject({
})
})
it ("should test the request handler", async () => [
])
it ("should test the error handler", async () => {
await func.invokeError(ctx)
})
})
Install jest
$> npm install --save-dev jest
Update package.json
Run
npm test
See code coverage
Use the following to set the LOG_LEVEL and pretty logging
LOG_LEVEL=debug LOG_PRETTY=true
If Funcmatic doesn't quite suit your project (or your tastes) here are some other projects that might be useful:
Funcmatic is licensed under the the MIT License. Copyright © 2019 Funcmatic Inc.
All files located in the node_modules and external directories are externally maintained libraries used by this software which have their own licenses; we recommend you read them, as their terms may differ from the terms in the MIT License.
FAQs
Middleware framework for AWS Lambda using ES2017 async functions (inspired by Koa.js)
We found that @funcmaticjs/funcmatic demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.