UNPKG

@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
"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