Socket
Socket
Sign inDemoInstall

node-ocpi

Package Overview
Dependencies
6
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.9 to 1.0.10

src/models/ChargingProfile.js

55

index.js

@@ -14,2 +14,55 @@ const Location = require('./src/models/Location');

module.exports = { Location, EVSE, Connector, CDR, Connector, Command, Feedback, Meter, Reservation, Tariff, Transaction, User, Credentials };
const {
ChargingProfilePeriod,
ChargingProfile,
ActiveChargingProfile,
ChargingProfileResponse,
ActiveChargingProfileResult,
ChargingProfileResult,
ClearProfileResult,
SetChargingProfile
} = require('./src/models/ChargingProfile');
const { CommandResult, CommandResponse } = require('./src/models/CommandResult');
const { DisplayText, roleEnum } = require('./src/models/DisplayText');
const {
EnergyContract,
LocationReferences,
Token,
AuthorizationInfo
} = require('./src/models/Token');
const OCPIResponse = require('./src/models/OCPIResponse');
module.exports = {
Location,
EVSE,
Connector,
CDR,
Command,
Feedback,
Meter,
Reservation,
Tariff,
User,
Transaction,
Credentials,
ChargingProfilePeriod,
ChargingProfile,
ActiveChargingProfile,
ChargingProfileResponse,
ActiveChargingProfileResult,
ChargingProfileResult,
ClearProfileResult,
SetChargingProfile,
CommandResult,
CommandResponse,
DisplayText,
EnergyContract,
LocationReferences,
Token,
AuthorizationInfo,
OCPIResponse,
};

2

package.json
{
"name": "node-ocpi",
"version": "1.0.9",
"version": "1.0.10",
"description": "An OCPI library for Node.js",

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

@@ -11,13 +11,13 @@ # node-ocpi Library Documentation

`node-ocpi` is a Node.js library designed for the implementation of the Open Charge Point Interface (OCPI) protocol. It provides structured models and validation for various OCPI entities, making it ideal for developers building applications for EV charging stations and related services.
`node-ocpi` is an advanced Node.js library designed for implementing the Open Charge Point Interface (OCPI) protocol. Offering structured models and comprehensive validation for various OCPI entities, it serves as an essential toolkit for developers in the electric vehicle (EV) charging station domain.
## Features
- Models for key OCPI entities: `Location`, `EVSE`, `Connector`, `CDR`, `Command`, `Transaction`, `Feedback`, `Meter`, `Reservation`, `Tariff`, `User`, `Credentials`.
- Comprehensive validation for OCPI-compliant data structures.
- Supports a wide range of OCPI operations and functionalities.
- Extensive models for key OCPI entities including `Location`, `EVSE`, `Connector`, `CDR`, `Command`, `Feedback`, `Meter`, `Reservation`, `Tariff`, `User`, `Transaction`, `Credentials`, `ChargingProfilePeriod`, `ChargingProfile`, `ActiveChargingProfile`, `ChargingProfileResponse`, `ActiveChargingProfileResult`, `ChargingProfileResult`, `ClearProfileResult`, `SetChargingProfile`, `CommandResult`, `CommandResponse`, `DisplayText`, `EnergyContract`, `LocationReferences`, `Token`, `AuthorizationInfo`, and `OCPIResponse`.
- Robust validation for OCPI-compliant data structures to ensure data integrity and accuracy.
- Compatible with a wide range of OCPI operations and functionalities, enhancing EV charging station services development.
## Installation
You can install `node-ocpi` using npm with the following command:
Install `node-ocpi` using npm:

@@ -28,10 +28,13 @@ ```bash

This command fetches and installs the library directly from the specified GitHub repository.
## Usage
Import the models you need from the `node-ocpi` library as follows:
Import the required models from `node-ocpi`:
```javascript
const { Location, EVSE, Connector, CDR, Command, Transaction, Feedback, Meter, Reservation, Tariff, User, Credentials } = require('node-ocpi');
const {
Location, EVSE, Connector, CDR, Command, Feedback, Meter, Reservation, Tariff, User, Transaction, Credentials,
ChargingProfilePeriod, ChargingProfile, ActiveChargingProfile, ChargingProfileResponse, ActiveChargingProfileResult, ChargingProfileResult, ClearProfileResult, SetChargingProfile,
CommandResult, CommandResponse,
DisplayText, EnergyContract, LocationReferences, Token, AuthorizationInfo, OCPIResponse
} = require('node-ocpi');
```

@@ -41,80 +44,51 @@

Below are examples showing how to create instances of each model and perform data validation using objects as input:
#### Location Example
#### Location
```javascript
const locationData = {
id: 'loc1',
type: 'ON_STREET',
name: 'Main Street Charging Station',
address: '123 Main St',
city: 'Anytown',
postalCode: '12345',
country: 'USA',
coordinates: { latitude: 52.520008, longitude: 13.404954 },
// Other properties as needed
};
const location = new Location(locationData);
const location = new Location({ /* Location data */ });
location.validate();
```
#### EVSE
#### CommandResult Example
```javascript
const evseData = {
uid: 'evse1',
locationId: 'loc1',
// Other properties as needed
};
const evse = new EVSE(evseData);
evse.validate();
const commandResult = new CommandResult({
result: 'ACCEPTED',
message: 'Command successfully executed'
});
commandResult.validate();
```
#### Connector
#### CommandResponse Example
```javascript
const connectorData = {
id: '1',
standard: 'IEC_62196_T2',
// Other properties as needed
};
const connector = new Connector(connectorData);
connector.validate();
const commandResponse = new CommandResponse({
result: 'REJECTED',
timeout: 30,
message: 'Command could not be executed'
});
commandResponse.validate();
```
#### CDR
#### OCPIResponse Example
```javascript
const cdrData = {
id: 'cdr1',
startDateTime: '2023-01-01T00:00:00Z',
// Other properties as needed
};
const cdr = new CDR(cdrData);
cdr.validate();
const ocpiResponse = new OCPIResponse({
statusCode: 2000,
statusMessage: 'Success',
timestamp: new Date().toISOString(),
data: { /* Your data object or array */ }
});
OCPIResponse.schema(YOUR_MODEL_SCHEMA).validate(ocpiResponse);
```
#### Credentials
... (Continue with other models similarly)
```javascript
const credentialsData = { /* Your credentials data */ };
const credentials = new Credentials(credentialsData);
credentials.validate();
```
## Integration with Express.js
Integrate `node-ocpi` in Express.js applications for efficient handling of OCPI data:
... (Continue with other models in a similar fashion)
## Using `node-ocpi` as Middleware in Express.js
You can use `node-ocpi` as middleware in Express.js applications to handle OCPI data:
```javascript
const express = require('express');
const { Location, EVSE, Connector, CDR } = require('node-ocpi');
const { Location } = require('node-ocpi');

@@ -124,3 +98,2 @@ const app = express();

// Example: POST route for adding a new charging location
app.post('/locations', (req, res) => {

@@ -130,3 +103,2 @@ try {

location.validate();
// Save location to database or handle as needed
res.status(201).send(location);

@@ -138,19 +110,15 @@ } catch (error) {

// More routes and logic...
app.listen(3000, () => {
console.log('Server is running on port 3000');
console.log('Server running on port 3000');
});
```
In this example, the `node-ocpi` library is used to validate incoming data for a new charging location.
## Contributing
Contributions to `node-ocpi` are always welcome. To contribute:
Contributions are welcome:
1. Fork the repository.
2. Create a new branch for your feature.
3. Implement your feature or bug fix.
4. Write or adapt tests as needed.
2. Create a new feature branch.
3. Develop your feature or fix.
4. Write or adapt tests.
5. Update the documentation.

@@ -162,2 +130,2 @@ 6. Commit and push your changes.

`node-ocpi` is released under the MIT License. See the [LICENSE](LICENSE.md) file for more details.
`node-ocpi` is released under the MIT License. See [LICENSE](LICENSE.md) for details.

@@ -1,4 +0,16 @@

const Location = require('./Location'); // Ensure the Location model is in the correct path
const Joi = require('joi'); // Assuming Joi is used for validation
class Dimension {
// Enums and Constants (based on your Python model)
const authMethodEnum = ['AUTH_REQUEST', 'COMMAND', 'WHITELIST'];
const cdrDimensionTypeEnum = [
"CURRENT", "ENERGY", "ENERGY_EXPORT", "ENERGY_IMPORT", "MAX_CURRENT", "MIN_CURRENT",
"MAX_POWER", "MIN_POWER", "PARKING_TIME", "POWER", "RESERVATION_TIME",
"STATE_OF_CHARGE", "TIME"
];
const connectorTypeEnum = ["Type1", "Type2", "CCS", "CHAdeMO", "Other"]; // Example values
const connectorFormatEnum = ["Socket", "Cable"];
const powerTypeEnum = ["AC_1_PHASE", "AC_3_PHASE", "DC"];
const tokenTypeEnum = ["RFID", "APP_USER", "OTHER"]; // Example values
class CdrDimension {
constructor(type, volume) {

@@ -11,47 +23,105 @@ this.type = String(type);

return Joi.object({
type: Joi.string().required(),
type: Joi.string().valid(...cdrDimensionTypeEnum).required(),
volume: Joi.number().required()
});
});
}
}
class ChargingPeriod {
constructor(startDateTime, dimensions) {
this.startDateTime = new Date(startDateTime);
this.dimensions = dimensions.map(d => new Dimension(d.type, d.volume));
class GeoLocation {
constructor(latitude, longitude) {
this.latitude = Number(latitude);
this.longitude = Number(longitude);
}
static get schema() {
return Joi.object({
startDateTime: Joi.date().required(),
dimensions: Joi.array().items(Dimension.schema).required()
});
return Joi.object({
latitude: Joi.number().required(),
longitude: Joi.number().required()
});
}
}
class Token {
constructor({ uid, type, authId, visualNumber, issuer, valid, whitelist, language, lastUpdated }) {
class CdrLocation {
constructor({id, name, address, city, postalCode, country, coordinates, evseUid, evseId, connectorId, connectorStandard, connectorFormat, connectorPowerType}) {
this.id = String(id);
this.name = String(name);
this.address = String(address);
this.city = String(city);
this.postalCode = String(postalCode);
this.country = String(country);
this.coordinates = new GeoLocation(coordinates.latitude, coordinates.longitude);
this.evseUid = String(evseUid);
this.evseId = String(evseId);
this.connectorId = String(connectorId);
this.connectorStandard = String(connectorStandard);
this.connectorFormat = String(connectorFormat);
this.connectorPowerType = String(connectorPowerType);
}
static get schema() {
return Joi.object({
id: Joi.string().required(),
name: Joi.string().optional(),
address: Joi.string().required(),
city: Joi.string().required(),
postalCode: Joi.string().required(),
country: Joi.string().required(),
coordinates: GeoLocation.schema.required(),
evseUid: Joi.string().required(),
evseId: Joi.string().required(),
connectorId: Joi.string().optional(),
connectorStandard: Joi.string().required(), //Joi.string().valid(...connectorTypeEnum).required(),
connectorFormat: Joi.string().valid(...connectorFormatEnum).required(),
connectorPowerType: Joi.string().valid(...powerTypeEnum).required()
});
}
}
class CdrToken {
constructor({countryCode, partyId, uid, type, contractId}) {
this.countryCode = String(countryCode);
this.partyId = String(partyId);
this.uid = String(uid);
this.type = String(type);
this.authId = String(authId);
this.visualNumber = String(visualNumber);
this.issuer = String(issuer);
this.valid = Boolean(valid);
this.whitelist = String(whitelist);
this.language = String(language);
this.lastUpdated = new Date(lastUpdated);
this.contractId = String(contractId);
}
static get schema() {
return Joi.object({
countryCode: Joi.string().length(2).required(),
partyId: Joi.string().max(3).required(),
uid: Joi.string().max(36).required(),
type: Joi.string().required(),//Joi.string().valid(...tokenTypeEnum).required(),
contractId: Joi.string().max(36).required()
});
}
}
class Price {
constructor(amount, currency) {
this.amount = Number(amount);
this.currency = String(currency);
}
static get schema() {
return Joi.object({
amount: Joi.number().required(),
currency: Joi.string().length(3).required()
});
}
}
class ChargingPeriod {
constructor(startDateTime, dimensions, tariffId) {
this.startDateTime = new Date(startDateTime);
this.dimensions = dimensions.map(d => new CdrDimension(d.type, d.volume));
this.tariffId = String(tariffId);
}
static get schema() {
return Joi.object({
uid: Joi.string().required(),
type: Joi.string().required(),
authId: Joi.string().required(),
visualNumber: Joi.string().required(),
issuer: Joi.string().required(),
valid: Joi.boolean().required(),
whitelist: Joi.string().required(),
language: Joi.string().required(),
lastUpdated: Joi.date().required()
});
startDateTime: Joi.date().required(),
dimensions: Joi.array().items(CdrDimension.schema).required(),
tariffId: Joi.string().optional()
});
}

@@ -61,3 +131,3 @@ }

class CDR {
constructor(id, startDateTime, endDateTime, authId, authMethod, location, evseId, connectorId, meterId, currency, totalCost, chargingPeriods, totalEnergy, totalTime, lastUpdated, stopReason, totalParkingTime, totalReservationCost, remark, signedData, relatedCDRs, locationReference, productData, chargingPreferences, environmentalImpact) {
constructor({id, startDateTime, endDateTime, authId, authMethod, location, evseId, connectorId, meterId, currency, totalCost, chargingPeriods, totalEnergy, totalTime, lastUpdated, stopReason, totalParkingTime, totalReservationCost, remark, signedData, relatedCDRs, locationReference, productData, chargingPreferences, environmentalImpact, cdrToken}) {
this.id = String(id);

@@ -68,3 +138,3 @@ this.startDateTime = new Date(startDateTime);

this.authMethod = String(authMethod);
this.location = location instanceof Location ? location : null;
this.location = new CdrLocation(location);
this.evseId = String(evseId);

@@ -74,3 +144,4 @@ this.connectorId = String(connectorId);

this.currency = String(currency);
this.totalCost = Number(totalCost);
this.totalCost = new Price(totalCost.amount, totalCost.currency);
this.chargingPeriods = chargingPeriods.map(period => new ChargingPeriod(period.startDateTime, period.dimensions, period.tariffId));
this.totalEnergy = Number(totalEnergy);

@@ -89,6 +160,3 @@ this.totalTime = Number(totalTime);

this.environmentalImpact = environmentalImpact || {};
this.cdrToken = new Token(cdrToken.uid, cdrToken.type, cdrToken.authId, cdrToken.visualNumber, cdrToken.issuer, cdrToken.valid, cdrToken.whitelist, cdrToken.language, cdrToken.lastUpdated);
this.chargingPeriods = chargingPeriods.map(period => new ChargingPeriod(period.startDateTime, period.dimensions));
this.cdrToken = new CdrToken(cdrToken);
}

@@ -102,26 +170,24 @@

authId: Joi.string().required(),
authMethod: Joi.string().required(),
location: Joi.object().required(), // You should define a Joi schema for Location
authMethod: Joi.string().valid(...authMethodEnum).required(),
location: CdrLocation.schema.required(),
evseId: Joi.string().required(),
connectorId: Joi.string().required(),
meterId: Joi.string().required(),
currency: Joi.string().required(),
totalCost: Joi.number().required(),
currency: Joi.string().length(3).required(),
totalCost: Price.schema.required(),
chargingPeriods: Joi.array().items(ChargingPeriod.schema).required(),
totalEnergy: Joi.number().required(),
totalTime: Joi.number().required(),
lastUpdated: Joi.string().required(),
stopReason: Joi.string(), // Optional field, add validation logic if needed
totalParkingTime: Joi.number(), // Optional field, add validation logic if needed
totalReservationCost: Joi.number(), // Optional field, add validation logic if needed
remark: Joi.string(), // Optional field, add validation logic if needed
signedData: Joi.string(), // Optional field, add validation logic if needed
relatedCDRs: Joi.array().items(Joi.string()), // You should define a Joi schema for related CDRs
locationReference: Joi.object(), // You should define a Joi schema for LocationReference
productData: Joi.object(), // You should define a Joi schema for ProductData
chargingPreferences: Joi.object(), // You should define a Joi schema for ChargingPreferences
environmentalImpact: Joi.object(), // You should define a Joi schema for EnvironmentalImpact
cdrToken: Token.schema.required(),
chargingPeriods: Joi.array().items(ChargingPeriod.schema).required(),
lastUpdated: Joi.string().isoDate().required(),
stopReason: Joi.string().optional(),
totalParkingTime: Joi.number().optional(),
totalReservationCost: Joi.number().optional(),
remark: Joi.string().optional(),
signedData: Joi.string().optional(),
relatedCDRs: Joi.array().items(Joi.string()).optional(),
locationReference: Joi.object().optional(),
productData: Joi.object().optional(),
chargingPreferences: Joi.object().optional(),
environmentalImpact: Joi.object().optional(),
cdrToken: CdrToken.schema.required()
});

@@ -139,3 +205,2 @@

module.exports = CDR
module.exports = CDR;
const Joi = require('joi');
class Logo {
constructor(url, category, type, width, height) {
constructor({ url, category, type, width, height }) {
this.url = url;

@@ -10,4 +10,5 @@ this.category = category;

this.height = height;
}
}
validate() {

@@ -34,8 +35,9 @@ const schema = Joi.object({

class BusinessDetails {
constructor(name, website, logo) {
constructor({ name, website, logo }) {
this.name = name;
this.website = website;
this.logo = logo instanceof Logo ? logo : null;
}
this.logo = logo ? new Logo(logo) : null;
}
validate() {

@@ -59,12 +61,37 @@ const schema = Joi.object({

class Credentials {
constructor({ token, url, businessDetails, partyId, countryCode, role }) {
this.token = token;
this.url = url;
this.business_details = businessDetails instanceof BusinessDetails ? businessDetails : null;
class CredentialsRole {
constructor({ role, businessDetails, partyId, countryCode }) {
this.role = role;
this.business_details = businessDetails ? new BusinessDetails(businessDetails) : null;
this.party_id = partyId;
this.country_code = countryCode;
this.role = role;
}
validate() {
const schema = Joi.object({
role: Joi.string().valid('CPO', 'EMSP', 'HUB', 'OCPP', 'OTHER').required(), // Adjust based on Python's `role` enum
business_details: Joi.object().type(BusinessDetails).required(),
party_id: Joi.string().max(3).required(),
country_code: Joi.string().length(2).required()
});
return this._validateWithSchema(schema);
}
_validateWithSchema(schema) {
const { error } = schema.validate(this);
if (error) {
throw new Error(error.details.map(d => d.message).join(', '));
}
return true;
}
}
class Credentials {
constructor({ token, url, roles }) {
this.token = token;
this.url = url;
this.roles = roles.map(role => new CredentialsRole(role));
}
validate() {

@@ -74,6 +101,3 @@ const schema = Joi.object({

url: Joi.string().uri().required(),
business_details: Joi.object().type(BusinessDetails).required(),
party_id: Joi.string().required(),
country_code: Joi.string().required(),
role: Joi.string().valid('CPO', 'EMSP', 'HUB', 'OCPP', 'OTHER').required()
roles: Joi.array().items(Joi.object().type(CredentialsRole)).required()
});

@@ -92,2 +116,2 @@ return this._validateWithSchema(schema);

module.exports = Credentials
module.exports = Credentials;

@@ -68,2 +68,18 @@ const Joi = require('joi');

// GeoLocation Class
class GeoLocation {
constructor(latitude, longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
static get schema() {
return Joi.object({
latitude: Joi.string().regex(/-?[0-9]{1,3}\.[0-9]{5,7}/).required(),
longitude: Joi.string().regex(/-?[0-9]{1,3}\.[0-9]{5,7}/).required(),
});
}
}
// Facility Submodel

@@ -70,0 +86,0 @@ class Facility {

const Joi = require('joi');
// Enums:
const tariffDimensionTypeEnum = ["ENERGY", "FLAT", "PARKING_TIME", "TIME"];
const tariffTypeEnum = ["AD_HOC_PAYMENT", "PROFILE_CHEAP", "PROFILE_FAST", "PROFILE_GREEN", "REGULAR"];
const dayOfWeekEnum = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"];
const reservationRestrictionTypeEnum = ["RESERVATION", "RESERVATION_EXPIRES"];
class Price {
constructor(exclVat, inclVat) {
this.exclVat = Number(exclVat);
this.inclVat = inclVat ? Number(inclVat) : null;
}
static get schema() {
return Joi.object({
exclVat: Joi.number().required(),
inclVat: Joi.number().optional()
});
}
}
class PriceComponent {
constructor(type, price, stepSize) {
this.type = String(type);
this.price = Number(price);
this.stepSize = Number(stepSize);
constructor(type, price, vat, stepSize, name) {
this.type = type;
this.price = price;
this.vat = vat;
this.stepSize = stepSize;
this.name = name;
}
static get schema() {
return Joi.object({
type: Joi.string().required(),
return Joi.object({
type: Joi.string().valid(...tariffDimensionTypeEnum).required(),
price: Joi.number().required(),
stepSize: Joi.number().required()
});
vat: Joi.number().optional(),
stepSize: Joi.number().required(),
name: Joi.string().optional()
});
}
}
class TariffRestrictions {
constructor(startTime, endTime, startDate, endDate, minKwh, maxKwh, minCurrent, maxCurrent, minPower, maxPower, minDuration, maxDuration, dayOfWeek, reservation) {
this.startTime = startTime;
this.endTime = endTime;
this.startDate = startDate;
this.endDate = endDate;
this.minKwh = minKwh;
this.maxKwh = maxKwh;
this.minCurrent = minCurrent;
this.maxCurrent = maxCurrent;
this.minPower = minPower;
this.maxPower = maxPower;
this.minDuration = minDuration;
this.maxDuration = maxDuration;
this.dayOfWeek = dayOfWeek;
this.reservation = reservation;
}
static get schema() {
return Joi.object({
startTime: Joi.string().length(5).optional(),
endTime: Joi.string().length(5).optional(),
startDate: Joi.string().length(10).optional(),
endDate: Joi.string().length(10).optional(),
minKwh: Joi.number().optional(),
maxKwh: Joi.number().optional(),
minCurrent: Joi.number().optional(),
maxCurrent: Joi.number().optional(),
minPower: Joi.number().optional(),
maxPower: Joi.number().optional(),
minDuration: Joi.number().optional(),
maxDuration: Joi.number().optional(),
dayOfWeek: Joi.array().items(Joi.string().valid(...dayOfWeekEnum)).optional(),
reservation: Joi.string().valid(...reservationRestrictionTypeEnum).optional()
});
}
}
class TariffElement {
constructor(priceComponents) {
this.priceComponents = priceComponents.map(pc => new PriceComponent(pc.type, pc.price, pc.stepSize));
constructor(priceComponents, restrictions) {
this.priceComponents = priceComponents.map(pc => new PriceComponent(pc.type, pc.price, pc.vat, pc.stepSize, pc.name));
this.restrictions = new TariffRestrictions(restrictions.startTime, restrictions.endTime, restrictions.startDate, restrictions.endDate, restrictions.minKwh, restrictions.maxKwh, restrictions.minCurrent, restrictions.maxCurrent, restrictions.minPower, restrictions.maxPower, restrictions.minDuration, restrictions.maxDuration, restrictions.dayOfWeek, restrictions.reservation);
}
static get schema() {
return Joi.object({
priceComponents: Joi.array().items(PriceComponent.schema).required()
});
return Joi.object({
priceComponents: Joi.array().items(PriceComponent.schema).required(),
restrictions: TariffRestrictions.schema.optional()
});
}

@@ -32,14 +96,16 @@ }

class Tariff {
constructor({ id, currency, elements }) {
constructor(id, currency, type, elements) {
this.id = String(id);
this.currency = String(currency); // Currency code, e.g., 'EUR'
this.elements = elements.map(el => new TariffElement(el.priceComponents));
this.currency = String(currency);
this.type = type;
this.elements = elements.map(el => new TariffElement(el.priceComponents, el.restrictions));
}
validate() {
const schema = Joi.object({
const schema = Joi.object({
id: Joi.string().required(),
currency: Joi.string().required(),
type: Joi.string().valid(...tariffTypeEnum).optional(),
elements: Joi.array().items(TariffElement.schema).required()
});
});

@@ -46,0 +112,0 @@ const { error } = schema.validate(this);

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc