@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
575 lines • 25 kB
JavaScript
"use strict";
// Copyright (c) 2023 S44, LLC
// Copyright Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache 2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.systemConfigSchema = exports.websocketServerSchema = exports.systemConfigInputSchema = exports.websocketServerInputSchema = void 0;
const zod_1 = require("zod");
const model_1 = require("../ocpp/model");
const __1 = require("..");
// TODO: Refactor other objects out of system config, such as certificatesModuleInputSchema etc.
exports.websocketServerInputSchema = zod_1.z.object({
// TODO: Add support for tenant ids on server level for tenant-specific behavior
id: zod_1.z.string().optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8080).optional(),
pingInterval: zod_1.z.number().int().positive().default(60).optional(),
protocol: zod_1.z.enum(['ocpp1.6', 'ocpp2.0.1']).default('ocpp2.0.1').optional(),
securityProfile: zod_1.z.number().int().min(0).max(3).default(0).optional(),
allowUnknownChargingStations: zod_1.z.boolean().default(false).optional(),
tlsKeyFilePath: zod_1.z.string().optional(), // Leaf certificate's private key pem which decrypts the message from client
tlsCertificateChainFilePath: zod_1.z.string().optional(), // Certificate chain pem consist of a leaf followed by sub CAs
mtlsCertificateAuthorityKeyFilePath: zod_1.z.string().optional(), // Sub CA's private key which signs the leaf (e.g.,
// charging station certificate and csms certificate)
rootCACertificateFilePath: zod_1.z.string().optional(), // Root CA certificate that overrides default CA certificates
// allowed by Mozilla
});
exports.systemConfigInputSchema = zod_1.z.object({
env: zod_1.z.enum(['development', 'production']),
centralSystem: zod_1.z.object({
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
}),
modules: zod_1.z.object({
certificates: zod_1.z
.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Certificates).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
})
.optional(),
configuration: zod_1.z.object({
heartbeatInterval: zod_1.z.number().int().positive().default(60).optional(),
bootRetryInterval: zod_1.z.number().int().positive().default(10).optional(),
ocpp2_0_1: zod_1.z
.object({
unknownChargerStatus: zod_1.z
.enum([
model_1.OCPP2_0_1.RegistrationStatusEnumType.Accepted,
model_1.OCPP2_0_1.RegistrationStatusEnumType.Pending,
model_1.OCPP2_0_1.RegistrationStatusEnumType.Rejected,
])
.default(model_1.OCPP2_0_1.RegistrationStatusEnumType.Accepted)
.optional(), // Unknown chargers have no entry in BootConfig table
getBaseReportOnPending: zod_1.z.boolean().default(true).optional(),
bootWithRejectedVariables: zod_1.z.boolean().default(true).optional(),
autoAccept: zod_1.z.boolean().default(true).optional(), // If false, only data endpoint can update boot status to accepted
})
.optional(),
ocpp1_6: zod_1.z
.object({
unknownChargerStatus: zod_1.z
.enum([
model_1.OCPP1_6.BootNotificationResponseStatus.Accepted,
model_1.OCPP1_6.BootNotificationResponseStatus.Pending,
model_1.OCPP1_6.BootNotificationResponseStatus.Rejected,
])
.default(model_1.OCPP1_6.BootNotificationResponseStatus.Accepted)
.optional(), // Unknown chargers have no entry in BootConfig table
})
.optional(),
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Configuration).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
}),
evdriver: zod_1.z.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.EVDriver).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
}),
monitoring: zod_1.z.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Monitoring).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
}),
reporting: zod_1.z.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Reporting).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
}),
smartcharging: zod_1.z
.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.SmartCharging).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
})
.optional(),
tenant: zod_1.z
.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Tenant).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
})
.optional(),
transactions: zod_1.z.object({
endpointPrefix: zod_1.z.string().default(__1.EventGroup.Transactions).optional(),
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8081).optional(),
costUpdatedInterval: zod_1.z.number().int().positive().default(60).optional(),
sendCostUpdatedOnMeterValue: zod_1.z.boolean().default(false).optional(),
signedMeterValuesConfiguration: zod_1.z
.object({
publicKeyFileId: zod_1.z.string(),
signingMethod: zod_1.z.enum(['RSASSA-PKCS1-v1_5', 'ECDSA']),
})
.optional(),
}),
}),
data: zod_1.z.object({
sequelize: zod_1.z.object({
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(5432).optional(),
database: zod_1.z.string().default('csms').optional(),
dialect: zod_1.z.any().default('sqlite').optional(),
username: zod_1.z.string().optional(),
password: zod_1.z.string().optional(),
storage: zod_1.z.string().default('csms.sqlite').optional(),
sync: zod_1.z.boolean().default(false).optional(),
alter: zod_1.z.boolean().default(false).optional(),
}),
}),
util: zod_1.z.object({
cache: zod_1.z
.object({
memory: zod_1.z.boolean().optional(),
redis: zod_1.z
.object({
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(6379).optional(),
})
.optional(),
})
.refine((obj) => obj.memory || obj.redis, {
message: 'A cache implementation must be set',
}),
messageBroker: zod_1.z
.object({
kafka: zod_1.z
.object({
topicPrefix: zod_1.z.string().optional(),
topicName: zod_1.z.string().optional(),
brokers: zod_1.z.array(zod_1.z.string()),
sasl: zod_1.z.object({
mechanism: zod_1.z.string(),
username: zod_1.z.string(),
password: zod_1.z.string(),
}),
})
.optional(),
amqp: zod_1.z
.object({
url: zod_1.z.string(),
exchange: zod_1.z.string(),
})
.optional(),
})
.refine((obj) => obj.kafka || obj.amqp, {
message: 'A message broker implementation must be set',
}),
fileAccess: zod_1.z
.object({
s3: zod_1.z
.object({
region: zod_1.z.string().optional(),
endpoint: zod_1.z.string().optional(),
defaultBucketName: zod_1.z.string().default('citrineos-s3-bucket'),
s3ForcePathStyle: zod_1.z.boolean().default(true),
accessKeyId: zod_1.z.string().optional(),
secretAccessKey: zod_1.z.string().optional(),
})
.optional(),
local: zod_1.z
.object({
defaultFilePath: zod_1.z.string().default('/data'),
})
.optional(),
directus: zod_1.z
.object({
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8055).optional(),
token: zod_1.z.string().optional(),
username: zod_1.z.string().optional(),
password: zod_1.z.string().optional(),
generateFlows: zod_1.z.boolean().default(false).optional(),
})
.refine((obj) => obj.generateFlows && !obj.host, {
message: 'Directus host must be set if generateFlows is true',
})
.optional(),
})
.refine((obj) => obj.s3 || obj.local || obj.directus, {
message: 'A file access implementation must be set',
})
.refine((obj) => {
const implementations = [obj.s3, obj.local, obj.directus];
const presentCount = implementations.filter(Boolean).length;
return presentCount <= 1;
}, {
message: 'Only one file access implementation should be set',
}),
swagger: zod_1.z
.object({
path: zod_1.z.string().default('/docs').optional(),
logoPath: zod_1.z.string(),
exposeData: zod_1.z.boolean().default(true).optional(),
exposeMessage: zod_1.z.boolean().default(true).optional(),
})
.optional(),
networkConnection: zod_1.z.object({
websocketServers: zod_1.z.array(exports.websocketServerInputSchema.optional()),
}),
certificateAuthority: zod_1.z.object({
v2gCA: zod_1.z
.object({
name: zod_1.z.enum(['hubject']).default('hubject'),
hubject: zod_1.z
.object({
baseUrl: zod_1.z.string().default('https://open.plugncharge-test.hubject.com'),
tokenUrl: zod_1.z
.string()
.default('https://hubject.stoplight.io/api/v1/projects/cHJqOjk0NTg5/nodes/6bb8b3bc79c2e-authorization-token'),
isoVersion: zod_1.z.enum(['ISO15118-2', 'ISO15118-20']).default('ISO15118-2'),
})
.optional(),
})
.refine((obj) => {
if (obj.name === 'hubject') {
return obj.hubject;
}
else {
return false;
}
}),
chargingStationCA: zod_1.z
.object({
name: zod_1.z.enum(['acme']).default('acme'),
acme: zod_1.z
.object({
env: zod_1.z.enum(['staging', 'production']).default('staging'),
accountKeyFilePath: zod_1.z.string(),
email: zod_1.z.string(),
})
.optional(),
})
.refine((obj) => {
if (obj.name === 'acme') {
return obj.acme;
}
else {
return false;
}
}),
}),
}),
logLevel: zod_1.z.number().min(0).max(6).default(0).optional(),
maxCallLengthSeconds: zod_1.z.number().int().positive().default(5).optional(),
maxCachingSeconds: zod_1.z.number().int().positive().default(10).optional(),
ocpiServer: zod_1.z.object({
host: zod_1.z.string().default('localhost').optional(),
port: zod_1.z.number().int().positive().default(8085).optional(),
}),
userPreferences: zod_1.z.object({
telemetryConsent: zod_1.z.boolean().default(false).optional(),
}),
configFileName: zod_1.z.string().default('config.json').optional(),
configDir: zod_1.z.string().optional(),
});
exports.websocketServerSchema = zod_1.z
.object({
// TODO: Add support for tenant ids on server level for tenant-specific behavior
id: zod_1.z.string(),
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
pingInterval: zod_1.z.number().int().positive(),
protocol: zod_1.z.enum(['ocpp1.6', 'ocpp2.0.1']),
securityProfile: zod_1.z.number().int().min(0).max(3),
allowUnknownChargingStations: zod_1.z.boolean(),
tlsKeyFilePath: zod_1.z.string().optional(),
tlsCertificateChainFilePath: zod_1.z.string().optional(),
mtlsCertificateAuthorityKeyFilePath: zod_1.z.string().optional(),
rootCACertificateFilePath: zod_1.z.string().optional(),
})
.refine((obj) => {
switch (obj.securityProfile) {
case 0: // No security
case 1: // Basic Auth
return true;
case 2: // Basic Auth + TLS
return obj.tlsKeyFilePath && obj.tlsCertificateChainFilePath;
case 3: // mTLS
return (obj.tlsCertificateChainFilePath &&
obj.tlsKeyFilePath &&
obj.mtlsCertificateAuthorityKeyFilePath);
default:
return false;
}
});
exports.systemConfigSchema = zod_1.z
.object({
env: zod_1.z.enum(['development', 'production']),
centralSystem: zod_1.z.object({
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
}),
modules: zod_1.z.object({
certificates: zod_1.z
.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
})
.optional(),
evdriver: zod_1.z.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
}),
configuration: zod_1.z
.object({
heartbeatInterval: zod_1.z.number().int().positive(),
bootRetryInterval: zod_1.z.number().int().positive(),
ocpp2_0_1: zod_1.z
.object({
unknownChargerStatus: zod_1.z.enum([
model_1.OCPP2_0_1.RegistrationStatusEnumType.Accepted,
model_1.OCPP2_0_1.RegistrationStatusEnumType.Pending,
model_1.OCPP2_0_1.RegistrationStatusEnumType.Rejected,
]), // Unknown chargers have no entry in BootConfig table
getBaseReportOnPending: zod_1.z.boolean(),
bootWithRejectedVariables: zod_1.z.boolean(),
/**
* If false, only data endpoint can update boot status to accepted
*/
autoAccept: zod_1.z.boolean(),
})
.optional(),
ocpp1_6: zod_1.z
.object({
unknownChargerStatus: zod_1.z.enum([
model_1.OCPP1_6.BootNotificationResponseStatus.Accepted,
model_1.OCPP1_6.BootNotificationResponseStatus.Pending,
model_1.OCPP1_6.BootNotificationResponseStatus.Rejected,
]), // Unknown chargers have no entry in BootConfig table
})
.optional(),
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
})
.refine((obj) => obj.ocpp1_6 || obj.ocpp2_0_1, {
message: 'A protocol configuration must be set',
}), // Configuration module is required
monitoring: zod_1.z.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
}),
reporting: zod_1.z.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
}),
smartcharging: zod_1.z
.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
})
.optional(),
tenant: zod_1.z.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
}),
transactions: zod_1.z
.object({
endpointPrefix: zod_1.z.string(),
host: zod_1.z.string().optional(),
port: zod_1.z.number().int().positive().optional(),
costUpdatedInterval: zod_1.z.number().int().positive().optional(),
sendCostUpdatedOnMeterValue: zod_1.z.boolean().optional(),
signedMeterValuesConfiguration: zod_1.z
.object({
publicKeyFileId: zod_1.z.string(),
signingMethod: zod_1.z.enum(['RSASSA-PKCS1-v1_5', 'ECDSA']),
})
.optional(),
})
.refine((obj) => !(obj.costUpdatedInterval && obj.sendCostUpdatedOnMeterValue) &&
(obj.costUpdatedInterval || obj.sendCostUpdatedOnMeterValue), {
message: 'Can only update cost based on the interval or in response to a transaction event /meter value' +
' update. Not allowed to have both costUpdatedInterval and sendCostUpdatedOnMeterValue configured',
}), // Transactions module is required
}),
data: zod_1.z.object({
sequelize: zod_1.z.object({
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
database: zod_1.z.string(),
dialect: zod_1.z.any(),
username: zod_1.z.string(),
password: zod_1.z.string(),
storage: zod_1.z.string(),
sync: zod_1.z.boolean(),
alter: zod_1.z.boolean().optional(),
maxRetries: zod_1.z.number().int().positive().optional(),
retryDelay: zod_1.z.number().int().positive().optional(),
}),
}),
util: zod_1.z.object({
cache: zod_1.z
.object({
memory: zod_1.z.boolean().optional(),
redis: zod_1.z
.object({
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
})
.optional(),
})
.refine((obj) => obj.memory || obj.redis, {
message: 'A cache implementation must be set',
}),
messageBroker: zod_1.z
.object({
kafka: zod_1.z
.object({
topicPrefix: zod_1.z.string().optional(),
topicName: zod_1.z.string().optional(),
brokers: zod_1.z.array(zod_1.z.string()),
sasl: zod_1.z.object({
mechanism: zod_1.z.string(),
username: zod_1.z.string(),
password: zod_1.z.string(),
}),
})
.optional(),
amqp: zod_1.z
.object({
url: zod_1.z.string(),
exchange: zod_1.z.string(),
})
.optional(),
})
.refine((obj) => obj.kafka || obj.amqp, {
message: 'A message broker implementation must be set',
}),
fileAccess: zod_1.z
.object({
s3: zod_1.z
.object({
region: zod_1.z.string().optional(),
endpoint: zod_1.z.string().optional(),
defaultBucketName: zod_1.z.string().default('citrineos-s3-bucket'),
s3ForcePathStyle: zod_1.z.boolean().default(true),
accessKeyId: zod_1.z.string().optional(),
secretAccessKey: zod_1.z.string().optional(),
})
.optional(),
local: zod_1.z
.object({
defaultFilePath: zod_1.z.string().default('/data'),
})
.optional(),
directus: zod_1.z
.object({
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
token: zod_1.z.string().optional(),
username: zod_1.z.string().optional(),
password: zod_1.z.string().optional(),
generateFlows: zod_1.z.boolean(),
})
.optional(),
})
.refine((obj) => obj.s3 || obj.local || obj.directus, {
message: 'A file access implementation must be set',
})
.refine((obj) => {
const implementations = [obj.s3, obj.local, obj.directus];
const presentCount = implementations.filter(Boolean).length;
return presentCount <= 1;
}, {
message: 'Only one file access implementation should be set',
}),
swagger: zod_1.z
.object({
path: zod_1.z.string(),
logoPath: zod_1.z.string(),
exposeData: zod_1.z.boolean(),
exposeMessage: zod_1.z.boolean(),
})
.optional(),
networkConnection: zod_1.z.object({
websocketServers: zod_1.z.array(exports.websocketServerSchema).refine((array) => {
const idsSeen = new Set();
return array.filter((obj) => {
if (idsSeen.has(obj.id)) {
return false;
}
else {
idsSeen.add(obj.id);
return true;
}
});
}),
}),
certificateAuthority: zod_1.z.object({
v2gCA: zod_1.z
.object({
name: zod_1.z.enum(['hubject']),
hubject: zod_1.z
.object({
baseUrl: zod_1.z.string(),
tokenUrl: zod_1.z.string(),
isoVersion: zod_1.z.enum(['ISO15118-2', 'ISO15118-20']),
})
.optional(),
})
.refine((obj) => {
if (obj.name === 'hubject') {
return obj.hubject;
}
else {
return false;
}
}),
chargingStationCA: zod_1.z
.object({
name: zod_1.z.enum(['acme']),
acme: zod_1.z
.object({
env: zod_1.z.enum(['staging', 'production']),
accountKeyFilePath: zod_1.z.string(),
email: zod_1.z.string(),
})
.optional(),
})
.refine((obj) => {
if (obj.name === 'acme') {
return obj.acme;
}
else {
return false;
}
}),
}),
}),
logLevel: zod_1.z.number().min(0).max(6),
maxCallLengthSeconds: zod_1.z.number().int().positive(),
maxCachingSeconds: zod_1.z.number().int().positive(),
ocpiServer: zod_1.z.object({
host: zod_1.z.string(),
port: zod_1.z.number().int().positive(),
}),
userPreferences: zod_1.z.object({
telemetryConsent: zod_1.z.boolean().optional(),
}),
configFileName: zod_1.z.string().default('config.json'),
configDir: zod_1.z.string().optional(),
})
.refine((obj) => obj.maxCachingSeconds >= obj.maxCallLengthSeconds, {
message: 'maxCachingSeconds cannot be less than maxCallLengthSeconds',
});
//# sourceMappingURL=types.js.map