refactor: migrate all internal imports to package.json subpath aliases

- Add #domain, #protocol, #utils aliases to package.json imports map
- Replace all relative imports (./, ../, ../..) in app/ and
  database/mongo_migrations/ with the corresponding #alias imports,
  eliminating relative path traversal across the codebase
- Remove dead code: ConfigurationVersionModel.findByConfigurationIdentifier
  (unused, superseded by paginateVersions) and unused exception classes
  (NotFoundException, UnprocessableEntityException, DatabaseException)
  from app/exceptions/index.ts, keeping only BadRequestException
- Fix pre-existing syntax error in configuration_version.ts (mismatched
  parenthesis) that broke tsc
- Fix eqeqeq violation in configuration_service.ts and isNaN ->
  Number.isNaN in configuration_engine.ts / cwmp-1-0.ts / exchange_controller.ts
- Fix no-shadow violation in cwmp-1-0.ts EventStruct constructor
- Prettier formatting pass on touched files

Validated: tsc --noEmit clean, eslint app/ clean, full test suite green.
This commit is contained in:
Kevin L'Hereec committed 2026-07-22 11:45:14 +02:00
1 parent 4177f1adbd
commit f9e56c78b1
74 files changed
+1243 -863

No files matched your search

+45 -15
View File
@@ -6,7 +6,10 @@ import ConfigurationService, {
} from '#services/configuration_service'
import ConfigurationEngine from '#services/configuration_engine'
import ConfigurationModel from '#models/configuration'
import { createConfigurationValidator, updateConfigurationValidator } from '#validators/configuration'
import {
createConfigurationValidator,
updateConfigurationValidator,
} from '#validators/configuration'
import logger from '@adonisjs/core/services/logger'
/**
@@ -43,7 +46,8 @@ export default class ConfigurationsController {
perPage: Number(request.input('perPage', 20)),
tag: request.input('tag'),
name: request.input('name') || undefined,
enabled: request.input('enabled') !== undefined ? request.input('enabled') === 'true' : undefined,
enabled:
request.input('enabled') !== undefined ? request.input('enabled') === 'true' : undefined,
productClass: request.input('productClass'),
}
@@ -93,11 +97,21 @@ export default class ConfigurationsController {
*/
async store({ request, response }: HttpContext) {
try {
const data = await request.validateUsing(createConfigurationValidator) as CreateConfigurationDto
const data = (await request.validateUsing(
createConfigurationValidator
)) as CreateConfigurationDto
// Check for overlaps
const overlapResult = await this.configService.detectOverlap(data.criteria || {}, data.priority)
if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) {
const overlapResult = await this.configService.detectOverlap(
data.criteria || {},
data.priority
)
if (
overlapResult &&
overlapResult.overlappingConfigurations &&
overlapResult.overlappingConfigurations.length > 0 &&
!request.input('ignoreOverlap')
) {
return response.status(409).json({
message: 'Configuration overlaps with existing ones',
overlaps: overlapResult.overlappingConfigurations,
@@ -122,7 +136,9 @@ export default class ConfigurationsController {
*/
async update({ params, request, response }: HttpContext) {
try {
const data = await request.validateUsing(updateConfigurationValidator) as UpdateConfigurationDto
const data = (await request.validateUsing(
updateConfigurationValidator
)) as UpdateConfigurationDto
// Check for overlaps if priority changed
if (data.priority) {
@@ -131,13 +147,18 @@ export default class ConfigurationsController {
data.priority,
params.id
)
if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) {
return response.status(409).json({
message: 'Configuration overlaps with existing ones',
overlaps: overlapResult.overlappingConfigurations,
})
if (
overlapResult &&
overlapResult.overlappingConfigurations &&
overlapResult.overlappingConfigurations.length > 0 &&
!request.input('ignoreOverlap')
) {
return response.status(409).json({
message: 'Configuration overlaps with existing ones',
overlaps: overlapResult.overlappingConfigurations,
})
}
}
}
// Update configuration
const config = await this.configService.update(params.id, data)
@@ -147,7 +168,10 @@ export default class ConfigurationsController {
return response.ok(config)
} catch (error: any) {
logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Update configuration failed')
logger.error(
{ error: error.message, id: params.id },
'[ConfigAPI] Update configuration failed'
)
if (error.field) {
return response.status(422).json({ message: error.message, field: error.field })
}
@@ -164,7 +188,10 @@ export default class ConfigurationsController {
await this.configService.delete(params.id)
return response.status(204).send(null)
} catch (error: any) {
logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Delete configuration failed')
logger.error(
{ error: error.message, id: params.id },
'[ConfigAPI] Delete configuration failed'
)
return response.status(500).json({ message: 'Failed to delete configuration' })
}
}
@@ -182,7 +209,10 @@ export default class ConfigurationsController {
}
return response.ok(config)
} catch (error: any) {
logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Toggle configuration failed')
logger.error(
{ error: error.message, id: params.id },
'[ConfigAPI] Toggle configuration failed'
)
return response.status(500).json({ message: 'Failed to toggle configuration' })
}
}
+12 -12
View File
@@ -1,17 +1,17 @@
import type { HttpContext } from '@adonisjs/core/http'
import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js'
import { StateMachineManager } from '../services/cwmp/state_machine_manager.js'
import { Device, detectDataModelFromParams } from '../domain/device/device.js'
import { DeviceId } from '../domain/device/device_id.js'
import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js'
import DeviceModel from '../models/device.js'
import { getDeviceHashIdentifier } from '../utils/device_utils.js'
import { getReferenceEngine } from '../services/references/reference_engine.js'
import { normalizeEventCode } from '../utils/cwmp-events.js'
import { CWMPMessageBuilder } from '#protocol/common/cwmp_message_builder'
import { StateMachineManager } from '#services/cwmp/state_machine_manager'
import { Device, detectDataModelFromParams } from '#domain/device/device'
import { DeviceId } from '#domain/device/device_id'
import { DeviceParameter, DeviceProperty } from '#domain/device/device_parameter'
import DeviceModel from '#models/device'
import { getDeviceHashIdentifier } from '#utils/device_utils'
import { getReferenceEngine } from '#services/references/reference_engine'
import { normalizeEventCode } from '#utils/cwmp-events'
import logger from '@adonisjs/core/services/logger'
import type { Inform, CwmpMessage, ParameterValueStruct } from '../protocol/tr069_types.js'
import { ParameterType } from '../domain/shared/types.js'
import { FaultCodeType } from '../protocol/tr069_types.js'
import type { Inform, CwmpMessage, ParameterValueStruct } from '#protocol/tr069_types'
import { ParameterType } from '#domain/shared/types'
import { FaultCodeType } from '#protocol/tr069_types'
export default class CwmpController {
private static managerInitialized = false
+8 -4
View File
@@ -1,7 +1,7 @@
import type { HttpContext } from '@adonisjs/core/http'
import DeviceService from '#services/device_service'
import { PaginationData } from '#services/device_service'
import { CommandType } from '../domain/commands/command.js'
import { type PaginationData } from '#services/device_service'
import { type CommandType } from '#domain/commands/command'
import DeviceModel from '#models/device'
export default class DevicesController {
@@ -77,10 +77,14 @@ export default class DevicesController {
const parts: string[] = []
if (pager.first) {
parts.push(`<${baseUrl}?offset=${pager.first.offset}&limit=${pager.first.limit}>; rel="first"`)
parts.push(
`<${baseUrl}?offset=${pager.first.offset}&limit=${pager.first.limit}>; rel="first"`
)
}
if (pager.previous) {
parts.push(`<${baseUrl}?offset=${pager.previous.offset}&limit=${pager.previous.limit}>; rel="prev"`)
parts.push(
`<${baseUrl}?offset=${pager.previous.offset}&limit=${pager.previous.limit}>; rel="prev"`
)
}
if (pager.next) {
parts.push(`<${baseUrl}?offset=${pager.next.offset}&limit=${pager.next.limit}>; rel="next"`)
+6 -6
View File
@@ -247,7 +247,10 @@ export default class ExchangeController {
// Track SSE stream setup time
const setupDurationMs = performance.now() - startTime
ExchangeMetricsService.getInstance().recordApiResponseTime('GET /exchanges/stream', setupDurationMs)
ExchangeMetricsService.getInstance().recordApiResponseTime(
'GET /exchanges/stream',
setupDurationMs
)
const listener = (exchange: any) => {
if (closed) return
@@ -333,10 +336,7 @@ export default class ExchangeController {
*
* Requirement 6.9: Event id equals the exchange's sequenceNumber
*/
private sendSSEEvent(
res: import('node:http').ServerResponse,
exchange: any
): void {
private sendSSEEvent(res: import('node:http').ServerResponse, exchange: any): void {
const data = JSON.stringify(exchange)
res.write(`id: ${exchange.sequenceNumber}\ndata: ${data}\n\n`)
}
@@ -347,7 +347,7 @@ export default class ExchangeController {
*/
private isValidIso8601(value: string): boolean {
const date = new Date(value)
if (isNaN(date.getTime())) {
if (Number.isNaN(date.getTime())) {
return false
}
+11 -3
View File
@@ -3,7 +3,11 @@ import FirmwareCatalogService, {
type CreateFirmwareEntryDto,
type CreateMigrationRouteDto,
} from '#services/firmware_catalog_service'
import { createFirmwareEntryValidator, updateFirmwareEntryValidator, createMigrationRouteValidator } from '#validators/firmware_catalog'
import {
createFirmwareEntryValidator,
updateFirmwareEntryValidator,
createMigrationRouteValidator,
} from '#validators/firmware_catalog'
import logger from '@adonisjs/core/services/logger'
/**
@@ -105,7 +109,9 @@ export default class FirmwareCatalogController {
*/
async createEntry({ request, response }: HttpContext) {
try {
const data = await request.validateUsing(createFirmwareEntryValidator) as CreateFirmwareEntryDto
const data = (await request.validateUsing(
createFirmwareEntryValidator
)) as CreateFirmwareEntryDto
const entry = await this.catalogService.createEntry(data)
return response.status(201).json(entry)
@@ -135,7 +141,9 @@ export default class FirmwareCatalogController {
*/
async createRoute({ request, response }: HttpContext) {
try {
const data = await request.validateUsing(createMigrationRouteValidator) as CreateMigrationRouteDto
const data = (await request.validateUsing(
createMigrationRouteValidator
)) as CreateMigrationRouteDto
const route = await this.catalogService.createMigrationRoute(data)
return response.status(201).json(route)
+17 -3
View File
@@ -33,11 +33,25 @@ export default class PagesController {
// Predefined model paths for the "Get Model" modal
// Detect data model from device properties to suggest the right root
const isTr181 = (device as any).dataModel === 'tr-181' ||
const isTr181 =
(device as any).dataModel === 'tr-181' ||
(device as any).properties?.some((p: any) => p.name?.startsWith('Device.'))
const modelPaths = isTr181
? ['Device.', 'Device.DeviceInfo.', 'Device.IP.', 'Device.WiFi.', 'Device.ManagementServer.', 'Device.Hosts.']
: ['InternetGatewayDevice.', 'InternetGatewayDevice.DeviceInfo.', 'InternetGatewayDevice.LANDevice.', 'InternetGatewayDevice.WANDevice.', 'InternetGatewayDevice.ManagementServer.']
? [
'Device.',
'Device.DeviceInfo.',
'Device.IP.',
'Device.WiFi.',
'Device.ManagementServer.',
'Device.Hosts.',
]
: [
'InternetGatewayDevice.',
'InternetGatewayDevice.DeviceInfo.',
'InternetGatewayDevice.LANDevice.',
'InternetGatewayDevice.WANDevice.',
'InternetGatewayDevice.ManagementServer.',
]
return (inertia as any).render('DeviceDetail', { device, commands, modelPaths })
}
+11 -4
View File
@@ -1,6 +1,6 @@
import type { HttpContext } from '@adonisjs/core/http'
import DeviceModel from '#models/device'
import { getReferenceEngine } from '../services/references/reference_engine.js'
import { getReferenceEngine } from '#services/references/reference_engine'
export default class ReferencesController {
/**
@@ -24,7 +24,9 @@ export default class ReferencesController {
}
const ref = (device.references || []).find((r: any) => r.provider === params.provider)
if (!ref) {
return response.status(404).json({ message: `No reference from provider '${params.provider}'` })
return response
.status(404)
.json({ message: `No reference from provider '${params.provider}'` })
}
return response.json(ref)
}
@@ -51,7 +53,10 @@ export default class ReferencesController {
(r: any) => !results.some((nr) => nr.provider === r.provider)
)
const merged = [...existing, ...results]
await DeviceModel.updateOne({ identifier: params.identifier }, { $set: { references: merged, updatedAt: new Date() } })
await DeviceModel.updateOne(
{ identifier: params.identifier },
{ $set: { references: merged, updatedAt: new Date() } }
)
}
return response.json({ resolved: results.length, references: results })
@@ -72,7 +77,9 @@ export default class ReferencesController {
const result = await engine.resolveOne(params.provider, device, context)
if (!result) {
return response.status(404).json({ message: `Provider '${params.provider}' not found or cannot resolve` })
return response
.status(404)
.json({ message: `Provider '${params.provider}' not found or cannot resolve` })
}
// Merge into device references
+6 -4
View File
@@ -1,6 +1,6 @@
import { DeviceId } from '../device/device_id.js'
import { ModeType } from '../shared/mode.js'
import { ObjectId } from 'mongodb'
import { type DeviceId } from '#domain/device/device_id'
import { type ModeType } from '#domain/shared/mode'
import { type ObjectId } from 'mongodb'
export type CommandId = string
@@ -136,7 +136,9 @@ export class Command {
}
public isCompleted(): boolean {
return [CommandStatus.COMPLETED, CommandStatus.FAILED, CommandStatus.CANCELLED].includes(this._status)
return [CommandStatus.COMPLETED, CommandStatus.FAILED, CommandStatus.CANCELLED].includes(
this._status
)
}
public hasExpired(timeoutMinutes: number = 30): boolean {
@@ -1,4 +1,4 @@
import { Configuration } from './configuration.js'
import { type Configuration } from '#domain/configuration/configuration'
export interface ConfigurationVersion {
configurationIdentifier: string
+4 -4
View File
@@ -1,13 +1,13 @@
import { type DeviceId } from './device_id.js'
import { type DeviceId } from '#domain/device/device_id'
import {
DeviceParameter,
DeviceProperty,
type ParameterSnapshot,
type PropertySnapshot,
} from './device_parameter.js'
import { ParameterType } from '../shared/types.js'
} from '#domain/device/device_parameter'
import { ParameterType } from '#domain/shared/types'
import { createHash } from 'node:crypto'
import type { ParameterValueStruct, ParameterInfoStruct } from '../../protocol/tr069_types.js'
import type { ParameterValueStruct, ParameterInfoStruct } from '#protocol/tr069_types'
export type GpnStatus = 'supported' | 'unsupported' | 'unknown'
+6 -1
View File
@@ -30,7 +30,12 @@ export class DeviceId {
)
}
public static fromInform(informData: { Manufacturer?: string; OUI?: string; ProductClass?: string; SerialNumber?: string }): DeviceId {
public static fromInform(informData: {
Manufacturer?: string
OUI?: string
ProductClass?: string
SerialNumber?: string
}): DeviceId {
return new DeviceId(
String(informData?.Manufacturer ?? ''),
String(informData?.OUI ?? ''),
+1 -1
View File
@@ -1,4 +1,4 @@
import { ParameterType } from '../shared/types.js'
import { ParameterType } from '#domain/shared/types'
export interface ParameterSnapshot {
name: string
+4 -4
View File
@@ -1,7 +1,7 @@
import { type DeviceId } from './device_id.js'
import { type PropertySnapshot, type ParameterSnapshot } from './device_parameter.js'
import { type DeviceComplianceInfo } from '../configuration/compliance.js'
import { type DataModel, type GpnStatus, type GpnCircuitBreaker } from './device.js'
import { type DeviceId } from '#domain/device/device_id'
import { type PropertySnapshot, type ParameterSnapshot } from '#domain/device/device_parameter'
import { type DeviceComplianceInfo } from '#domain/configuration/compliance'
import { type DataModel, type GpnStatus, type GpnCircuitBreaker } from '#domain/device/device'
import type { DeviceReference } from '#services/references/types'
export interface DeviceRecord {
-15
View File
@@ -1,21 +1,6 @@
import { Exception } from '@adonisjs/core/exceptions'
export class NotFoundException extends Exception {
static status = 404
static code = 'E_NOT_FOUND'
}
export class BadRequestException extends Exception {
static status = 400
static code = 'E_BAD_REQUEST'
}
export class UnprocessableEntityException extends Exception {
static status = 422
static code = 'E_UNPROCESSABLE'
}
export class DatabaseException extends Exception {
static status = 503
static code = 'E_DATABASE_ERROR'
}
+2 -2
View File
@@ -1,7 +1,7 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { parseSoapXml, extractCwmpMessage } from '../protocol/soap.js'
import type { SoapEnvelope, CwmpMessage } from '../protocol/tr069_types.js'
import { parseSoapXml, extractCwmpMessage } from '#protocol/soap'
import type { SoapEnvelope, CwmpMessage } from '#protocol/tr069_types'
/**
* CWMP Middleware
+9
View File
@@ -3,6 +3,10 @@ import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class InertiaMiddleware extends BaseInertiaMiddleware {
/**
* Implements BaseInertiaMiddleware's abstract `share` method.
* Invoked internally by @adonisjs/inertia — not called directly from this codebase.
*/
async share(ctx: HttpContext) {
return {
errors: ctx.session?.flashMessages.get('errors'),
@@ -13,6 +17,11 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware {
}
}
/**
* Standard AdonisJS middleware entrypoint.
* Invoked dynamically by AdonisJS via the named middleware loader in start/kernel.ts —
* not called directly from this codebase.
*/
async handle(ctx: HttpContext, next: NextFn) {
await this.init(ctx)
await next()
+9 -4
View File
@@ -1,6 +1,6 @@
import { BaseModel } from '#database/base_model'
import { CommandRecord, CommandStatus } from '../domain/commands/command.js'
import { ModeType } from '../domain/shared/mode.js'
import { type CommandRecord, CommandStatus } from '#domain/commands/command'
import { type ModeType } from '#domain/shared/mode'
import { ObjectId } from 'mongodb'
export default class CommandModel extends BaseModel {
@@ -8,14 +8,19 @@ export default class CommandModel extends BaseModel {
static async findByDevice(deviceIdentifier: string): Promise<CommandRecord[]> {
const collection = this.getCollection()
return collection.find({ deviceIdentifier }).sort({ createdAt: -1 }).toArray() as unknown as Promise<CommandRecord[]>
return (await collection
.find({ deviceIdentifier })
.sort({ createdAt: -1 })
.toArray()) as unknown as Promise<CommandRecord[]>
}
static async findPending(deviceIdentifier: string, mode?: ModeType): Promise<CommandRecord[]> {
const filter: any = { deviceIdentifier, status: CommandStatus.PENDING }
if (mode) filter.mode = mode
const collection = this.getCollection()
return collection.find(filter).sort({ priority: 1 }).toArray() as unknown as Promise<CommandRecord[]>
return collection.find(filter).sort({ priority: 1 }).toArray() as unknown as Promise<
CommandRecord[]
>
}
static async createCommand(command: Omit<CommandRecord, '_id'>): Promise<CommandRecord> {
+13 -5
View File
@@ -1,6 +1,6 @@
import { BaseModel } from '#database/base_model'
import { Configuration } from '../domain/configuration/configuration.js'
import { Filter, Sort } from 'mongodb'
import { type Configuration } from '#domain/configuration/configuration'
import { type Filter, type Sort } from 'mongodb'
export default class ConfigurationModel extends BaseModel {
static collectionName = 'configurations'
@@ -11,7 +11,9 @@ export default class ConfigurationModel extends BaseModel {
static async findAllEnabled(sort: Sort = { priority: -1 }): Promise<Configuration[]> {
const collection = this.getCollection()
return collection.find({ enabled: true }).sort(sort).toArray() as unknown as Promise<Configuration[]>
return (await collection.find({ enabled: true }).sort(sort).toArray()) as unknown as Promise<
Configuration[]
>
}
static async paginateConfigurations(
@@ -19,7 +21,13 @@ export default class ConfigurationModel extends BaseModel {
perPage: number,
filter: Filter<any> = {},
sort: Sort = { priority: -1 }
): Promise<{ data: Configuration[]; total: number; page: number; perPage: number; lastPage: number }> {
): Promise<{
data: Configuration[]
total: number
page: number
perPage: number
lastPage: number
}> {
return this.paginate(page, perPage, filter, sort)
}
@@ -28,7 +36,7 @@ export default class ConfigurationModel extends BaseModel {
if (existing) {
throw new Error(`Configuration with identifier '${data.identifier}' already exists`)
}
return this.create(data) as Promise<Configuration>
return (await this.create(data)) as Configuration
}
static async distinctTags(): Promise<string[]> {
+8 -14
View File
@@ -1,26 +1,20 @@
import { BaseModel } from '#database/base_model'
import { type ConfigurationVersion } from '../domain/configuration/configuration_version.js'
import { Sort } from 'mongodb'
import { type ConfigurationVersion } from '#domain/configuration/configuration_version'
export default class ConfigurationVersionModel extends BaseModel {
static collectionName = 'configuration_versions'
static async findByConfigurationIdentifier(
configurationIdentifier: string,
sort: Sort = { versionNumber: -1 }
): Promise<ConfigurationVersion[]> {
const collection = this.getCollection()
return collection
.find({ configurationIdentifier })
.sort(sort)
.toArray() as unknown as Promise<ConfigurationVersion[]>
}
static async paginateVersions(
configurationIdentifier: string,
page: number,
perPage: number
): Promise<{ data: ConfigurationVersion[]; total: number; page: number; perPage: number; lastPage: number }> {
): Promise<{
data: ConfigurationVersion[]
total: number
page: number
perPage: number
lastPage: number
}> {
return this.paginate(page, perPage, { configurationIdentifier }, { versionNumber: -1 })
}
+6 -9
View File
@@ -1,8 +1,8 @@
import { BaseModel } from '#database/base_model'
import { DeviceRecord } from '../domain/device/device_record.js'
import { DeviceId } from '../domain/device/device_id.js'
import { DeviceComplianceInfo } from '../domain/configuration/compliance.js'
import { Filter, Sort } from 'mongodb'
import { type DeviceRecord } from '#domain/device/device_record'
import { type DeviceId } from '#domain/device/device_id'
import { type DeviceComplianceInfo } from '#domain/configuration/compliance'
import { type Filter, type Sort } from 'mongodb'
export default class DeviceModel extends BaseModel {
static collectionName = 'devices'
@@ -87,10 +87,7 @@ export default class DeviceModel extends BaseModel {
await this.updateOne({ identifier }, { compliance })
}
static async updateProvisioningCode(
identifier: string,
provisioningCode: string
): Promise<void> {
static async updateProvisioningCode(identifier: string, provisioningCode: string): Promise<void> {
await this.updateOne({ identifier }, { provisioningCode })
}
@@ -102,7 +99,7 @@ export default class DeviceModel extends BaseModel {
$set: {
'compliance.status': 'pending',
'compliance.lastError': null,
updatedAt: new Date(),
'updatedAt': new Date(),
},
}
)
+4 -4
View File
@@ -45,19 +45,19 @@ export default class ExchangeModel extends BaseModel {
// When using `latest` or `before`, we want the most recent messages.
// Query in descending order then reverse to return in ascending order.
if (options?.latest || options?.before) {
const results = await collection
const results = (await collection
.find(filter)
.sort({ timestamp: -1 })
.limit(limit)
.toArray() as unknown as CwmpExchange[]
.toArray()) as unknown as CwmpExchange[]
return results.reverse()
}
return collection
return (await collection
.find(filter)
.sort({ timestamp: 1 })
.limit(limit)
.toArray() as unknown as Promise<CwmpExchange[]>
.toArray()) as unknown as CwmpExchange[]
}
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { BaseModel } from '#database/base_model'
import { FirmwareEntry } from '../domain/firmware/firmware_catalog.js'
import { type FirmwareEntry } from '#domain/firmware/firmware_catalog'
export default class FirmwareEntryModel extends BaseModel {
static collectionName = 'firmware_entries'
+7 -5
View File
@@ -1,12 +1,12 @@
import { BaseModel } from '#database/base_model'
import { MigrationRoute } from '../domain/firmware/firmware_catalog.js'
import { Sort } from 'mongodb'
import { type MigrationRoute } from '#domain/firmware/firmware_catalog'
import { type Sort } from 'mongodb'
export default class MigrationRouteModel extends BaseModel {
static collectionName = 'migration_routes'
static async findByIdentifier(identifier: string): Promise<MigrationRoute | null> {
return this.findOne({ identifier }) as Promise<MigrationRoute | null>
return (await this.findOne({ identifier })) as Promise<MigrationRoute | null>
}
static async findByProductClass(
@@ -14,7 +14,9 @@ export default class MigrationRouteModel extends BaseModel {
sort: Sort = { priority: -1 }
): Promise<MigrationRoute[]> {
const collection = this.getCollection()
return collection.find({ productClass }).sort(sort).toArray() as unknown as Promise<MigrationRoute[]>
return (await collection.find({ productClass }).sort(sort).toArray()) as unknown as Promise<
MigrationRoute[]
>
}
static async createIfNotExists(data: Partial<MigrationRoute>): Promise<MigrationRoute> {
@@ -22,7 +24,7 @@ export default class MigrationRouteModel extends BaseModel {
if (existing) {
throw new Error(`Migration route with identifier '${data.identifier}' already exists`)
}
return this.create(data) as Promise<MigrationRoute>
return (await this.create(data)) as Promise<MigrationRoute>
}
static async deleteByIdentifier(identifier: string): Promise<boolean> {
+3 -3
View File
@@ -21,20 +21,20 @@ import {
SetParameterValues,
TransferCompleteResponse,
Upload,
} from '../cwmp-1-0.js'
} from '#protocol/cwmp-1-0'
import {
CwmpID,
NAMESPACES,
SoapEnvelope,
type SoapEnvelopeAttribute,
SoapEnvelopeHeader,
} from '../soap_envelope.js'
} from '#protocol/soap_envelope'
import type {
DownloadTaskPayload,
UploadTaskPayload,
SetParameterValuesTaskPayload,
SetParameterAttributesTaskPayload,
} from '../tr069_types.js'
} from '#protocol/tr069_types'
function generateCwmpID(): string {
return randomBytes(4).toString('hex')
+412 -306
View File
File diff suppressed because it is too large. Load diff
+50 -40
View File
@@ -1,9 +1,9 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_0, FaultCodeType } from './cwmp-1-0.js'
import { Cwmp_1_0, FaultCodeType } from '#protocol/cwmp-1-0'
/**
* CWMP Version 1.1 Protocol Classes
*
*
* This version extends CWMP 1.0 with additional features:
* - Additional fault codes (9014-9019)
* - New event code: "10 AUTONOMOUS TRANSFER COMPLETE"
@@ -12,17 +12,25 @@ import { Cwmp_1_0, FaultCodeType } from './cwmp-1-0.js'
*/
export class Cwmp_1_1 extends Cwmp_1_0 {
public '@class' = '.Cwmp_1_1'
public GetAllQueuedTransfers?: GetAllQueuedTransfers
public GetAllQueuedTransfersResponse?: GetAllQueuedTransfersResponse
public AutonomousTransferComplete?: AutonomousTransferComplete
public AutonomousTransferCompleteResponse?: AutonomousTransferCompleteResponse
public 'GetAllQueuedTransfers'?: GetAllQueuedTransfers
public 'GetAllQueuedTransfersResponse'?: GetAllQueuedTransfersResponse
public 'AutonomousTransferComplete'?: AutonomousTransferComplete
public 'AutonomousTransferCompleteResponse'?: AutonomousTransferCompleteResponse
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.GetAllQueuedTransfers = props.GetAllQueuedTransfers ? new GetAllQueuedTransfers(props.GetAllQueuedTransfers) : undefined
this.GetAllQueuedTransfersResponse = props.GetAllQueuedTransfersResponse ? new GetAllQueuedTransfersResponse(props.GetAllQueuedTransfersResponse) : undefined
this.AutonomousTransferComplete = props.AutonomousTransferComplete ? new AutonomousTransferComplete(props.AutonomousTransferComplete) : undefined
this.AutonomousTransferCompleteResponse = props.AutonomousTransferCompleteResponse ? new AutonomousTransferCompleteResponse(props.AutonomousTransferCompleteResponse) : undefined
this.GetAllQueuedTransfers = props.GetAllQueuedTransfers
? new GetAllQueuedTransfers(props.GetAllQueuedTransfers)
: undefined
this.GetAllQueuedTransfersResponse = props.GetAllQueuedTransfersResponse
? new GetAllQueuedTransfersResponse(props.GetAllQueuedTransfersResponse)
: undefined
this.AutonomousTransferComplete = props.AutonomousTransferComplete
? new AutonomousTransferComplete(props.AutonomousTransferComplete)
: undefined
this.AutonomousTransferCompleteResponse = props.AutonomousTransferCompleteResponse
? new AutonomousTransferCompleteResponse(props.AutonomousTransferCompleteResponse)
: undefined
}
}
@@ -100,14 +108,14 @@ export enum EventCode_1_1 {
export class AllQueuedTransferStruct {
public '@class' = '.AllQueuedTransferStruct'
public CommandKey: string
public State: number
public IsDownload: boolean
public FileType: string
public FileSize: number
public TargetFileName: string
public 'CommandKey': string
public 'State': number
public 'IsDownload': boolean
public 'FileType': string
public 'FileSize': number
public 'TargetFileName': string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.CommandKey = props.CommandKey || ''
this.State = props.State || 1
this.IsDownload = props.IsDownload || false
@@ -119,32 +127,34 @@ export class AllQueuedTransferStruct {
export class AllTransferList {
public '@class' = '.AllTransferList'
public AllQueuedTransferStruct: AllQueuedTransferStruct[] = []
public 'AllQueuedTransferStruct': AllQueuedTransferStruct[] = []
constructor(props: any = {}) {
this.AllQueuedTransferStruct = (props.AllQueuedTransferStruct || []).map((item: any) => new AllQueuedTransferStruct(item))
'constructor'(props: any = {}) {
this.AllQueuedTransferStruct = (props.AllQueuedTransferStruct || []).map(
(item: any) => new AllQueuedTransferStruct(item)
)
}
}
export class GetAllQueuedTransfers {
public '@class' = '.GetAllQueuedTransfers'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:GetAllQueuedTransfers></cwmp:GetAllQueuedTransfers>`
}
}
export class GetAllQueuedTransfersResponse {
public '@class' = '.GetAllQueuedTransfersResponse'
public TransferList?: AllTransferList
public 'TransferList'?: AllTransferList
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.TransferList = props.TransferList ? new AllTransferList(props.TransferList) : undefined
}
getXml() {
'getXml'() {
const items = this.TransferList?.AllQueuedTransferStruct || []
const listXml = items
.map(
@@ -170,17 +180,17 @@ export class GetAllQueuedTransfersResponse {
export class AutonomousTransferComplete {
public '@class' = '.AutonomousTransferComplete'
public AnnounceURL: string
public TransferURL: string
public IsDownload: boolean
public FileType: string
public FileSize: number
public TargetFileName: string
public FaultStruct: any // Reusing FaultStruct from base
public StartTime: Date
public CompleteTime: Date
public 'AnnounceURL': string
public 'TransferURL': string
public 'IsDownload': boolean
public 'FileType': string
public 'FileSize': number
public 'TargetFileName': string
public 'FaultStruct': any // Reusing FaultStruct from base
public 'StartTime': Date
public 'CompleteTime': Date
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.AnnounceURL = props.AnnounceURL || ''
this.TransferURL = props.TransferURL || ''
this.IsDownload = props.IsDownload || false
@@ -192,7 +202,7 @@ export class AutonomousTransferComplete {
this.CompleteTime = props.CompleteTime ? new Date(props.CompleteTime) : new Date()
}
getXml() {
'getXml'() {
const fault = this.FaultStruct
? `<FaultStruct><FaultCode>${this.FaultStruct.FaultCode ?? 0}</FaultCode><FaultString>${this.FaultStruct.FaultString || ''}</FaultString></FaultStruct>`
: ''
@@ -213,9 +223,9 @@ export class AutonomousTransferComplete {
export class AutonomousTransferCompleteResponse {
public '@class' = '.AutonomousTransferCompleteResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return '<cwmp:AutonomousTransferCompleteResponse/>'
}
}
}
+157 -131
View File
@@ -1,6 +1,9 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_0, FaultStruct } from './cwmp-1-0.js'
import { Cwmp_1_1, AutonomousTransferComplete as AutonomousTransferComplete_1_1 } from './cwmp-1-1.js'
import { Cwmp_1_0, FaultStruct } from '#protocol/cwmp-1-0'
import {
Cwmp_1_1,
AutonomousTransferComplete as AutonomousTransferComplete_1_1,
} from '#protocol/cwmp-1-1'
/**
* CWMP Version 1.2 Protocol Classes
@@ -17,35 +20,55 @@ export class Cwmp_1_2 extends Cwmp_1_1 {
public '@class' = '.Cwmp_1_2'
// New RPCs
public ScheduleDownload?: ScheduleDownload
public ScheduleDownloadResponse?: ScheduleDownloadResponse
public CancelTransfer?: CancelTransfer
public CancelTransferResponse?: CancelTransferResponse
public ChangeDUState?: ChangeDUState
public ChangeDUStateResponse?: ChangeDUStateResponse
public DUStateChangeComplete?: DUStateChangeComplete
public DUStateChangeCompleteResponse?: DUStateChangeCompleteResponse
public AutonomousDUStateChangeComplete?: AutonomousDUStateChangeComplete
public AutonomousDUStateChangeCompleteResponse?: AutonomousDUStateChangeCompleteResponse
public 'ScheduleDownload'?: ScheduleDownload
public 'ScheduleDownloadResponse'?: ScheduleDownloadResponse
public 'CancelTransfer'?: CancelTransfer
public 'CancelTransferResponse'?: CancelTransferResponse
public 'ChangeDUState'?: ChangeDUState
public 'ChangeDUStateResponse'?: ChangeDUStateResponse
public 'DUStateChangeComplete'?: DUStateChangeComplete
public 'DUStateChangeCompleteResponse'?: DUStateChangeCompleteResponse
public 'AutonomousDUStateChangeComplete'?: AutonomousDUStateChangeComplete
public 'AutonomousDUStateChangeCompleteResponse'?: AutonomousDUStateChangeCompleteResponse
// Override AutonomousTransferComplete to use TransferCompleteFaultStruct
public AutonomousTransferComplete?: AutonomousTransferComplete
public 'AutonomousTransferComplete'?: AutonomousTransferComplete
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.ScheduleDownload = props.ScheduleDownload ? new ScheduleDownload(props.ScheduleDownload) : undefined
this.ScheduleDownloadResponse = props.ScheduleDownloadResponse ? new ScheduleDownloadResponse(props.ScheduleDownloadResponse) : undefined
this.CancelTransfer = props.CancelTransfer ? new CancelTransfer(props.CancelTransfer) : undefined
this.CancelTransferResponse = props.CancelTransferResponse ? new CancelTransferResponse(props.CancelTransferResponse) : undefined
this.ScheduleDownload = props.ScheduleDownload
? new ScheduleDownload(props.ScheduleDownload)
: undefined
this.ScheduleDownloadResponse = props.ScheduleDownloadResponse
? new ScheduleDownloadResponse(props.ScheduleDownloadResponse)
: undefined
this.CancelTransfer = props.CancelTransfer
? new CancelTransfer(props.CancelTransfer)
: undefined
this.CancelTransferResponse = props.CancelTransferResponse
? new CancelTransferResponse(props.CancelTransferResponse)
: undefined
this.ChangeDUState = props.ChangeDUState ? new ChangeDUState(props.ChangeDUState) : undefined
this.ChangeDUStateResponse = props.ChangeDUStateResponse ? new ChangeDUStateResponse(props.ChangeDUStateResponse) : undefined
this.DUStateChangeComplete = props.DUStateChangeComplete ? new DUStateChangeComplete(props.DUStateChangeComplete) : undefined
this.DUStateChangeCompleteResponse = props.DUStateChangeCompleteResponse ? new DUStateChangeCompleteResponse(props.DUStateChangeCompleteResponse) : undefined
this.AutonomousDUStateChangeComplete = props.AutonomousDUStateChangeComplete ? new AutonomousDUStateChangeComplete(props.AutonomousDUStateChangeComplete) : undefined
this.AutonomousDUStateChangeCompleteResponse = props.AutonomousDUStateChangeCompleteResponse ? new AutonomousDUStateChangeCompleteResponse(props.AutonomousDUStateChangeCompleteResponse) : undefined
this.ChangeDUStateResponse = props.ChangeDUStateResponse
? new ChangeDUStateResponse(props.ChangeDUStateResponse)
: undefined
this.DUStateChangeComplete = props.DUStateChangeComplete
? new DUStateChangeComplete(props.DUStateChangeComplete)
: undefined
this.DUStateChangeCompleteResponse = props.DUStateChangeCompleteResponse
? new DUStateChangeCompleteResponse(props.DUStateChangeCompleteResponse)
: undefined
this.AutonomousDUStateChangeComplete = props.AutonomousDUStateChangeComplete
? new AutonomousDUStateChangeComplete(props.AutonomousDUStateChangeComplete)
: undefined
this.AutonomousDUStateChangeCompleteResponse = props.AutonomousDUStateChangeCompleteResponse
? new AutonomousDUStateChangeCompleteResponse(props.AutonomousDUStateChangeCompleteResponse)
: undefined
// Override from 1.1 when provided
this.AutonomousTransferComplete = props.AutonomousTransferComplete ? new AutonomousTransferComplete(props.AutonomousTransferComplete) : this.AutonomousTransferComplete
this.AutonomousTransferComplete = props.AutonomousTransferComplete
? new AutonomousTransferComplete(props.AutonomousTransferComplete)
: this.AutonomousTransferComplete
}
}
@@ -77,13 +100,13 @@ export enum EventCode_1_2 {
// Time window types used by ScheduleDownload
export class TimeWindowStruct {
public '@class' = '.TimeWindowStruct'
public WindowStart: number
public WindowEnd: number
public WindowMode: string
public UserMessage: string
public MaxRetries: number
public 'WindowStart': number
public 'WindowEnd': number
public 'WindowMode': string
public 'UserMessage': string
public 'MaxRetries': number
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.WindowStart = props.WindowStart ?? 0
this.WindowEnd = props.WindowEnd ?? 0
this.WindowMode = props.WindowMode ?? ''
@@ -94,26 +117,28 @@ export class TimeWindowStruct {
export class TimeWindowList {
public '@class' = '.TimeWindowList'
public TimeWindowStruct: TimeWindowStruct[] = []
public 'TimeWindowStruct': TimeWindowStruct[] = []
constructor(props: any = {}) {
this.TimeWindowStruct = (props.TimeWindowStruct || []).map((tw: any) => new TimeWindowStruct(tw))
'constructor'(props: any = {}) {
this.TimeWindowStruct = (props.TimeWindowStruct || []).map(
(tw: any) => new TimeWindowStruct(tw)
)
}
}
// ScheduleDownload RPC
export class ScheduleDownload {
public '@class' = '.ScheduleDownload'
public CommandKey: string
public FileType: string
public URL: string
public Username?: string
public Password?: string
public FileSize?: number
public TargetFileName?: string
public TimeWindowList: TimeWindowList
public 'CommandKey': string
public 'FileType': string
public 'URL': string
public 'Username'?: string
public 'Password'?: string
public 'FileSize'?: number
public 'TargetFileName'?: string
public 'TimeWindowList': TimeWindowList
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.CommandKey = props.CommandKey || ''
this.FileType = props.FileType || ''
this.URL = props.URL || ''
@@ -124,7 +149,7 @@ export class ScheduleDownload {
this.TimeWindowList = new TimeWindowList(props.TimeWindowList || {})
}
getXml() {
'getXml'() {
const tw = this.TimeWindowList?.TimeWindowStruct || []
const twXml = tw
.map(
@@ -156,9 +181,9 @@ export class ScheduleDownload {
export class ScheduleDownloadResponse {
public '@class' = '.ScheduleDownloadResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:ScheduleDownloadResponse/>`
}
}
@@ -166,12 +191,12 @@ export class ScheduleDownloadResponse {
// CancelTransfer RPC
export class CancelTransfer {
public '@class' = '.CancelTransfer'
public CommandKey: string
constructor(props: any = {}) {
public 'CommandKey': string
'constructor'(props: any = {}) {
this.CommandKey = props.CommandKey || ''
}
getXml() {
'getXml'() {
return `<cwmp:CancelTransfer>
<CommandKey>${this.CommandKey}</CommandKey>
</cwmp:CancelTransfer>`
@@ -180,9 +205,9 @@ export class CancelTransfer {
export class CancelTransferResponse {
public '@class' = '.CancelTransferResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:CancelTransferResponse/>`
}
}
@@ -190,19 +215,20 @@ export class CancelTransferResponse {
// Software Module Management types
export type DeploymentUnitState = 'Installed' | 'Uninstalled' | 'Failed'
export class OperationStruct { // abstract base
export class OperationStruct {
// abstract base
public '@class' = '.OperationStruct'
}
export class InstallOpStruct extends OperationStruct {
public '@class' = '.InstallOpStruct'
public URL: string
public UUID?: string
public Username?: string
public Password?: string
public ExecutionEnvRef?: string
public 'URL': string
public 'UUID'?: string
public 'Username'?: string
public 'Password'?: string
public 'ExecutionEnvRef'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super()
this.URL = props.URL || ''
this.UUID = props.UUID
@@ -214,13 +240,13 @@ export class InstallOpStruct extends OperationStruct {
export class UpdateOpStruct extends OperationStruct {
public '@class' = '.UpdateOpStruct'
public UUID?: string
public Version?: string
public URL?: string
public Username?: string
public Password?: string
public 'UUID'?: string
public 'Version'?: string
public 'URL'?: string
public 'Username'?: string
public 'Password'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super()
this.UUID = props.UUID
this.Version = props.Version
@@ -232,11 +258,11 @@ export class UpdateOpStruct extends OperationStruct {
export class UninstallOpStruct extends OperationStruct {
public '@class' = '.UninstallOpStruct'
public UUID: string
public Version?: string
public ExecutionEnvRef?: string
public 'UUID': string
public 'Version'?: string
public 'ExecutionEnvRef'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super()
this.UUID = props.UUID || ''
this.Version = props.Version
@@ -246,10 +272,10 @@ export class UninstallOpStruct extends OperationStruct {
export class DeploymentUnitFaultStruct {
public '@class' = '.DeploymentUnitFaultStruct'
public FaultCode: number
public FaultString?: string
public 'FaultCode': number
public 'FaultString'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.FaultCode = props.FaultCode ?? 0
this.FaultString = props.FaultString
}
@@ -257,17 +283,17 @@ export class DeploymentUnitFaultStruct {
export class OpResultStruct {
public '@class' = '.OpResultStruct'
public UUID: string
public DeploymentUnitRef: string
public Version: string
public CurrentState: DeploymentUnitState
public Resolved: boolean
public ExecutionUnitRefList: string
public StartTime: Date
public CompleteTime: Date
public Fault: DeploymentUnitFaultStruct
public 'UUID': string
public 'DeploymentUnitRef': string
public 'Version': string
public 'CurrentState': DeploymentUnitState
public 'Resolved': boolean
public 'ExecutionUnitRefList': string
public 'StartTime': Date
public 'CompleteTime': Date
public 'Fault': DeploymentUnitFaultStruct
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.UUID = props.UUID || ''
this.DeploymentUnitRef = props.DeploymentUnitRef || ''
this.Version = props.Version || ''
@@ -282,9 +308,9 @@ export class OpResultStruct {
export class AutonOpResultStruct extends OpResultStruct {
public '@class' = '.AutonOpResultStruct'
public OperationPerformed: string
public 'OperationPerformed': string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.OperationPerformed = props.OperationPerformed || ''
}
@@ -293,51 +319,55 @@ export class AutonOpResultStruct extends OpResultStruct {
// ChangeDUState RPC
export class ChangeDUState {
public '@class' = '.ChangeDUState'
public Operations: OperationStruct[] = []
public CommandKey?: string
public 'Operations': OperationStruct[] = []
public 'CommandKey'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.Operations = (props.Operations || []).map((op: any) => {
const type = op?.['@class'] || op?.type
if (type === 'InstallOpStruct' || op?.URL) return new InstallOpStruct(op)
if (type === 'UpdateOpStruct' || op?.Version || (op?.UUID && !op?.ExecutionEnvRef)) return new UpdateOpStruct(op)
if (type === 'UninstallOpStruct' || op?.ExecutionEnvRef || (op?.UUID && !op?.URL && !op?.Version)) return new UninstallOpStruct(op)
if (type === 'UpdateOpStruct' || op?.Version || (op?.UUID && !op?.ExecutionEnvRef))
return new UpdateOpStruct(op)
if (
type === 'UninstallOpStruct' ||
op?.ExecutionEnvRef ||
(op?.UUID && !op?.URL && !op?.Version)
)
return new UninstallOpStruct(op)
return new OperationStruct()
})
this.CommandKey = props.CommandKey
}
getXml() {
const opsXml = this.Operations
.map((op) => {
if (op instanceof InstallOpStruct) {
return `<Operations xsi:type="cwmp:InstallOpStruct">
'getXml'() {
const opsXml = this.Operations.map((op) => {
if (op instanceof InstallOpStruct) {
return `<Operations xsi:type="cwmp:InstallOpStruct">
<URL>${op.URL}</URL>
${op.UUID ? `<UUID>${op.UUID}</UUID>` : ''}
${op.Username ? `<Username>${op.Username}</Username>` : ''}
${op.Password ? `<Password>${op.Password}</Password>` : ''}
${op.ExecutionEnvRef ? `<ExecutionEnvRef>${op.ExecutionEnvRef}</ExecutionEnvRef>` : ''}
</Operations>`
}
if (op instanceof UpdateOpStruct) {
return `<Operations xsi:type="cwmp:UpdateOpStruct">
}
if (op instanceof UpdateOpStruct) {
return `<Operations xsi:type="cwmp:UpdateOpStruct">
${op.UUID ? `<UUID>${op.UUID}</UUID>` : ''}
${op.Version ? `<Version>${op.Version}</Version>` : ''}
${op.URL ? `<URL>${op.URL}</URL>` : ''}
${op.Username ? `<Username>${op.Username}</Username>` : ''}
${op.Password ? `<Password>${op.Password}</Password>` : ''}
</Operations>`
}
if (op instanceof UninstallOpStruct) {
return `<Operations xsi:type="cwmp:UninstallOpStruct">
}
if (op instanceof UninstallOpStruct) {
return `<Operations xsi:type="cwmp:UninstallOpStruct">
<UUID>${op.UUID}</UUID>
${op.Version ? `<Version>${op.Version}</Version>` : ''}
${op.ExecutionEnvRef ? `<ExecutionEnvRef>${op.ExecutionEnvRef}</ExecutionEnvRef>` : ''}
</Operations>`
}
return ''
})
.join('')
}
return ''
}).join('')
return `<cwmp:ChangeDUState>
${opsXml}
@@ -348,9 +378,9 @@ export class ChangeDUState {
export class ChangeDUStateResponse {
public '@class' = '.ChangeDUStateResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:ChangeDUStateResponse/>`
}
}
@@ -358,18 +388,17 @@ export class ChangeDUStateResponse {
// DUStateChangeComplete RPCs
export class DUStateChangeComplete {
public '@class' = '.DUStateChangeComplete'
public Results: OpResultStruct[]
public CommandKey?: string
public 'Results': OpResultStruct[]
public 'CommandKey'?: string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.Results = (props.Results || []).map((r: any) => new OpResultStruct(r))
this.CommandKey = props.CommandKey
}
getXml() {
const resultsXml = this.Results
.map(
(r) => `
'getXml'() {
const resultsXml = this.Results.map(
(r) => `
<Results>
<UUID>${r.UUID}</UUID>
<DeploymentUnitRef>${r.DeploymentUnitRef}</DeploymentUnitRef>
@@ -384,8 +413,7 @@ export class DUStateChangeComplete {
${r.Fault.FaultString ? `<FaultString>${r.Fault.FaultString}</FaultString>` : ''}
</Fault>
</Results>`
)
.join('')
).join('')
return `<cwmp:DUStateChangeComplete>
${resultsXml}
@@ -396,25 +424,24 @@ export class DUStateChangeComplete {
export class DUStateChangeCompleteResponse {
public '@class' = '.DUStateChangeCompleteResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:DUStateChangeCompleteResponse/>`
}
}
export class AutonomousDUStateChangeComplete {
public '@class' = '.AutonomousDUStateChangeComplete'
public Results: AutonOpResultStruct[]
public 'Results': AutonOpResultStruct[]
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.Results = (props.Results || []).map((r: any) => new AutonOpResultStruct(r))
}
getXml() {
const resultsXml = this.Results
.map(
(r) => `
'getXml'() {
const resultsXml = this.Results.map(
(r) => `
<Results>
<UUID>${r.UUID}</UUID>
<DeploymentUnitRef>${r.DeploymentUnitRef}</DeploymentUnitRef>
@@ -430,8 +457,7 @@ export class AutonomousDUStateChangeComplete {
</Fault>
<OperationPerformed>${r.OperationPerformed}</OperationPerformed>
</Results>`
)
.join('')
).join('')
return `<cwmp:AutonomousDUStateChangeComplete>
${resultsXml}
@@ -441,9 +467,9 @@ export class AutonomousDUStateChangeComplete {
export class AutonomousDUStateChangeCompleteResponse {
public '@class' = '.AutonomousDUStateChangeCompleteResponse'
constructor(props: any = {}) {}
'constructor'(props: any = {}) {}
getXml() {
'getXml'() {
return `<cwmp:AutonomousDUStateChangeCompleteResponse/>`
}
}
@@ -451,10 +477,10 @@ export class AutonomousDUStateChangeCompleteResponse {
// TransferComplete-specific fault struct for 1.2
export class TransferCompleteFaultStruct {
public '@class' = '.TransferCompleteFaultStruct'
public FaultCode: number
public FaultString: string
public 'FaultCode': number
public 'FaultString': string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.FaultCode = props.FaultCode ?? 0
this.FaultString = props.FaultString || ''
}
@@ -463,14 +489,14 @@ export class TransferCompleteFaultStruct {
// Override of 1.1 AutonomousTransferComplete to use TransferCompleteFaultStruct
export class AutonomousTransferComplete extends AutonomousTransferComplete_1_1 {
public '@class' = '.AutonomousTransferComplete'
public FaultStruct: TransferCompleteFaultStruct
public 'FaultStruct': TransferCompleteFaultStruct
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.FaultStruct = new TransferCompleteFaultStruct(props.FaultStruct || {})
}
getXml() {
'getXml'() {
return `<cwmp:AutonomousTransferComplete>
<AnnounceURL>${this.AnnounceURL}</AnnounceURL>
<TransferURL>${this.TransferURL}</TransferURL>
+9 -7
View File
@@ -1,5 +1,5 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_2 } from './cwmp-1-2.js'
import { Cwmp_1_2 } from '#protocol/cwmp-1-2'
/**
* CWMP Version 1.3 Protocol Classes
@@ -9,21 +9,23 @@ import { Cwmp_1_2 } from './cwmp-1-2.js'
*/
export class Cwmp_1_3 extends Cwmp_1_2 {
public '@class' = '.Cwmp_1_3'
public SessionTimeout?: SessionTimeout
public 'SessionTimeout'?: SessionTimeout
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.SessionTimeout = props.SessionTimeout ? new SessionTimeout(props.SessionTimeout) : undefined
this.SessionTimeout = props.SessionTimeout
? new SessionTimeout(props.SessionTimeout)
: undefined
}
}
// SOAP Header element for CWMP 1.3
export class SessionTimeout {
public '@class' = '.SessionTimeout'
public MustUnderstand: string = '0'
public _: number
public 'MustUnderstand': string = '0'
public '_': number
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '0'
this._ = props._ ?? 0
}
+16 -12
View File
@@ -1,5 +1,5 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_3 } from './cwmp-1-3.js'
import { Cwmp_1_3 } from '#protocol/cwmp-1-3'
/**
* CWMP Version 1.4 Protocol Classes
@@ -11,13 +11,17 @@ import { Cwmp_1_3 } from './cwmp-1-3.js'
*/
export class Cwmp_1_4 extends Cwmp_1_3 {
public '@class' = '.Cwmp_1_4'
public SupportedCWMPVersions?: SupportedCWMPVersions
public UseCWMPVersion?: UseCWMPVersion
public 'SupportedCWMPVersions'?: SupportedCWMPVersions
public 'UseCWMPVersion'?: UseCWMPVersion
constructor(props: any = {}) {
'constructor'(props: any = {}) {
super(props)
this.SupportedCWMPVersions = props.SupportedCWMPVersions ? new SupportedCWMPVersions(props.SupportedCWMPVersions) : undefined
this.UseCWMPVersion = props.UseCWMPVersion ? new UseCWMPVersion(props.UseCWMPVersion) : undefined
this.SupportedCWMPVersions = props.SupportedCWMPVersions
? new SupportedCWMPVersions(props.SupportedCWMPVersions)
: undefined
this.UseCWMPVersion = props.UseCWMPVersion
? new UseCWMPVersion(props.UseCWMPVersion)
: undefined
}
}
@@ -48,10 +52,10 @@ export enum EventCode_1_4 {
export class SupportedCWMPVersions {
public '@class' = '.SupportedCWMPVersions'
public MustUnderstand?: string // should be 0 per spec, optional due to SOAP 1.1 quirks
public _: string // space or comma separated list per implementation
public 'MustUnderstand'?: string // should be 0 per spec, optional due to SOAP 1.1 quirks
public '_': string // space or comma separated list per implementation
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '0'
this._ = props._ || ''
}
@@ -59,10 +63,10 @@ export class SupportedCWMPVersions {
export class UseCWMPVersion {
public '@class' = '.UseCWMPVersion'
public MustUnderstand: string = '1'
public _: string
public 'MustUnderstand': string = '1'
public '_': string
constructor(props: any = {}) {
'constructor'(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '1'
this._ = props._ || ''
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { XMLParser } from 'fast-xml-parser'
import type { CwmpMessage, SoapEnvelope, SoapHeader } from './tr069_types.js'
import type { CwmpMessage, SoapEnvelope, SoapHeader } from '#protocol/tr069_types'
export { type SoapEnvelope, type SoapHeader, type CwmpMessage } from './tr069_types.js'
export { type SoapEnvelope, type SoapHeader, type CwmpMessage } from '#protocol/tr069_types'
export const CWMP_VERSIONS: Record<string, string> = {
'urn:dslforum-org:cwmp-1-0': '1.0',
+5 -5
View File
@@ -3,11 +3,11 @@
*
* Séparées de soap.ts (le module de parsing d'adam-acs) pour éviter les conflits de nom.
*/
import { Cwmp_1_0 } from './cwmp-1-0.js'
import { Cwmp_1_1 } from './cwmp-1-1.js'
import { Cwmp_1_2 } from './cwmp-1-2.js'
import { Cwmp_1_3, SessionTimeout } from './cwmp-1-3.js'
import { Cwmp_1_4, SupportedCWMPVersions, UseCWMPVersion } from './cwmp-1-4.js'
import { Cwmp_1_0 } from '#protocol/cwmp-1-0'
import { Cwmp_1_1 } from '#protocol/cwmp-1-1'
import { Cwmp_1_2 } from '#protocol/cwmp-1-2'
import { Cwmp_1_3, SessionTimeout } from '#protocol/cwmp-1-3'
import { Cwmp_1_4, SupportedCWMPVersions, UseCWMPVersion } from '#protocol/cwmp-1-4'
type NamespaceDescriptor = {
SoapEnc: string
+11 -11
View File
@@ -59,27 +59,27 @@ export {
// Types primitifs
type CommandKeyType,
type ObjectNameType,
} from './cwmp-1-0.js'
} from '#protocol/cwmp-1-0'
// ─── CWMP 1.1 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_1, EventCode_1_1, FaultCodeType_1_1 } from './cwmp-1-1.js'
export { Cwmp_1_1, EventCode_1_1, FaultCodeType_1_1 } from '#protocol/cwmp-1-1'
// ─── CWMP 1.2 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_2, EventCode_1_2 } from './cwmp-1-2.js'
export { Cwmp_1_2, EventCode_1_2 } from '#protocol/cwmp-1-2'
// ─── CWMP 1.3 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_3, SessionTimeout } from './cwmp-1-3.js'
export { Cwmp_1_3, SessionTimeout } from '#protocol/cwmp-1-3'
// ─── CWMP 1.4 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_4, EventCode_1_4, SupportedCWMPVersions, UseCWMPVersion } from './cwmp-1-4.js'
export { Cwmp_1_4, EventCode_1_4, SupportedCWMPVersions, UseCWMPVersion } from '#protocol/cwmp-1-4'
// ─── Types locaux complémentaires ─────────────────────────────────────────────
import type { Cwmp_1_0 } from './cwmp-1-0.js'
import type { Cwmp_1_1 } from './cwmp-1-1.js'
import type { Cwmp_1_2 } from './cwmp-1-2.js'
import type { Cwmp_1_3 } from './cwmp-1-3.js'
import type { Cwmp_1_4 } from './cwmp-1-4.js'
import type { Cwmp_1_0 } from '#protocol/cwmp-1-0'
import type { Cwmp_1_1 } from '#protocol/cwmp-1-1'
import type { Cwmp_1_2 } from '#protocol/cwmp-1-2'
import type { Cwmp_1_3 } from '#protocol/cwmp-1-3'
import type { Cwmp_1_4 } from '#protocol/cwmp-1-4'
import type {
Inform,
TransferComplete,
@@ -88,7 +88,7 @@ import type {
GetParameterAttributesResponse,
SetParameterValuesResponse,
Fault as SoapFaultBody,
} from './cwmp-1-0.js'
} from '#protocol/cwmp-1-0'
/** Union de tous les corps CWMP selon la version du protocole */
export type CwmpBody = Cwmp_1_0 | Cwmp_1_1 | Cwmp_1_2 | Cwmp_1_3 | Cwmp_1_4
+11 -5
View File
@@ -1,7 +1,7 @@
import CommandModel from '#models/command'
import { CommandRecord, CommandStatus, CommandType } from '../domain/commands/command.js'
import { ModeType } from '../domain/shared/mode.js'
import { Priority } from '../domain/commands/command.js'
import { type CommandRecord, CommandStatus, type CommandType } from '#domain/commands/command'
import { ModeType } from '#domain/shared/mode'
import { Priority } from '#domain/commands/command'
export default class CommandService {
async getCommands() {
@@ -32,7 +32,10 @@ export default class CommandService {
})
}
async getICommandsForDeviceWithStatus(deviceIdentifier: string, status: CommandStatus): Promise<CommandRecord[]> {
async getICommandsForDeviceWithStatus(
deviceIdentifier: string,
status: CommandStatus
): Promise<CommandRecord[]> {
const collection = CommandModel.getCollection()
return collection
.find({ deviceIdentifier, mode: ModeType.I, status })
@@ -40,7 +43,10 @@ export default class CommandService {
.toArray() as unknown as Promise<CommandRecord[]>
}
async getNICommandsForDeviceWithStatus(deviceIdentifier: string, status: CommandStatus): Promise<CommandRecord[]> {
async getNICommandsForDeviceWithStatus(
deviceIdentifier: string,
status: CommandStatus
): Promise<CommandRecord[]> {
const collection = CommandModel.getCollection()
return collection
.find({ deviceIdentifier, mode: ModeType.NI, status })
+33 -18
View File
@@ -1,12 +1,16 @@
import ConfigurationModel from '#models/configuration'
import DeviceModel from '#models/device'
import FirmwareCatalogService from './firmware_catalog_service.js'
import type { Configuration, ParameterCriteria, ParameterValueType } from '../domain/configuration/configuration.js'
import type { Device } from '../domain/device/device.js'
import type { MigrationStep } from '../domain/firmware/migration_step.js'
import { isWithinSchedule } from '../utils/schedule_window.js'
import { matchGlob } from '../utils/glob_match.js'
import { computeContentHash, parseProvisioningCode } from '../utils/provisioning_code.js'
import FirmwareCatalogService from '#services/firmware_catalog_service'
import type {
Configuration,
ParameterCriteria,
ParameterValueType,
} from '#domain/configuration/configuration'
import type { Device } from '#domain/device/device'
import type { MigrationStep } from '#domain/firmware/migration_step'
import { isWithinSchedule } from '#utils/schedule_window'
import { matchGlob } from '#utils/glob_match'
import { computeContentHash, parseProvisioningCode } from '#utils/provisioning_code'
export interface ComplianceDeviceResult {
identifier: string
@@ -140,7 +144,10 @@ export default class ConfigurationEngine {
skipFirmware = true
} else {
// Get current device version (from properties or parameter)
const currentVersion = this.getDeviceVersion(device, selectedConfig.firmwareTarget.productClass)
const currentVersion = this.getDeviceVersion(
device,
selectedConfig.firmwareTarget.productClass
)
if (currentVersion && currentVersion !== selectedConfig.firmwareTarget.targetVersion) {
firmwareMigrationPath = await this.firmwareService.resolveUpgradePath(
@@ -153,9 +160,7 @@ export default class ConfigurationEngine {
}
// Check if compliant (no diffs and no firmware needed)
const isCompliant =
parameterDiffs.length === 0 &&
firmwareMigrationPath.length === 0
const isCompliant = parameterDiffs.length === 0 && firmwareMigrationPath.length === 0
return {
configuration: selectedConfig,
@@ -369,17 +374,21 @@ export default class ConfigurationEngine {
// Compare values (loose: both as strings to handle type mismatches)
// If the desired type is numeric, compare numerically
const desiredType: string = desired.type || ''
const isNumeric = desiredType === 'unsignedInt' || desiredType === 'int' ||
desiredType === 'xsd:unsignedInt' || desiredType === 'xsd:int'
const isNumeric =
desiredType === 'unsignedInt' ||
desiredType === 'int' ||
desiredType === 'xsd:unsignedInt' ||
desiredType === 'xsd:int'
const isBool = desiredType === 'boolean' || desiredType === 'xsd:boolean'
let valuesMatch: boolean
if (isNumeric) {
const numCurrent = Number(currentValue)
const numDesired = Number(desired.value)
valuesMatch = (!isNaN(numCurrent) && !isNaN(numDesired))
? numCurrent === numDesired
: String(currentValue) === String(desired.value)
valuesMatch =
!Number.isNaN(numCurrent) && !Number.isNaN(numDesired)
? numCurrent === numDesired
: String(currentValue) === String(desired.value)
} else if (isBool) {
const toBool = (v: any) => v === true || v === 'true' || v === '1'
valuesMatch = toBool(currentValue) === toBool(desired.value)
@@ -404,7 +413,10 @@ export default class ConfigurationEngine {
* Get the current value of a parameter from the device.
* Returns null if not found.
*/
private getDeviceParameterValue(device: Device, paramName: string): string | number | boolean | null {
private getDeviceParameterValue(
device: Device,
paramName: string
): string | number | boolean | null {
// Try method-based access first (Device class instances)
if (typeof (device as any).getParameter === 'function') {
const param = (device as any).getParameter(paramName)
@@ -419,7 +431,10 @@ export default class ConfigurationEngine {
}
// Fallback: plain DeviceRecord objects (parameters + properties arrays)
const allEntries = [...((device as any).parameters ?? []), ...((device as any).properties ?? [])]
const allEntries = [
...((device as any).parameters ?? []),
...((device as any).properties ?? []),
]
const entry = allEntries.find((p: any) => p.name === paramName)
if (!entry || entry.value === undefined || entry.value === null) return null
if (typeof entry.value === 'boolean') return entry.value
+22 -14
View File
@@ -7,10 +7,10 @@ import {
type DesiredParameter,
type InventoryPath,
PolicyType,
} from '../domain/configuration/configuration.js'
import type { ConfigurationVersion } from '../domain/configuration/configuration_version.js'
import { isParameterCoveredByInventory, suggestInventoryPath } from '../utils/inventory_coverage.js'
import { matchGlob } from '../utils/glob_match.js'
} from '#domain/configuration/configuration'
import type { ConfigurationVersion } from '#domain/configuration/configuration_version'
import { isParameterCoveredByInventory, suggestInventoryPath } from '#utils/inventory_coverage'
import { matchGlob } from '#utils/glob_match'
/**
* Allowed criteria fields — DeviceId attributes (immutable) + parameterCriteria (evaluated dynamically).
@@ -82,7 +82,9 @@ export default class ConfigurationService {
/**
* Create a new Configuration after validation.
*/
async create(data: CreateConfigurationDto): Promise<Configuration & { warning?: OverlapWarning }> {
async create(
data: CreateConfigurationDto
): Promise<Configuration & { warning?: OverlapWarning }> {
this.validateIdentifier(data.identifier)
this.validateVersion(data.version)
this.validatePriority(data.priority)
@@ -186,7 +188,10 @@ export default class ConfigurationService {
* Creates a version snapshot of the current state before applying changes.
* Auto-increments the patch version on behavioral changes and resets device compliance.
*/
async update(identifier: string, data: UpdateConfigurationDto): Promise<Configuration & { warning?: OverlapWarning }> {
async update(
identifier: string,
data: UpdateConfigurationDto
): Promise<Configuration & { warning?: OverlapWarning }> {
const existing = await ConfigurationModel.findByIdentifier(identifier)
if (!existing) {
throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404)
@@ -243,7 +248,8 @@ export default class ConfigurationService {
if (data.tags !== undefined) updatePayload.tags = data.tags
if (data.criteria !== undefined) updatePayload.criteria = data.criteria
if (data.inventory !== undefined) updatePayload.inventory = data.inventory
if (data.desiredParameters !== undefined) updatePayload.desiredParameters = data.desiredParameters
if (data.desiredParameters !== undefined)
updatePayload.desiredParameters = data.desiredParameters
if (data.firmwareTarget !== undefined) updatePayload.firmwareTarget = data.firmwareTarget
if (data.schedule !== undefined) updatePayload.schedule = data.schedule
@@ -271,7 +277,13 @@ export default class ConfigurationService {
identifier: string,
page: number = 1,
limit: number = 25
): Promise<{ data: ConfigurationVersion[]; total: number; page: number; perPage: number; lastPage: number }> {
): Promise<{
data: ConfigurationVersion[]
total: number
page: number
perPage: number
lastPage: number
}> {
const existing = await ConfigurationModel.findByIdentifier(identifier)
if (!existing) {
throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404)
@@ -298,11 +310,7 @@ export default class ConfigurationService {
/**
* Add and/or remove tags from a Configuration without modifying other fields.
*/
async updateTags(
identifier: string,
add?: string[],
remove?: string[]
): Promise<Configuration> {
async updateTags(identifier: string, add?: string[], remove?: string[]): Promise<Configuration> {
const existing = await ConfigurationModel.findByIdentifier(identifier)
if (!existing) {
throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404)
@@ -538,7 +546,7 @@ export default class ConfigurationService {
private sanitizeCriteria(criteria: ConfigurationCriteria): ConfigurationCriteria {
if (!criteria.parameterCriteria?.length) return criteria
const filtered = criteria.parameterCriteria.filter(
(pc) => pc.name != null && String(pc.name).trim() !== ''
(pc) => pc.name !== null && pc.name !== undefined && String(pc.name).trim() !== ''
)
return {
...criteria,
+18 -6
View File
@@ -1,5 +1,5 @@
import { Device } from '../../domain/device/device.js'
import { DeviceActionExecutor } from './device_action_executor.js'
import { type Device } from '#domain/device/device'
import { type DeviceActionExecutor } from '#services/cwmp/device_action_executor'
import logger from '@adonisjs/core/services/logger'
/**
@@ -23,17 +23,29 @@ export async function executeAction(
device: Device,
actionType: ActionType,
parameters: any[],
callback: ((result: unknown) => Promise<string>) = () => Promise.resolve('')
callback: (result: unknown) => Promise<string> = () => Promise.resolve('')
): Promise<string> {
switch (actionType) {
case ActionType.SET_PARAMETER_VALUES:
return deviceAction.applySetParameterValues(device, parameters as any[], callback as (r: boolean) => Promise<string>)
return deviceAction.applySetParameterValues(
device,
parameters as any[],
callback as (r: boolean) => Promise<string>
)
case ActionType.SET_PARAMETER_ATTRIBUTES:
return deviceAction.applySetParameterAttributes(device, parameters as any[], callback as (r: boolean) => Promise<string>)
return deviceAction.applySetParameterAttributes(
device,
parameters as any[],
callback as (r: boolean) => Promise<string>
)
case ActionType.SCHEDULE_INFORM:
return deviceAction.applyScheduleInform(device, parameters[0] as any, callback as (r: boolean) => Promise<string>)
return deviceAction.applyScheduleInform(
device,
parameters[0] as any,
callback as (r: boolean) => Promise<string>
)
case ActionType.DOWNLOAD: {
const p = parameters[0]
@@ -1,15 +1,12 @@
import ConfigurationEngine from '../configuration_engine.js'
import type { EngineResult } from '../configuration_engine.js'
import type { DeviceStateResult, DeviceContext } from './main_state_machine.js'
import { PartialOperationalState } from './main_state_machine.js'
import ConfigurationEngine from '#services/configuration_engine'
import type { EngineResult } from '#services/configuration_engine'
import type { DeviceStateResult, DeviceContext } from '#services/cwmp/main_state_machine'
import { PartialOperationalState } from '#services/cwmp/main_state_machine'
import DeviceModel from '#models/device'
import type {
GetParameterNamesResponse,
GetParameterValuesResponse,
} from '../../protocol/tr069_types.js'
import type { GetParameterNamesResponse, GetParameterValuesResponse } from '#protocol/tr069_types'
import logger from '@adonisjs/core/services/logger'
import { ComplianceStatus } from '../../domain/configuration/compliance.js'
import { buildProvisioningCode } from '../../utils/provisioning_code.js'
import { ComplianceStatus } from '#domain/configuration/compliance'
import { buildProvisioningCode } from '#utils/provisioning_code'
/**
* ConfigurationEvaluationState
+6 -6
View File
@@ -1,8 +1,8 @@
import { type Device } from '../../domain/device/device.js'
import { DeviceParameter } from '../../domain/device/device_parameter.js'
import { ParameterType } from '../../domain/shared/types.js'
import { type CWMPTaskQueue, ResultStatus, type TaskResult } from './task_queue.js'
import DeviceModel from '../../models/device.js'
import { type Device } from '#domain/device/device'
import { DeviceParameter } from '#domain/device/device_parameter'
import { ParameterType } from '#domain/shared/types'
import { type CWMPTaskQueue, ResultStatus, type TaskResult } from '#services/cwmp/task_queue'
import DeviceModel from '#models/device'
import logger from '@adonisjs/core/services/logger'
import type {
GetParameterValuesResponse,
@@ -11,7 +11,7 @@ import type {
ParameterValueStruct,
ParameterAttributeStruct,
ParameterInfoStruct,
} from '../../protocol/tr069_types.js'
} from '#protocol/tr069_types'
// ─── Action parameter interfaces (exported pour usage externe) ─────────────────
@@ -1,12 +1,12 @@
import { type Device } from '../../domain/device/device.js'
import { type CWMPTaskQueue } from './task_queue.js'
import { DeviceActionExecutor } from './device_action_executor.js'
import { type Device } from '#domain/device/device'
import { type CWMPTaskQueue } from '#services/cwmp/task_queue'
import { DeviceActionExecutor } from '#services/cwmp/device_action_executor'
import {
ExecutingCommandsState,
type DeviceContext,
type DeviceStateResult,
} from './main_state_machine.js'
import type { CWMPTaskResultData } from '../../protocol/tr069_types.js'
} from '#services/cwmp/main_state_machine'
import type { CWMPTaskResultData } from '#protocol/tr069_types'
import logger from '@adonisjs/core/services/logger'
/**
+12 -12
View File
@@ -1,26 +1,26 @@
import { type Device } from '../../domain/device/device.js'
import { type CWMPTaskQueue } from './task_queue.js'
import { DeviceActionExecutor } from './device_action_executor.js'
import { getDeviceHashIdentifier } from '../../utils/device_utils.js'
import { CommandStatus, CommandType } from '../../domain/commands/command.js'
import DeviceModel from '../../models/device.js'
import CommandModel from '../../models/command.js'
import { ModeType } from '../../domain/shared/mode.js'
import { type Device } from '#domain/device/device'
import { type CWMPTaskQueue } from '#services/cwmp/task_queue'
import { DeviceActionExecutor } from '#services/cwmp/device_action_executor'
import { getDeviceHashIdentifier } from '#utils/device_utils'
import { CommandStatus, CommandType } from '#domain/commands/command'
import DeviceModel from '#models/device'
import CommandModel from '#models/command'
import { ModeType } from '#domain/shared/mode'
import logger from '@adonisjs/core/services/logger'
import { ConfigurationEvaluationState } from './configuration_evaluation_state.js'
import { reclassifyDeviceFromCatalog } from '../parameter_writability.js'
import { ConfigurationEvaluationState } from '#services/cwmp/configuration_evaluation_state'
import { reclassifyDeviceFromCatalog } from '#services/parameter_writability'
import type {
CWMPTaskResultData,
GetParameterNamesResponse,
GetParameterValuesResponse,
ParameterValueStruct,
} from '../../protocol/tr069_types.js'
} from '#protocol/tr069_types'
import type {
SetParameterAttribute,
DownloadParameter,
UploadParameter,
RebootParameter,
} from './device_action_executor.js'
} from '#services/cwmp/device_action_executor'
import { type ObjectId } from 'mongodb'
// ─── Interfaces ────────────────────────────────────────────────────────────────
+38 -16
View File
@@ -1,14 +1,14 @@
import { EventEmitter } from 'node:events'
import type { Device } from '../../domain/device/device.js'
import type { DeviceId } from '../../domain/device/device_id.js'
import { CWMPTaskQueue, ResultStatus } from './task_queue.js'
import { MainStateMachine } from './main_state_machine.js'
import { ImmediateCommandStateMachine } from './immediate_command_state_machine.js'
import { getDeviceHashIdentifier } from '../../utils/device_utils.js'
import { resolveHighestPriorityEvent, normalizeEventCode } from '../../utils/cwmp-events.js'
import { CommandStatus } from '../../domain/commands/command.js'
import { ModeType } from '../../domain/shared/mode.js'
import CommandModel from '../../models/command.js'
import type { Device } from '#domain/device/device'
import type { DeviceId } from '#domain/device/device_id'
import { CWMPTaskQueue, ResultStatus } from '#services/cwmp/task_queue'
import { MainStateMachine } from '#services/cwmp/main_state_machine'
import { ImmediateCommandStateMachine } from '#services/cwmp/immediate_command_state_machine'
import { getDeviceHashIdentifier } from '#utils/device_utils'
import { resolveHighestPriorityEvent, normalizeEventCode } from '#utils/cwmp-events'
import { CommandStatus } from '#domain/commands/command'
import { ModeType } from '#domain/shared/mode'
import CommandModel from '#models/command'
import logger from '@adonisjs/core/services/logger'
export enum MachineStatus {
@@ -129,7 +129,12 @@ export class StateMachineManager extends EventEmitter {
existingMachine.session = session
}
// Replace with MainStateMachine
const newMachine = new MainStateMachine(device, session, this.taskQueue, normalizedEvents)
const newMachine = new MainStateMachine(
device,
session,
this.taskQueue,
normalizedEvents
)
;(existingMachine as any).stateMachine = newMachine
existingMachine.events = evts
await newMachine.start()
@@ -148,7 +153,11 @@ export class StateMachineManager extends EventEmitter {
// Inform cycle which means the previous session is dead. Complete the old
// machine and create a fresh one for the new session.
logger.warn(
{ oldSession: existingSessionId, newSession: session, state: existingMachine.currentState },
{
oldSession: existingSessionId,
newSession: session,
state: existingMachine.currentState,
},
'[StateMachineManager] Replacing stale MainStateMachine from dropped session'
)
this.completeMachine(existingMachine)
@@ -179,7 +188,11 @@ export class StateMachineManager extends EventEmitter {
} else if (primaryEvent === 'CONNECTION REQUEST') {
// CONNECTION REQUEST only if no BOOTSTRAP/BOOT present
stateMachine = new ImmediateCommandStateMachine(device, session, this.taskQueue)
} else if (primaryEvent === 'PERIODIC' || primaryEvent === 'VALUE CHANGE' || primaryEvent === 'SCHEDULED') {
} else if (
primaryEvent === 'PERIODIC' ||
primaryEvent === 'VALUE CHANGE' ||
primaryEvent === 'SCHEDULED'
) {
// Lightweight MainStateMachine
stateMachine = new MainStateMachine(device, session, this.taskQueue, normalizedEvents)
} else if (primaryEvent === 'DIAGNOSTICS COMPLETE' || primaryEvent === 'TRANSFER COMPLETE') {
@@ -337,7 +350,8 @@ export class StateMachineManager extends EventEmitter {
// --- IP-only fallback (CPE reconnected on a new TCP port) ---
if (!deviceKey) {
const lastColon = remoteKey.lastIndexOf(':')
const remoteIp = lastColon > 0 ? remoteKey.slice(0, lastColon).replace(/^\[|\]$/g, '') : remoteKey
const remoteIp =
lastColon > 0 ? remoteKey.slice(0, lastColon).replace(/^\[|\]$/g, '') : remoteKey
const candidates: string[] = []
for (const [key, dkJson] of this.remoteToDevice.entries()) {
@@ -485,7 +499,11 @@ export class StateMachineManager extends EventEmitter {
/**
* Handle a CWMP response message
*/
async handleCWMPResponse(session: string, success: boolean, message: import('./../../protocol/tr069_types.js').CWMPTaskResultData): Promise<string> {
async handleCWMPResponse(
session: string,
success: boolean,
message: import('./../../protocol/tr069_types.js').CWMPTaskResultData
): Promise<string> {
const machineId = this.sessionMachines.get(session)
if (!machineId) return ''
@@ -545,7 +563,11 @@ export class StateMachineManager extends EventEmitter {
/**
* Enqueue an immediate task for a device (e.g., after Inform)
*/
async enqueueImmediate(device: Device, type: import('./../../protocol/tr069_types.js').CWMPTaskType, payload: import('./../../protocol/tr069_types.js').CWMPTaskPayload): Promise<void> {
async enqueueImmediate(
device: Device,
type: import('./../../protocol/tr069_types.js').CWMPTaskType,
payload: import('./../../protocol/tr069_types.js').CWMPTaskPayload
): Promise<void> {
this.taskQueue.addCWMPTask(device.deviceId, type, payload)
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { type DeviceId } from '../../domain/device/device_id.js'
import { type DeviceId } from '#domain/device/device_id'
import { EventEmitter } from 'node:events'
import { CWMPMessageBuilder } from '../../protocol/common/cwmp_message_builder.js'
import { CWMPMessageBuilder } from '#protocol/common/cwmp_message_builder'
import logger from '@adonisjs/core/services/logger'
import type {
CWMPTaskType,
@@ -12,7 +12,7 @@ import type {
DownloadTaskPayload,
UploadTaskPayload,
RebootTaskPayload,
} from '../../protocol/tr069_types.js'
} from '#protocol/tr069_types'
export class TaskResult {
status!: ResultStatus
+13 -6
View File
@@ -1,8 +1,8 @@
import DeviceModel from '#models/device'
import CommandModel from '#models/command'
import { type DeviceRecord } from '../domain/device/device_record.js'
import { CommandStatus, CommandType, Priority } from '../domain/commands/command.js'
import { ModeType } from '../domain/shared/mode.js'
import { type DeviceRecord } from '#domain/device/device_record'
import { CommandStatus, CommandType, Priority } from '#domain/commands/command'
import { ModeType } from '#domain/shared/mode'
import logger from '@adonisjs/core/services/logger'
export interface PaginationData {
@@ -163,8 +163,12 @@ export default class DeviceService {
// Fallback chain: ConnectionRequestUsername > ManagementServer.Username > OUI-ProductClass-Serial
const allParams = [...(device.properties || []), ...(device.parameters || [])]
const connReqUsername = allParams.find((p: any) => p.name?.includes('ConnectionRequestUsername'))
const connReqPassword = allParams.find((p: any) => p.name?.includes('ConnectionRequestPassword'))
const connReqUsername = allParams.find((p: any) =>
p.name?.includes('ConnectionRequestUsername')
)
const connReqPassword = allParams.find((p: any) =>
p.name?.includes('ConnectionRequestPassword')
)
const mgmtUsername = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Username'))
const mgmtPassword = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Password'))
@@ -211,7 +215,10 @@ export default class DeviceService {
)
continue // try next URL
}
logger.info({ status: response.status, url: url.toString() }, 'Connection Request successful')
logger.info(
{ status: response.status, url: url.toString() },
'Connection Request successful'
)
return true
} catch (error: any) {
logger.warn(
+8 -3
View File
@@ -1,4 +1,4 @@
import { Db } from 'mongodb'
import { type Db } from 'mongodb'
import { getMongoDb } from '#providers/mongodb_provider'
/**
@@ -72,7 +72,10 @@ export async function verifyCwmpExchangesIndexes(db?: Db): Promise<IndexVerifica
* Runs index verification at startup and logs warnings for missing indexes.
* Intended to be called after MongoDB provider has booted.
*/
export async function verifyIndexesAtStartup(db: Db, log: { warn: (msg: string) => void; info: (msg: string) => void }): Promise<void> {
export async function verifyIndexesAtStartup(
db: Db,
log: { warn: (msg: string) => void; info: (msg: string) => void }
): Promise<void> {
const result = await verifyCwmpExchangesIndexes(db)
if (result.allPresent) {
@@ -82,6 +85,8 @@ export async function verifyIndexesAtStartup(db: Db, log: { warn: (msg: string)
const missing = result.indexes.filter((idx) => !idx.exists)
for (const idx of missing) {
log.warn(`[ExchangeIndexCheck] Missing index on cwmp_exchanges: ${idx.name} (${idx.description})`)
log.warn(
`[ExchangeIndexCheck] Missing index on cwmp_exchanges: ${idx.name} (${idx.description})`
)
}
}
+2 -8
View File
@@ -53,10 +53,7 @@ export class ExchangeMetricsService {
this.maxSamples = maxSamples
}
static getInstance(
flushIntervalMs?: number,
maxSamples?: number
): ExchangeMetricsService {
static getInstance(flushIntervalMs?: number, maxSamples?: number): ExchangeMetricsService {
if (!ExchangeMetricsService.instance) {
ExchangeMetricsService.instance = new ExchangeMetricsService(flushIntervalMs, maxSamples)
}
@@ -161,10 +158,7 @@ export class ExchangeMetricsService {
Object.keys(metrics.apiResponseTimes).length > 0
if (hasData) {
logger.info(
{ metrics },
'[ExchangeMetrics] Aggregated performance metrics'
)
logger.info({ metrics }, '[ExchangeMetrics] Aggregated performance metrics')
}
// Reset samples
-1
View File
@@ -161,7 +161,6 @@ export class ExchangeStoreService {
* Internal persist implementation (called within the per-device queue).
*/
private async doPersistExchange(options: PersistOptions): Promise<CwmpExchange | null> {
try {
let rawXml = options.rawXml || ''
+8 -5
View File
@@ -1,9 +1,9 @@
import FirmwareEntryModel from '#models/firmware_entry'
import MigrationRouteModel from '#models/migration_route'
import { FirmwareEntry, MigrationRoute } from '../domain/firmware/firmware_catalog.js'
import { MigrationStep } from '../domain/firmware/migration_step.js'
import { matchGlob } from '../utils/glob_match.js'
import { BadRequestException } from '../exceptions/index.js'
import { type FirmwareEntry, type MigrationRoute } from '#domain/firmware/firmware_catalog'
import { type MigrationStep } from '#domain/firmware/migration_step'
import { matchGlob } from '#utils/glob_match'
import { BadRequestException } from '#exceptions/index'
export interface CreateFirmwareEntryDto {
identifier: string
@@ -125,7 +125,10 @@ export default class FirmwareCatalogService {
return MigrationRouteModel.find({}) as unknown as Promise<MigrationRoute[]>
}
async updateEntry(identifier: string, data: Partial<CreateFirmwareEntryDto>): Promise<FirmwareEntry> {
async updateEntry(
identifier: string,
data: Partial<CreateFirmwareEntryDto>
): Promise<FirmwareEntry> {
const collection = FirmwareEntryModel.getCollection()
const result = await collection.findOneAndUpdate(
{ identifier },
+31 -15
View File
@@ -1,8 +1,8 @@
import ConfigurationService from './configuration_service.js'
import FirmwareCatalogService from './firmware_catalog_service.js'
import type { CreateConfigurationDto } from './configuration_service.js'
import type { CreateFirmwareEntryDto } from './firmware_catalog_service.js'
import { Db } from 'mongodb'
import ConfigurationService from '#services/configuration_service'
import FirmwareCatalogService from '#services/firmware_catalog_service'
import type { CreateConfigurationDto } from '#services/configuration_service'
import type { CreateFirmwareEntryDto } from '#services/firmware_catalog_service'
import { type Db } from 'mongodb'
interface MigrationReport {
discoveryMigrated: number
@@ -23,7 +23,7 @@ export default class MigrationService {
constructor(
private db: Db,
private configService = new ConfigurationService(),
private firmwareCatalogService = new FirmwareCatalogService(),
private firmwareCatalogService = new FirmwareCatalogService()
) {}
/**
@@ -78,7 +78,9 @@ export default class MigrationService {
await this.extractFirmwareEntry(action, legacy)
report.firmwareEntriesCreated++
} catch (error) {
report.errors.push(`Firmware extraction for ${legacy.identifier}: ${(error as Error).message}`)
report.errors.push(
`Firmware extraction for ${legacy.identifier}: ${(error as Error).message}`
)
}
}
}
@@ -180,8 +182,16 @@ export default class MigrationService {
/**
* Build desired parameters from legacy Set Parameter actions
*/
private buildDesiredParametersFromLegacy(legacy: any): Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> {
const params: Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> = []
private buildDesiredParametersFromLegacy(legacy: any): Array<{
name: string
value: string | number | boolean
type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime'
}> {
const params: Array<{
name: string
value: string | number | boolean
type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime'
}> = []
if (!legacy.actions) return params
@@ -190,7 +200,12 @@ export default class MigrationService {
params.push({
name: action.parameter,
value: action.value,
type: typeof action.value === 'boolean' ? 'boolean' : typeof action.value === 'number' ? 'int' : 'string',
type:
typeof action.value === 'boolean'
? 'boolean'
: typeof action.value === 'number'
? 'int'
: 'string',
})
}
}
@@ -275,11 +290,12 @@ export default class MigrationService {
}
private buildLegacyProvisioningFromConfig(config: any, originalId: string): any {
const actions = config.desiredParameters?.map((param: any) => ({
type: 'SET_PARAMETER',
parameter: param.name,
value: param.value,
})) || []
const actions =
config.desiredParameters?.map((param: any) => ({
type: 'SET_PARAMETER',
parameter: param.name,
value: param.value,
})) || []
return {
identifier: originalId,
+19 -9
View File
@@ -1,18 +1,20 @@
import { createRequire } from 'node:module'
import type { DeviceSnapshot } from '../domain/device/device.js'
import type { Device } from '../domain/device/device.js'
import { DeviceParameter } from '../domain/device/device_parameter.js'
import type { DeviceSnapshot } from '#domain/device/device'
import type { Device } from '#domain/device/device'
import { DeviceParameter } from '#domain/device/device_parameter'
const require = createRequire(import.meta.url)
const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string; type?: string }> =
require('../../config/tr181-catalog.json')
const tr181Catalog: Array<{
path: string
writable: boolean
deviceClass: string
type?: string
}> = require('../../config/tr181-catalog.json')
// Index chargé une fois au démarrage — O(1) lookup
const catalogIndex = new Map(tr181Catalog.map((e) => [e.path, e.writable]))
const catalogTypeIndex = new Map(tr181Catalog.map((e) => [e.path, e.type || 'xsd:string']))
const catalogWritableIndex = new Set(
tr181Catalog.filter((e) => e.writable).map((e) => e.path)
)
const catalogWritableIndex = new Set(tr181Catalog.filter((e) => e.writable).map((e) => e.path))
/** Remplace les segments numériques par {i} pour matcher les entrées du catalogue */
export function normalizePath(path: string): string {
@@ -69,7 +71,15 @@ export function reclassifyDeviceFromCatalog(device: Device): number {
const snap = prop.toSnapshot()
const catalogType = catalogTypeIndex.get(normalized) || snap.type
device.setParameter(
new DeviceParameter(snap.name, snap.value, catalogType as any, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
new DeviceParameter(
snap.name,
snap.value,
catalogType as any,
snap.createdAt,
snap.updatedAt,
snap.notification,
snap.accessList
)
)
;(device as any)._properties.delete(prop.name)
reclassified++
+1 -1
View File
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { ReferencesConfig } from './types.js'
import type { ReferencesConfig } from '#services/references/types'
const CONFIG_PATH = join(process.cwd(), 'config', 'references.json')
@@ -1,12 +1,11 @@
import type { DeviceRecord } from '../../../domain/device/device_record.js'
import type { DeviceRecord } from '#domain/device/device_record'
import type {
ReferenceProvider,
ResolvedReference,
InformContext,
ProviderConfig,
} from '../types.js'
import { ReferenceEngine } from '../reference_engine.js'
} from '#services/references/types'
import { ReferenceEngine } from '#services/references/reference_engine'
/**
* Wassup Provider — resolves customer identity via the Wassup protocol.
@@ -33,13 +32,22 @@ export class WassupProvider implements ReferenceProvider {
this.timeout = config.timeout || 3000
this.resolveBy = config.resolveBy || 'ip'
this.service = config.options?.service || 'KRMSCO'
this.requestedProperties = config.options?.requestedProperties || ['sai', 'spr', 'clo', 'puid', 'utz']
this.requestedProperties = config.options?.requestedProperties || [
'sai',
'spr',
'clo',
'puid',
'utz',
]
}
async resolve(device: DeviceRecord, context: InformContext): Promise<ResolvedReference | null> {
const resolveValue = ReferenceEngine.extractResolveValue(device, this.resolveBy, context)
if (!resolveValue) {
console.debug({ provider: this.name, resolveBy: this.resolveBy }, '[Wassup] Cannot resolve: no value')
console.debug(
{ provider: this.name, resolveBy: this.resolveBy },
'[Wassup] Cannot resolve: no value'
)
return null
}
@@ -53,7 +61,7 @@ export class WassupProvider implements ReferenceProvider {
const response = await fetch(wassupUrl, {
method: 'GET',
headers: {
'X_WASSUP_USER_IP': resolveValue,
X_WASSUP_USER_IP: resolveValue,
},
signal: controller.signal,
})
@@ -99,13 +107,13 @@ export class WassupProvider implements ReferenceProvider {
// Map X_WASSUP_* keys to short names
const properties: Record<string, string> = {}
const keyMapping: Record<string, string> = {
'X_WASSUP_SAI': 'sai',
'X_WASSUP_SPR': 'spr',
'X_WASSUP_CLO': 'clo',
'X_WASSUP_PUID': 'puid',
'X_WASSUP_UTZ': 'utz',
'X_WASSUP_IUR': 'iur',
'X_WASSUP_IUS': 'ius',
X_WASSUP_SAI: 'sai',
X_WASSUP_SPR: 'spr',
X_WASSUP_CLO: 'clo',
X_WASSUP_PUID: 'puid',
X_WASSUP_UTZ: 'utz',
X_WASSUP_IUR: 'iur',
X_WASSUP_IUS: 'ius',
}
for (const [wassupKey, shortKey] of Object.entries(keyMapping)) {
@@ -114,7 +122,14 @@ export class WassupProvider implements ReferenceProvider {
}
}
console.info({ provider: this.name, resolveBy: `${this.resolveBy}:${resolveValue}`, keys: Object.keys(properties) }, '[Wassup] Resolved')
console.info(
{
provider: this.name,
resolveBy: `${this.resolveBy}:${resolveValue}`,
keys: Object.keys(properties),
},
'[Wassup] Resolved'
)
return {
provider: this.name,
@@ -125,7 +140,10 @@ export class WassupProvider implements ReferenceProvider {
}
} catch (error: any) {
const isTimeout = error.name === 'AbortError'
console.error({ provider: this.name, error: error.message, isTimeout }, '[Wassup] Resolution failed')
console.error(
{ provider: this.name, error: error.message, isTimeout },
'[Wassup] Resolution failed'
)
return {
provider: this.name,
+30 -15
View File
@@ -1,13 +1,13 @@
import type { DeviceRecord } from '../../domain/device/device_record.js'
import type { DeviceRecord } from '#domain/device/device_record'
import type {
ReferenceProvider,
ResolvedReference,
InformContext,
ProviderConfig,
} from './types.js'
import { TRIGGER_EVENT_MAP } from './types.js'
import { loadReferencesConfig } from './config_loader.js'
import { WassupProvider } from './providers/wassup_provider.js'
} from '#services/references/types'
import { TRIGGER_EVENT_MAP } from '#services/references/types'
import { loadReferencesConfig } from '#services/references/config_loader'
import { WassupProvider } from '#services/references/providers/wassup_provider'
export class ReferenceEngine {
private providers: ReferenceProvider[] = []
@@ -27,7 +27,10 @@ export class ReferenceEngine {
const provider = this.createProvider(providerConfig)
if (provider) {
this.providers.push(provider)
console.info({ name: providerConfig.name, type: providerConfig.type }, '[References] Provider registered')
console.info(
{ name: providerConfig.name, type: providerConfig.type },
'[References] Provider registered'
)
}
}
}
@@ -41,13 +44,19 @@ export class ReferenceEngine {
console.warn({ name: config.name }, '[References] http-json provider not yet implemented')
return null
case 'http-properties':
console.warn({ name: config.name }, '[References] http-properties provider not yet implemented')
console.warn(
{ name: config.name },
'[References] http-properties provider not yet implemented'
)
return null
case 'static':
console.warn({ name: config.name }, '[References] static provider not yet implemented')
return null
default:
console.error({ name: config.name, type: config.type }, '[References] Unknown provider type')
console.error(
{ name: config.name, type: config.type },
'[References] Unknown provider type'
)
return null
}
}
@@ -60,9 +69,7 @@ export class ReferenceEngine {
const applicable = this.getApplicableProviders(context.events)
if (applicable.length === 0) return []
const results = await Promise.allSettled(
applicable.map((p) => p.resolve(device, context))
)
const results = await Promise.allSettled(applicable.map((p) => p.resolve(device, context)))
const resolved: ResolvedReference[] = []
for (const result of results) {
@@ -79,7 +86,11 @@ export class ReferenceEngine {
/**
* Resolve a single provider by name.
*/
async resolveOne(providerName: string, device: DeviceRecord, context: InformContext): Promise<ResolvedReference | null> {
async resolveOne(
providerName: string,
device: DeviceRecord,
context: InformContext
): Promise<ResolvedReference | null> {
const provider = this.providers.find((p) => p.name === providerName)
if (!provider) {
console.warn({ providerName }, '[References] Provider not found')
@@ -112,7 +123,11 @@ export class ReferenceEngine {
/**
* Extracts the resolve value from a device based on the resolveBy config.
*/
static extractResolveValue(device: DeviceRecord, resolveBy: string, context: InformContext): string | null {
static extractResolveValue(
device: DeviceRecord,
resolveBy: string,
context: InformContext
): string | null {
switch (resolveBy) {
case 'ip':
return context.remoteIp || null
@@ -123,14 +138,14 @@ export class ReferenceEngine {
case 'mac': {
const allParams = [...(device.properties || []), ...(device.parameters || [])]
const macParam = allParams.find((p) => p.name?.includes('MACAddress'))
return macParam?.value as string || null
return (macParam?.value as string) || null
}
default:
if (resolveBy.startsWith('parameter:')) {
const path = resolveBy.slice('parameter:'.length)
const allParams = [...(device.properties || []), ...(device.parameters || [])]
const param = allParams.find((p) => p.name === path)
return param?.value as string || null
return (param?.value as string) || null
}
return null
}
+1 -1
View File
@@ -5,7 +5,7 @@
* from N independent providers (Wassup, CRM, NOC, etc.)
*/
import type { DeviceRecord } from '../../domain/device/device_record.js'
import type { DeviceRecord } from '#domain/device/device_record'
// ─── Provider Interface ────────────────────────────────────────────────────────
+9 -7
View File
@@ -12,13 +12,15 @@ export function initTelemetry() {
// OpenTelemetry setup is optional - only initialize when available
import('@opentelemetry/sdk-node')
.then(({ NodeSDK }) => {
return import('@opentelemetry/auto-instrumentations-node').then(({ getNodeAutoInstrumentations }) => {
const sdk = new NodeSDK({
serviceName: env.get('OTEL_SERVICE_NAME', 'papaye-v2'),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
})
return import('@opentelemetry/auto-instrumentations-node').then(
({ getNodeAutoInstrumentations }) => {
const sdk = new NodeSDK({
serviceName: env.get('OTEL_SERVICE_NAME', 'papaye-v2'),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
}
)
})
.catch(() => {
// OpenTelemetry not available, skip
+9 -2
View File
@@ -98,12 +98,19 @@ export class Tr069TraceService {
writeExchange(deviceId: string, direction: TraceDirection, sessionId: string, xml: string): void {
const tail = this.writeQueues.get(deviceId) ?? Promise.resolve()
const next = tail.then(() => this.doWrite(deviceId, direction, sessionId, xml)).catch(() => undefined)
const next = tail
.then(() => this.doWrite(deviceId, direction, sessionId, xml))
.catch(() => undefined)
this.writeQueues.set(deviceId, next)
}
private async doWrite(deviceId: string, direction: TraceDirection, sessionId: string, xml: string): Promise<void> {
private async doWrite(
deviceId: string,
direction: TraceDirection,
sessionId: string,
xml: string
): Promise<void> {
mkdirSync(LOG_DIR, { recursive: true })
const filePath = join(LOG_DIR, `${deviceId}.log`)
+20 -10
View File
@@ -31,16 +31,26 @@ export function normalizeEventCode(raw: string): string {
// Numeric-only → expand to standard label
switch (trimmed) {
case '0': return 'BOOTSTRAP'
case '1': return 'BOOT'
case '2': return 'PERIODIC'
case '3': return 'SCHEDULED'
case '4': return 'VALUE CHANGE'
case '5': return 'KICKED'
case '6': return 'CONNECTION REQUEST'
case '7': return 'TRANSFER COMPLETE'
case '8': return 'DIAGNOSTICS COMPLETE'
default: return trimmed
case '0':
return 'BOOTSTRAP'
case '1':
return 'BOOT'
case '2':
return 'PERIODIC'
case '3':
return 'SCHEDULED'
case '4':
return 'VALUE CHANGE'
case '5':
return 'KICKED'
case '6':
return 'CONNECTION REQUEST'
case '7':
return 'TRANSFER COMPLETE'
case '8':
return 'DIAGNOSTICS COMPLETE'
default:
return trimmed
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { InventoryPath } from '../domain/configuration/configuration.js'
import type { InventoryPath } from '#domain/configuration/configuration'
/**
* Checks whether a desired parameter is covered by at least one inventory path.
+7 -7
View File
@@ -11,17 +11,17 @@ export function computeNextPeriodicInform(
): Date | null {
if (!lastPeriodicInformAt) return null
const intervalSeconds = typeof periodicInformInterval === 'string'
? parseInt(periodicInformInterval, 10)
: periodicInformInterval
const intervalSeconds =
typeof periodicInformInterval === 'string'
? Number.parseInt(periodicInformInterval, 10)
: periodicInformInterval
if (!intervalSeconds || intervalSeconds <= 0) return null
const lastDate = lastPeriodicInformAt instanceof Date
? lastPeriodicInformAt
: new Date(lastPeriodicInformAt)
const lastDate =
lastPeriodicInformAt instanceof Date ? lastPeriodicInformAt : new Date(lastPeriodicInformAt)
if (isNaN(lastDate.getTime())) return null
if (Number.isNaN(lastDate.getTime())) return null
return new Date(lastDate.getTime() + intervalSeconds * 1000)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto'
import type { Configuration } from '../domain/configuration/configuration.js'
import type { Configuration } from '#domain/configuration/configuration'
/**
* Computes a 12-character hex content hash of a Configuration's desired state.
+2 -5
View File
@@ -1,4 +1,4 @@
import type { ScheduleWindow } from '../domain/configuration/configuration.js'
import type { ScheduleWindow } from '#domain/configuration/configuration'
/**
* Checks if the current time (or provided time) falls within the schedule window.
@@ -41,10 +41,7 @@ export function isWithinSchedule(schedule: ScheduleWindow | undefined, now?: Dat
/**
* Gets the local hour and day-of-week for a given Date in a specific IANA timezone.
*/
function getLocalTimeParts(
date: Date,
timezone: string
): { hour: number; dayOfWeek: number } {
function getLocalTimeParts(date: Date, timezone: string): { hour: number; dayOfWeek: number } {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
hour: 'numeric',
+13 -3
View File
@@ -37,9 +37,15 @@ const scheduleWindowSchema = vine.object({
export const createConfigurationValidator = vine.compile(
vine.object({
identifier: vine.string().trim().regex(/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/),
identifier: vine
.string()
.trim()
.regex(/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/),
name: vine.string().trim().minLength(1),
version: vine.string().trim().regex(/^\d+\.\d+\.\d+$/),
version: vine
.string()
.trim()
.regex(/^\d+\.\d+\.\d+$/),
priority: vine.number().positive(),
policy: vine.enum(['once', 'maintain']),
enabled: vine.boolean().optional(),
@@ -55,7 +61,11 @@ export const createConfigurationValidator = vine.compile(
export const updateConfigurationValidator = vine.compile(
vine.object({
name: vine.string().trim().minLength(1).optional(),
version: vine.string().trim().regex(/^\d+\.\d+\.\d+$/).optional(),
version: vine
.string()
.trim()
.regex(/^\d+\.\d+\.\d+$/)
.optional(),
priority: vine.number().positive().optional(),
policy: vine.enum(['once', 'maintain']).optional(),
enabled: vine.boolean().optional(),
+8 -2
View File
@@ -2,7 +2,10 @@ import vine from '@vinejs/vine'
export const createFirmwareEntryValidator = vine.compile(
vine.object({
identifier: vine.string().trim().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
identifier: vine
.string()
.trim()
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
productClass: vine.string().trim().minLength(1),
version: vine.string().trim().minLength(1),
downloadUrl: vine.string().trim().url(),
@@ -25,7 +28,10 @@ export const updateFirmwareEntryValidator = vine.compile(
export const createMigrationRouteValidator = vine.compile(
vine.object({
identifier: vine.string().trim().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
identifier: vine
.string()
.trim()
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
productClass: vine.string().trim().minLength(1),
sourceVersionPattern: vine.string().trim().minLength(1),
targetVersion: vine.string().trim().minLength(1),
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateDevicesCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateCommandsCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateDiscoveryConfigsCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateProvisioningConfigsCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateConfigurationsCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateConfigurationVersionsCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateFirmwareEntriesCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateMigrationRoutesCollection extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class AddComplianceToDevices extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class DropLegacyCollections extends MongoMigration {
async up(): Promise<void> {
@@ -1,4 +1,4 @@
import { MongoMigration } from '../mongo_migrator.js'
import { MongoMigration } from '#database/mongo_migrator'
export default class CreateCwmpExchangesCollection extends MongoMigration {
async up(): Promise<void> {
+4 -1
View File
@@ -41,7 +41,10 @@
"#database/*": "./database/*.js",
"#tests/*": "./tests/*.js",
"#start/*": "./start/*.js",
"#config/*": "./config/*.js"
"#config/*": "./config/*.js",
"#domain/*": "./app/domain/*.js",
"#protocol/*": "./app/protocol/*.js",
"#utils/*": "./app/utils/*.js"
},
"devDependencies": {
"@adonisjs/assembler": "^8.4.0",