feat(operations): add audited runtime catalog
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@multi-simadmin/contracts": "workspace:*",
|
"@multi-simadmin/contracts": "workspace:*",
|
||||||
|
"@multi-simadmin/operation-registry": "workspace:*",
|
||||||
"better-sqlite3": "12.11.1",
|
"better-sqlite3": "12.11.1",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
"fastify": "5.10.0"
|
"fastify": "5.10.0"
|
||||||
|
|||||||
@@ -0,0 +1,938 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"operationId": "deleteCallHistoryId",
|
||||||
|
"title": "Delete Call History ID",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.deleteCallHistoryId.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "deleteEsimProfilesIccid",
|
||||||
|
"title": "Delete eSIM Profile ICCID",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.deleteEsimProfilesIccid.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "deleteNotificationsQueueId",
|
||||||
|
"title": "Delete Notifications Queue ID",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.deleteNotificationsQueueId.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "deleteSmsConversationPhoneNumber",
|
||||||
|
"title": "Delete SMS Conversation Phone Number",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.deleteSmsConversationPhoneNumber.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "deleteSmsMessageId",
|
||||||
|
"title": "Delete SMS Message ID",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.deleteSmsMessageId.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getAirplaneMode",
|
||||||
|
"title": "Get Airplane Mode",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getAirplaneMode.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getApn",
|
||||||
|
"title": "Get APN",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getApn.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getAuthSettings",
|
||||||
|
"title": "Get Auth Settings",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getAuthSettings.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getAuthStatus",
|
||||||
|
"title": "Get Auth Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getAuthStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getAutomationConfig",
|
||||||
|
"title": "Get Automation Config",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getAutomationConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getAutomationLogs",
|
||||||
|
"title": "Get Automation Logs",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getAutomationLogs.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getBandLock",
|
||||||
|
"title": "Get Band Lock",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getBandLock.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getBasebandRestartStatus",
|
||||||
|
"title": "Get Baseband Restart Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getBasebandRestartStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCallForwarding",
|
||||||
|
"title": "Get Call Forwarding",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCallForwarding.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCallHistory",
|
||||||
|
"title": "Get Call History",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCallHistory.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCallSettings",
|
||||||
|
"title": "Get Call Settings",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCallSettings.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCallVolume",
|
||||||
|
"title": "Get Call Volume",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCallVolume.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCalls",
|
||||||
|
"title": "Get Calls",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCalls.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCellLock",
|
||||||
|
"title": "Get Cell Lock",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCellLock.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getCells",
|
||||||
|
"title": "Get Cells",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getCells.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getConnectivity",
|
||||||
|
"title": "Get Connectivity",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getConnectivity.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getData",
|
||||||
|
"title": "Get Data",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getData.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDevice",
|
||||||
|
"title": "Get Device",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDevice.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDeviceNetworkDdnsConfig",
|
||||||
|
"title": "Get Device Network DDNS Config",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDeviceNetworkDdnsConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDeviceNetworkDdnsLogs",
|
||||||
|
"title": "Get Device Network DDNS Logs",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDeviceNetworkDdnsLogs.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDeviceNetworkDdnsStatus",
|
||||||
|
"title": "Get Device Network DDNS Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDeviceNetworkDdnsStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDeviceNetworkWlanProfiles",
|
||||||
|
"title": "Get Device Network WLAN Profiles",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDeviceNetworkWlanProfiles.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getDeviceNetworkWlanStatus",
|
||||||
|
"title": "Get Device Network WLAN Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getDeviceNetworkWlanStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getEsimConfig",
|
||||||
|
"title": "Get eSIM Config",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getEsimConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getEsimEuicc",
|
||||||
|
"title": "Get eSIM eUICC",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getEsimEuicc.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getEsimLpacStatus",
|
||||||
|
"title": "Get eSIM LPAC Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getEsimLpacStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getEsimProfiles",
|
||||||
|
"title": "Get eSIM Profiles",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getEsimProfiles.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getHealth",
|
||||||
|
"title": "Get Health",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getHealth.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getImsStatus",
|
||||||
|
"title": "Get IMS Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getImsStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getLocationCellInfo",
|
||||||
|
"title": "Get Location Cell Info",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getLocationCellInfo.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetwork",
|
||||||
|
"title": "Get Network",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetwork.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetworkConnectionAddresses",
|
||||||
|
"title": "Get Network Connection Addresses",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetworkConnectionAddresses.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetworkInterfaces",
|
||||||
|
"title": "Get Network Interfaces",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetworkInterfaces.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetworkOperators",
|
||||||
|
"title": "Get Network Operators",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetworkOperators.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetworkOperatorsScan",
|
||||||
|
"title": "Scan Network Operators",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetworkOperatorsScan.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNetworkSignalStrength",
|
||||||
|
"title": "Get Network Signal Strength",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNetworkSignalStrength.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNotificationsConfig",
|
||||||
|
"title": "Get Notifications Config",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNotificationsConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNotificationsLogs",
|
||||||
|
"title": "Get Notifications Logs",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNotificationsLogs.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getNotificationsQueue",
|
||||||
|
"title": "Get Notifications Queue",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getNotificationsQueue.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getOtaStatus",
|
||||||
|
"title": "Get OTA Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getOtaStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getRadioMode",
|
||||||
|
"title": "Get Radio Mode",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getRadioMode.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getRoaming",
|
||||||
|
"title": "Get Roaming",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getRoaming.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getSim",
|
||||||
|
"title": "Get SIM",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getSim.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getSmsConversation",
|
||||||
|
"title": "Get SMS Conversation",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getSmsConversation.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getSmsList",
|
||||||
|
"title": "Get SMS List",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getSmsList.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getSmsStats",
|
||||||
|
"title": "Get SMS Stats",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getSmsStats.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getStats",
|
||||||
|
"title": "Get Stats",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getStats.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getStatsCpu",
|
||||||
|
"title": "Get Stats CPU",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getStatsCpu.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getVoicemailStatus",
|
||||||
|
"title": "Get Voicemail Status",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getVoicemailStatus.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "getWorkMode",
|
||||||
|
"title": "Get Work Mode",
|
||||||
|
"riskLevel": "R0",
|
||||||
|
"capability": "query",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.getWorkMode.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAirplaneMode",
|
||||||
|
"title": "Set Airplane Mode",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAirplaneMode.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postApn",
|
||||||
|
"title": "Set APN",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postApn.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAuthLogin",
|
||||||
|
"title": "Log In",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAuthLogin.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAuthLogout",
|
||||||
|
"title": "Log Out",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAuthLogout.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAuthPassword",
|
||||||
|
"title": "Change Auth Password",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAuthPassword.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAuthSettings",
|
||||||
|
"title": "Set Auth Settings",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAuthSettings.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAuthSetup",
|
||||||
|
"title": "Set Up Auth",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAuthSetup.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAutomationConfig",
|
||||||
|
"title": "Set Automation Config",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAutomationConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAutomationLogsClear",
|
||||||
|
"title": "Clear Automation Logs",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAutomationLogsClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postAutomationTestTaskId",
|
||||||
|
"title": "Test Automation Task ID",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postAutomationTestTaskId.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postBandLock",
|
||||||
|
"title": "Set Band Lock",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postBandLock.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postBasebandRestart",
|
||||||
|
"title": "Restart Baseband",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postBasebandRestart.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallAnswer",
|
||||||
|
"title": "Answer Call",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallAnswer.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallDial",
|
||||||
|
"title": "Dial Call",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallDial.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallForwarding",
|
||||||
|
"title": "Set Call Forwarding",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallForwarding.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallHangup",
|
||||||
|
"title": "Hang Up Call",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallHangup.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallHangupAll",
|
||||||
|
"title": "Hang Up All Calls",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallHangupAll.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallHistoryClear",
|
||||||
|
"title": "Clear Call History",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallHistoryClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallSettings",
|
||||||
|
"title": "Set Call Settings",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallSettings.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCallVolume",
|
||||||
|
"title": "Set Call Volume",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCallVolume.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCellLock",
|
||||||
|
"title": "Set Cell Lock",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCellLock.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCellLockUnlockAll",
|
||||||
|
"title": "Unlock All Cell Locks",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCellLockUnlockAll.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCellMonitorStart",
|
||||||
|
"title": "Start Cell Monitor",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCellMonitorStart.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postCellMonitorStop",
|
||||||
|
"title": "Stop Cell Monitor",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postCellMonitorStop.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postData",
|
||||||
|
"title": "Set Data",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postData.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkDdnsConfig",
|
||||||
|
"title": "Set Device Network DDNS Config",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkDdnsConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkDdnsLogsClear",
|
||||||
|
"title": "Clear Device Network DDNS Logs",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkDdnsLogsClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkDdnsSync",
|
||||||
|
"title": "Sync Device Network DDNS",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkDdnsSync.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanConnect",
|
||||||
|
"title": "Connect Device Network WLAN",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanConnect.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanDisconnect",
|
||||||
|
"title": "Disconnect Device Network WLAN",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanDisconnect.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanEnabled",
|
||||||
|
"title": "Set Device Network WLAN Enabled",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanEnabled.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanForget",
|
||||||
|
"title": "Forget Device Network WLAN",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanForget.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanProfile",
|
||||||
|
"title": "Set Device Network WLAN Profile",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanProfile.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postDeviceNetworkWlanScan",
|
||||||
|
"title": "Scan Device Network WLAN",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postDeviceNetworkWlanScan.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postEsimConfig",
|
||||||
|
"title": "Set eSIM Config",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postEsimConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postEsimLpacRepair",
|
||||||
|
"title": "Repair eSIM LPAC",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postEsimLpacRepair.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postEsimProfiles",
|
||||||
|
"title": "Add eSIM Profile",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postEsimProfiles.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postEsimProfilesIccidEnable",
|
||||||
|
"title": "Enable eSIM Profile ICCID",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postEsimProfilesIccidEnable.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postEsimProfilesIccidRename",
|
||||||
|
"title": "Rename eSIM Profile ICCID",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postEsimProfilesIccidRename.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNetworkRegisterAuto",
|
||||||
|
"title": "Register Network Automatically",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNetworkRegisterAuto.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNetworkRegisterManual",
|
||||||
|
"title": "Register Network Manually",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNetworkRegisterManual.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsConfig",
|
||||||
|
"title": "Set Notifications Config",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsConfig.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsLogsClear",
|
||||||
|
"title": "Clear Notifications Logs",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsLogsClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsQueueClear",
|
||||||
|
"title": "Clear Notifications Queue",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsQueueClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsQueueIdRetry",
|
||||||
|
"title": "Retry Notifications Queue ID",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsQueueIdRetry.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsQueueRetryAll",
|
||||||
|
"title": "Retry All Notifications Queue Items",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsQueueRetryAll.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postNotificationsTestChannel",
|
||||||
|
"title": "Test Notifications Channel",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postNotificationsTestChannel.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postOtaApply",
|
||||||
|
"title": "Apply OTA",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postOtaApply.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postOtaCancel",
|
||||||
|
"title": "Cancel OTA",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postOtaCancel.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postOtaLatestRelease",
|
||||||
|
"title": "Get OTA Latest Release",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postOtaLatestRelease.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postOtaOnlinePrepare",
|
||||||
|
"title": "Prepare OTA Online",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postOtaOnlinePrepare.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postOtaUpload",
|
||||||
|
"title": "Upload OTA",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postOtaUpload.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postRadioMode",
|
||||||
|
"title": "Set Radio Mode",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postRadioMode.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postRoaming",
|
||||||
|
"title": "Set Roaming",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postRoaming.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postServiceRestart",
|
||||||
|
"title": "Restart Service",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postServiceRestart.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSimCache",
|
||||||
|
"title": "Run SIM Cache",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSimCache.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSimDetailsRefresh",
|
||||||
|
"title": "Run SIM Details Refresh",
|
||||||
|
"riskLevel": "R1",
|
||||||
|
"capability": "command",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSimDetailsRefresh.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSmsBatchDelete",
|
||||||
|
"title": "Delete SMS Batch",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": true,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSmsBatchDelete.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSmsClear",
|
||||||
|
"title": "Clear SMS",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSmsClear.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSmsSend",
|
||||||
|
"title": "Send SMS",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSmsSend.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postSystemReboot",
|
||||||
|
"title": "Reboot System",
|
||||||
|
"riskLevel": "R3",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postSystemReboot.parameters.v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationId": "postWorkMode",
|
||||||
|
"title": "Set Work Mode",
|
||||||
|
"riskLevel": "R2",
|
||||||
|
"capability": "job",
|
||||||
|
"batchable": false,
|
||||||
|
"parameterSchemaId": "simadmin.58e2204.postWorkMode.parameters.v1"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import catalog from './operation-catalog-58e2204.json' with { type: 'json' };
|
||||||
|
import type { OperationCatalogRegistry, OperationCatalogRow } from './operation-catalog-service.js';
|
||||||
|
|
||||||
|
/** Generated safe six-field projection of the audited 58e2204 registry. */
|
||||||
|
export const operationCatalogRegistry: OperationCatalogRegistry = Object.freeze({
|
||||||
|
listOperations: (): readonly OperationCatalogRow[] => catalog as readonly OperationCatalogRow[],
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { operationCatalogRegistry } from './operation-catalog-data.js';
|
||||||
|
import { OperationCatalogService } from './operation-catalog-service.js';
|
||||||
|
|
||||||
|
const service = new OperationCatalogService(operationCatalogRegistry);
|
||||||
|
|
||||||
|
describe('OperationCatalogService', () => {
|
||||||
|
it('returns a frozen contract-safe default page with six fields only', () => {
|
||||||
|
const page = service.list({});
|
||||||
|
expect(page.page).toEqual({ page: 1, pageSize: 25, total: 117 });
|
||||||
|
expect(page.items).toHaveLength(25);
|
||||||
|
expect(Object.keys(page.items[0]!)).toEqual([
|
||||||
|
'operationId',
|
||||||
|
'title',
|
||||||
|
'risk',
|
||||||
|
'capability',
|
||||||
|
'batchable',
|
||||||
|
'parameterSchemaId',
|
||||||
|
]);
|
||||||
|
expect(page.items.map(({ operationId }) => operationId)).toEqual(
|
||||||
|
[...page.items.map(({ operationId }) => operationId)].sort(),
|
||||||
|
);
|
||||||
|
expect(Object.isFrozen(page)).toBe(true);
|
||||||
|
expect(Object.isFrozen(page.items)).toBe(true);
|
||||||
|
expect(page.items.every(Object.isFrozen)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters all supported fields and searches case-insensitively', () => {
|
||||||
|
const filtered = service.list({
|
||||||
|
risk: 'R0',
|
||||||
|
capability: 'query',
|
||||||
|
batchable: false,
|
||||||
|
search: 'sms',
|
||||||
|
});
|
||||||
|
expect(filtered.items.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
filtered.items.every(
|
||||||
|
(item) =>
|
||||||
|
item.risk === 'R0' &&
|
||||||
|
item.capability === 'query' &&
|
||||||
|
!item.batchable &&
|
||||||
|
/sms/i.test(`${item.operationId} ${item.title}`),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps operationId ascending as the final tie breaker when descending', () => {
|
||||||
|
for (const sort of ['operationId', 'risk', 'capability'] as const) {
|
||||||
|
const page = service.list({ pageSize: 100, sort, direction: 'desc' });
|
||||||
|
for (let index = 1; index < page.items.length; index += 1) {
|
||||||
|
const previous = page.items[index - 1]!;
|
||||||
|
const current = page.items[index]!;
|
||||||
|
if (previous[sort] === current[sort])
|
||||||
|
expect(previous.operationId < current.operationId).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty beyond-last page without changing total', () => {
|
||||||
|
const page = service.list({ page: 999 });
|
||||||
|
expect(page.items).toEqual([]);
|
||||||
|
expect(page.page.total).toBe(117);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type {
|
||||||
|
OperationCatalogEntry,
|
||||||
|
OperationPage,
|
||||||
|
OperationPageQuery,
|
||||||
|
} from '@multi-simadmin/contracts';
|
||||||
|
|
||||||
|
export interface OperationCatalogRow {
|
||||||
|
readonly operationId: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly riskLevel: OperationCatalogEntry['risk'];
|
||||||
|
readonly capability: string;
|
||||||
|
readonly batchable: boolean;
|
||||||
|
readonly parameterSchemaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationCatalogRegistry {
|
||||||
|
listOperations(): readonly OperationCatalogRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const freeze = <T>(value: T): T => Object.freeze(value);
|
||||||
|
const compare = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
||||||
|
|
||||||
|
/** Pure safe projection over the audited runtime registry. */
|
||||||
|
export class OperationCatalogService {
|
||||||
|
constructor(private readonly registry: OperationCatalogRegistry) {}
|
||||||
|
|
||||||
|
list(query: OperationPageQuery): OperationPage {
|
||||||
|
const page = query.page ?? 1;
|
||||||
|
const pageSize = query.pageSize ?? 25;
|
||||||
|
const search = query.search?.toLowerCase();
|
||||||
|
const rows: OperationCatalogEntry[] = [];
|
||||||
|
|
||||||
|
for (const row of this.registry.listOperations()) {
|
||||||
|
if (query.risk !== undefined && row.riskLevel !== query.risk) continue;
|
||||||
|
if (query.capability !== undefined && row.capability !== query.capability) continue;
|
||||||
|
if (query.batchable !== undefined && row.batchable !== query.batchable) continue;
|
||||||
|
if (
|
||||||
|
search !== undefined &&
|
||||||
|
!row.operationId.toLowerCase().includes(search) &&
|
||||||
|
!row.title.toLowerCase().includes(search)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.push(
|
||||||
|
freeze({
|
||||||
|
operationId: row.operationId,
|
||||||
|
title: row.title,
|
||||||
|
risk: row.riskLevel,
|
||||||
|
capability: row.capability,
|
||||||
|
batchable: row.batchable,
|
||||||
|
parameterSchemaId: row.parameterSchemaId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = query.sort ?? 'operationId';
|
||||||
|
const direction = query.direction === 'desc' ? -1 : 1;
|
||||||
|
const value = (row: OperationCatalogEntry): string => {
|
||||||
|
if (sort === 'risk') return row.risk;
|
||||||
|
if (sort === 'capability') return row.capability;
|
||||||
|
return row.operationId;
|
||||||
|
};
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
const byField = compare(value(left), value(right));
|
||||||
|
return byField === 0 ? compare(left.operationId, right.operationId) : direction * byField;
|
||||||
|
});
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
return freeze({
|
||||||
|
items: freeze(rows.slice(offset, offset + pageSize)),
|
||||||
|
page: freeze({ page, pageSize, total: rows.length }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { expect, it } from 'vitest';
|
||||||
|
import { buildControlPlaneApp } from './control-plane.js';
|
||||||
|
import { migrateDatabase } from './infrastructure/database/migrations.js';
|
||||||
|
|
||||||
|
it('registers the read-only operations catalog without touching upstream', async () => {
|
||||||
|
const db = new Database(':memory:');
|
||||||
|
migrateDatabase(db);
|
||||||
|
let upstreamCalls = 0;
|
||||||
|
const app = buildControlPlaneApp({
|
||||||
|
db,
|
||||||
|
store: { set: async () => '', get: async () => undefined, delete: async () => false },
|
||||||
|
upstream: {
|
||||||
|
get: async () => {
|
||||||
|
upstreamCalls += 1;
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
request: async () => {
|
||||||
|
upstreamCalls += 1;
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const response = await app.inject('/api/v1/operations?pageSize=1');
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.json().page.total).toBe(117);
|
||||||
|
expect(upstreamCalls).toBe(0);
|
||||||
|
await app.close();
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
|
import { registerOperationRoutes } from './interface/http/operation-routes.js';
|
||||||
|
import { operationCatalogRegistry } from './application/operations/operation-catalog-data.js';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import { buildApp, type BuildAppOptions } from './app.js';
|
import { buildApp, type BuildAppOptions } from './app.js';
|
||||||
import {
|
import {
|
||||||
@@ -54,8 +56,10 @@ export function buildControlPlaneApp(options: ControlPlaneOptions): ControlPlane
|
|||||||
deletion.reconcileInterruptedJobs();
|
deletion.reconcileInterruptedJobs();
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
...options.app,
|
...options.app,
|
||||||
registerRoutes: (app) =>
|
registerRoutes: (app) => {
|
||||||
registerInstanceRoutes(app, { instances, connections, login, deletion }),
|
registerInstanceRoutes(app, { instances, connections, login, deletion });
|
||||||
|
registerOperationRoutes(app, operationCatalogRegistry);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
Object.assign(app, {
|
Object.assign(app, {
|
||||||
retryPendingSecretCleanup: () => instances.retryPendingSecretCleanup(),
|
retryPendingSecretCleanup: () => instances.retryPendingSecretCleanup(),
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import { buildApp } from '../../app.js';
|
||||||
|
import { operationCatalogRegistry } from '../../application/operations/operation-catalog-data.js';
|
||||||
|
import { registerOperationRoutes } from './operation-routes.js';
|
||||||
|
|
||||||
|
const apps: ReturnType<typeof buildApp>[] = [];
|
||||||
|
afterEach(async () => Promise.all(apps.splice(0).map((app) => app.close())));
|
||||||
|
const app = () => {
|
||||||
|
const instance = buildApp({
|
||||||
|
registerRoutes: (server) => registerOperationRoutes(server, operationCatalogRegistry),
|
||||||
|
});
|
||||||
|
apps.push(instance);
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('GET /api/v1/operations', () => {
|
||||||
|
it('returns only the safe catalog projection', async () => {
|
||||||
|
const response = await app().inject('/api/v1/operations?pageSize=1');
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.json()).toMatchObject({ page: { page: 1, pageSize: 1, total: 117 } });
|
||||||
|
expect(Object.keys(response.json().items[0])).toEqual([
|
||||||
|
'operationId',
|
||||||
|
'title',
|
||||||
|
'risk',
|
||||||
|
'capability',
|
||||||
|
'batchable',
|
||||||
|
'parameterSchemaId',
|
||||||
|
]);
|
||||||
|
for (const forbidden of [
|
||||||
|
'method',
|
||||||
|
'pathTemplate',
|
||||||
|
'sourceEvidence',
|
||||||
|
'sensitiveFields',
|
||||||
|
'handler',
|
||||||
|
])
|
||||||
|
expect(response.body).not.toContain(`\"${forbidden}\"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'unknown=x',
|
||||||
|
'page=1&page=2',
|
||||||
|
'page[]=1',
|
||||||
|
'page=0',
|
||||||
|
'page=01',
|
||||||
|
'page=1.5',
|
||||||
|
'pageSize=101',
|
||||||
|
'risk=R4',
|
||||||
|
'capability=bogus',
|
||||||
|
'batchable=True',
|
||||||
|
'batchable=1',
|
||||||
|
'sort=title',
|
||||||
|
'direction=down',
|
||||||
|
`search=${'x'.repeat(201)}`,
|
||||||
|
])('rejects invalid strict scalar query: %s', async (query) => {
|
||||||
|
const response = await app().inject(`/api/v1/operations?${query}`);
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.headers['content-type']).toContain('application/problem+json');
|
||||||
|
expect(response.json()).toMatchObject({ code: 'VALIDATION_FAILED', status: 400 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts every frozen capability including job', async () => {
|
||||||
|
const response = await app().inject('/api/v1/operations?capability=job&pageSize=100');
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.json().items.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
response.json().items.every((item: { capability: string }) => item.capability === 'job'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type { OperationPageQuery } from '@multi-simadmin/contracts';
|
||||||
|
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
||||||
|
import { OperationCatalogService } from '../../application/operations/operation-catalog-service.js';
|
||||||
|
import type { OperationCatalogRegistry } from '../../application/operations/operation-catalog-service.js';
|
||||||
|
|
||||||
|
const capabilities = new Set(['query', 'command', 'job']);
|
||||||
|
const keys = new Set([
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'sort',
|
||||||
|
'direction',
|
||||||
|
'risk',
|
||||||
|
'capability',
|
||||||
|
'batchable',
|
||||||
|
'search',
|
||||||
|
]);
|
||||||
|
class CatalogValidationError extends Error {}
|
||||||
|
|
||||||
|
function scalarQuery(request: FastifyRequest): Record<string, string> {
|
||||||
|
const raw = request.query;
|
||||||
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
||||||
|
throw new CatalogValidationError();
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(raw)) {
|
||||||
|
if (!keys.has(key) || typeof value !== 'string') throw new CatalogValidationError();
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQuery(request: FastifyRequest): OperationPageQuery {
|
||||||
|
const query = scalarQuery(request);
|
||||||
|
const result: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sort?: 'operationId' | 'risk' | 'capability';
|
||||||
|
direction?: 'asc' | 'desc';
|
||||||
|
risk?: 'R0' | 'R1' | 'R2' | 'R3';
|
||||||
|
capability?: string;
|
||||||
|
batchable?: boolean;
|
||||||
|
search?: string;
|
||||||
|
} = {};
|
||||||
|
const integer = (key: 'page' | 'pageSize', maximum?: number): void => {
|
||||||
|
const value = query[key];
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (!/^[1-9]\d*$/.test(value)) throw new CatalogValidationError();
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isSafeInteger(parsed) || (maximum !== undefined && parsed > maximum))
|
||||||
|
throw new CatalogValidationError();
|
||||||
|
result[key] = parsed;
|
||||||
|
};
|
||||||
|
integer('page');
|
||||||
|
integer('pageSize', 100);
|
||||||
|
const sort = query.sort;
|
||||||
|
if (sort !== undefined) {
|
||||||
|
if (sort !== 'operationId' && sort !== 'risk' && sort !== 'capability')
|
||||||
|
throw new CatalogValidationError();
|
||||||
|
result.sort = sort;
|
||||||
|
}
|
||||||
|
const direction = query.direction;
|
||||||
|
if (direction !== undefined) {
|
||||||
|
if (direction !== 'asc' && direction !== 'desc') throw new CatalogValidationError();
|
||||||
|
result.direction = direction;
|
||||||
|
}
|
||||||
|
const risk = query.risk;
|
||||||
|
if (risk !== undefined) {
|
||||||
|
if (risk !== 'R0' && risk !== 'R1' && risk !== 'R2' && risk !== 'R3')
|
||||||
|
throw new CatalogValidationError();
|
||||||
|
result.risk = risk;
|
||||||
|
}
|
||||||
|
const capability = query.capability;
|
||||||
|
if (capability !== undefined) {
|
||||||
|
if (!capabilities.has(capability)) throw new CatalogValidationError();
|
||||||
|
result.capability = capability;
|
||||||
|
}
|
||||||
|
const batchable = query.batchable;
|
||||||
|
if (batchable !== undefined) {
|
||||||
|
if (batchable !== 'true' && batchable !== 'false') throw new CatalogValidationError();
|
||||||
|
result.batchable = batchable === 'true';
|
||||||
|
}
|
||||||
|
const search = query.search;
|
||||||
|
if (search !== undefined) {
|
||||||
|
if (search.length > 200) throw new CatalogValidationError();
|
||||||
|
result.search = search;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerOperationRoutes(
|
||||||
|
app: FastifyInstance,
|
||||||
|
registry: OperationCatalogRegistry,
|
||||||
|
): void {
|
||||||
|
const service = new OperationCatalogService(registry);
|
||||||
|
app.get('/api/v1/operations', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return service.list(parseQuery(request));
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof CatalogValidationError)) throw error;
|
||||||
|
return reply.code(400).type('application/problem+json').send({
|
||||||
|
type: 'about:blank',
|
||||||
|
title: 'Bad Request',
|
||||||
|
status: 400,
|
||||||
|
code: 'VALIDATION_FAILED',
|
||||||
|
detail: 'The request did not satisfy the endpoint schema.',
|
||||||
|
requestId: request.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
export interface OperationCatalogMetadata {
|
||||||
|
readonly title: string;
|
||||||
|
readonly parameterSchemaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explicit reviewed titles keyed by every operation in the audited 58e2204 registry. */
|
||||||
|
const operationTitles58e2204: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
getDevice: 'Get Device',
|
||||||
|
getSim: 'Get SIM',
|
||||||
|
postSimDetailsRefresh: 'Run SIM Details Refresh',
|
||||||
|
postSimCache: 'Run SIM Cache',
|
||||||
|
getNetwork: 'Get Network',
|
||||||
|
getCells: 'Get Cells',
|
||||||
|
postCellMonitorStart: 'Start Cell Monitor',
|
||||||
|
postCellMonitorStop: 'Stop Cell Monitor',
|
||||||
|
getRadioMode: 'Get Radio Mode',
|
||||||
|
postRadioMode: 'Set Radio Mode',
|
||||||
|
getBandLock: 'Get Band Lock',
|
||||||
|
postBandLock: 'Set Band Lock',
|
||||||
|
getNetworkInterfaces: 'Get Network Interfaces',
|
||||||
|
getNetworkConnectionAddresses: 'Get Network Connection Addresses',
|
||||||
|
getDeviceNetworkDdnsConfig: 'Get Device Network DDNS Config',
|
||||||
|
postDeviceNetworkDdnsConfig: 'Set Device Network DDNS Config',
|
||||||
|
getDeviceNetworkDdnsStatus: 'Get Device Network DDNS Status',
|
||||||
|
postDeviceNetworkDdnsSync: 'Sync Device Network DDNS',
|
||||||
|
getDeviceNetworkDdnsLogs: 'Get Device Network DDNS Logs',
|
||||||
|
postDeviceNetworkDdnsLogsClear: 'Clear Device Network DDNS Logs',
|
||||||
|
getDeviceNetworkWlanStatus: 'Get Device Network WLAN Status',
|
||||||
|
postDeviceNetworkWlanEnabled: 'Set Device Network WLAN Enabled',
|
||||||
|
postDeviceNetworkWlanScan: 'Scan Device Network WLAN',
|
||||||
|
getDeviceNetworkWlanProfiles: 'Get Device Network WLAN Profiles',
|
||||||
|
postDeviceNetworkWlanForget: 'Forget Device Network WLAN',
|
||||||
|
postDeviceNetworkWlanConnect: 'Connect Device Network WLAN',
|
||||||
|
postDeviceNetworkWlanDisconnect: 'Disconnect Device Network WLAN',
|
||||||
|
postDeviceNetworkWlanProfile: 'Set Device Network WLAN Profile',
|
||||||
|
getNetworkSignalStrength: 'Get Network Signal Strength',
|
||||||
|
getLocationCellInfo: 'Get Location Cell Info',
|
||||||
|
getNetworkOperators: 'Get Network Operators',
|
||||||
|
getNetworkOperatorsScan: 'Scan Network Operators',
|
||||||
|
postNetworkRegisterManual: 'Register Network Manually',
|
||||||
|
postNetworkRegisterAuto: 'Register Network Automatically',
|
||||||
|
getApn: 'Get APN',
|
||||||
|
postApn: 'Set APN',
|
||||||
|
getCellLock: 'Get Cell Lock',
|
||||||
|
postCellLock: 'Set Cell Lock',
|
||||||
|
postCellLockUnlockAll: 'Unlock All Cell Locks',
|
||||||
|
getData: 'Get Data',
|
||||||
|
postData: 'Set Data',
|
||||||
|
getRoaming: 'Get Roaming',
|
||||||
|
postRoaming: 'Set Roaming',
|
||||||
|
getAirplaneMode: 'Get Airplane Mode',
|
||||||
|
postAirplaneMode: 'Set Airplane Mode',
|
||||||
|
postBasebandRestart: 'Restart Baseband',
|
||||||
|
getBasebandRestartStatus: 'Get Baseband Restart Status',
|
||||||
|
getWorkMode: 'Get Work Mode',
|
||||||
|
postWorkMode: 'Set Work Mode',
|
||||||
|
getEsimConfig: 'Get eSIM Config',
|
||||||
|
postEsimConfig: 'Set eSIM Config',
|
||||||
|
getEsimLpacStatus: 'Get eSIM LPAC Status',
|
||||||
|
postEsimLpacRepair: 'Repair eSIM LPAC',
|
||||||
|
getEsimEuicc: 'Get eSIM eUICC',
|
||||||
|
getEsimProfiles: 'Get eSIM Profiles',
|
||||||
|
postEsimProfiles: 'Add eSIM Profile',
|
||||||
|
postEsimProfilesIccidEnable: 'Enable eSIM Profile ICCID',
|
||||||
|
postEsimProfilesIccidRename: 'Rename eSIM Profile ICCID',
|
||||||
|
deleteEsimProfilesIccid: 'Delete eSIM Profile ICCID',
|
||||||
|
getCalls: 'Get Calls',
|
||||||
|
postCallDial: 'Dial Call',
|
||||||
|
postCallHangup: 'Hang Up Call',
|
||||||
|
postCallHangupAll: 'Hang Up All Calls',
|
||||||
|
postCallAnswer: 'Answer Call',
|
||||||
|
getCallVolume: 'Get Call Volume',
|
||||||
|
postCallVolume: 'Set Call Volume',
|
||||||
|
getCallForwarding: 'Get Call Forwarding',
|
||||||
|
postCallForwarding: 'Set Call Forwarding',
|
||||||
|
getCallSettings: 'Get Call Settings',
|
||||||
|
postCallSettings: 'Set Call Settings',
|
||||||
|
getCallHistory: 'Get Call History',
|
||||||
|
deleteCallHistoryId: 'Delete Call History ID',
|
||||||
|
postCallHistoryClear: 'Clear Call History',
|
||||||
|
getImsStatus: 'Get IMS Status',
|
||||||
|
getVoicemailStatus: 'Get Voicemail Status',
|
||||||
|
postSmsSend: 'Send SMS',
|
||||||
|
getSmsList: 'Get SMS List',
|
||||||
|
getSmsConversation: 'Get SMS Conversation',
|
||||||
|
getSmsStats: 'Get SMS Stats',
|
||||||
|
postSmsBatchDelete: 'Delete SMS Batch',
|
||||||
|
deleteSmsConversationPhoneNumber: 'Delete SMS Conversation Phone Number',
|
||||||
|
deleteSmsMessageId: 'Delete SMS Message ID',
|
||||||
|
postSmsClear: 'Clear SMS',
|
||||||
|
getStats: 'Get Stats',
|
||||||
|
getStatsCpu: 'Get Stats CPU',
|
||||||
|
getConnectivity: 'Get Connectivity',
|
||||||
|
postSystemReboot: 'Reboot System',
|
||||||
|
postServiceRestart: 'Restart Service',
|
||||||
|
getNotificationsConfig: 'Get Notifications Config',
|
||||||
|
postNotificationsConfig: 'Set Notifications Config',
|
||||||
|
postNotificationsTestChannel: 'Test Notifications Channel',
|
||||||
|
getNotificationsLogs: 'Get Notifications Logs',
|
||||||
|
postNotificationsLogsClear: 'Clear Notifications Logs',
|
||||||
|
getNotificationsQueue: 'Get Notifications Queue',
|
||||||
|
postNotificationsQueueRetryAll: 'Retry All Notifications Queue Items',
|
||||||
|
postNotificationsQueueClear: 'Clear Notifications Queue',
|
||||||
|
deleteNotificationsQueueId: 'Delete Notifications Queue ID',
|
||||||
|
postNotificationsQueueIdRetry: 'Retry Notifications Queue ID',
|
||||||
|
getAutomationConfig: 'Get Automation Config',
|
||||||
|
postAutomationConfig: 'Set Automation Config',
|
||||||
|
getAutomationLogs: 'Get Automation Logs',
|
||||||
|
postAutomationLogsClear: 'Clear Automation Logs',
|
||||||
|
postAutomationTestTaskId: 'Test Automation Task ID',
|
||||||
|
getOtaStatus: 'Get OTA Status',
|
||||||
|
postOtaUpload: 'Upload OTA',
|
||||||
|
postOtaLatestRelease: 'Get OTA Latest Release',
|
||||||
|
postOtaOnlinePrepare: 'Prepare OTA Online',
|
||||||
|
postOtaApply: 'Apply OTA',
|
||||||
|
postOtaCancel: 'Cancel OTA',
|
||||||
|
postAuthPassword: 'Change Auth Password',
|
||||||
|
getAuthSettings: 'Get Auth Settings',
|
||||||
|
postAuthSettings: 'Set Auth Settings',
|
||||||
|
getHealth: 'Get Health',
|
||||||
|
getAuthStatus: 'Get Auth Status',
|
||||||
|
postAuthSetup: 'Set Up Auth',
|
||||||
|
postAuthLogin: 'Log In',
|
||||||
|
postAuthLogout: 'Log Out',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Stable schema IDs are materialized once from the explicit, exact reviewed key set. */
|
||||||
|
export const operationMetadata58e2204: Readonly<Record<string, OperationCatalogMetadata>> =
|
||||||
|
Object.freeze(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(operationTitles58e2204).map(([operationId, title]) => [
|
||||||
|
operationId,
|
||||||
|
Object.freeze({
|
||||||
|
title,
|
||||||
|
parameterSchemaId: `simadmin.58e2204.${operationId}.parameters.v1`,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { listOperations as listRuntimeOperations } from './runtime-registry.ts';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OperationRegistry,
|
OperationRegistry,
|
||||||
RuntimeRegistryError,
|
RuntimeRegistryError,
|
||||||
@@ -6,4 +8,29 @@ export {
|
|||||||
requireExecutableOperation,
|
requireExecutableOperation,
|
||||||
requireOperation,
|
requireOperation,
|
||||||
} from './runtime-registry.ts';
|
} from './runtime-registry.ts';
|
||||||
export type { RuntimeOperationDescriptor, RuntimeRegistryErrorCode } from './runtime-registry.ts';
|
export type { RuntimeRegistryErrorCode } from './runtime-registry.ts';
|
||||||
|
export { operationMetadata58e2204 } from './catalog-metadata-58e2204.ts';
|
||||||
|
export type { OperationCatalogMetadata } from './catalog-metadata-58e2204.ts';
|
||||||
|
|
||||||
|
/** Small public catalog row shape avoids exporting the 117-member internal descriptor union. */
|
||||||
|
export interface PublicOperationCatalogRow {
|
||||||
|
readonly operationId: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly riskLevel: 'R0' | 'R1' | 'R2' | 'R3';
|
||||||
|
readonly capability: string;
|
||||||
|
readonly batchable: boolean;
|
||||||
|
readonly parameterSchemaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listPublicOperationCatalog = (): readonly PublicOperationCatalogRow[] =>
|
||||||
|
listRuntimeOperations().map(
|
||||||
|
({ operationId, title, riskLevel, capability, batchable, parameterSchemaId }) =>
|
||||||
|
Object.freeze<PublicOperationCatalogRow>({
|
||||||
|
operationId,
|
||||||
|
title,
|
||||||
|
riskLevel,
|
||||||
|
capability,
|
||||||
|
batchable,
|
||||||
|
parameterSchemaId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { operationAcceptanceOverrides58e2204 } from './acceptance-58e2204.ts';
|
import { operationAcceptanceOverrides58e2204 } from './acceptance-58e2204.ts';
|
||||||
|
import {
|
||||||
|
operationMetadata58e2204,
|
||||||
|
type OperationCatalogMetadata,
|
||||||
|
} from './catalog-metadata-58e2204.ts';
|
||||||
import { upstream58e2204Operations } from './upstream-58e2204.ts';
|
import { upstream58e2204Operations } from './upstream-58e2204.ts';
|
||||||
|
|
||||||
export type RuntimeRegistryErrorCode = 'UNKNOWN_OPERATION' | 'DEDICATED_FLOW_REQUIRED';
|
export type RuntimeRegistryErrorCode = 'UNKNOWN_OPERATION' | 'DEDICATED_FLOW_REQUIRED';
|
||||||
@@ -13,18 +17,28 @@ export class RuntimeRegistryError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpstreamOperation = (typeof upstream58e2204Operations)[number];
|
export interface RuntimeOperationDescriptor {
|
||||||
type AcceptanceOverride = Record<string, unknown> & { uiStrategy?: unknown };
|
readonly operationId: string;
|
||||||
|
readonly method: string;
|
||||||
|
readonly pathTemplate: string;
|
||||||
|
readonly handler: string;
|
||||||
|
readonly capability: string;
|
||||||
|
readonly riskLevel: 'R0' | 'R1' | 'R2' | 'R3';
|
||||||
|
readonly batchable: boolean;
|
||||||
|
readonly executionPolicy: string;
|
||||||
|
readonly upstreamDomain: string;
|
||||||
|
readonly module: string;
|
||||||
|
readonly uiStrategy: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly parameterSchemaId: string;
|
||||||
|
readonly [field: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export type RuntimeOperationDescriptor = Readonly<
|
type AcceptanceOverride = Readonly<Record<string, unknown>> & { readonly uiStrategy?: unknown };
|
||||||
UpstreamOperation & {
|
|
||||||
module: UpstreamOperation['upstreamDomain'];
|
|
||||||
uiStrategy: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
const OPERATION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
|
const OPERATION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
|
||||||
const EXPECTED_OPERATION_COUNT = 117;
|
const EXPECTED_OPERATION_COUNT = 117;
|
||||||
|
const PARAMETER_SCHEMA_ID_PATTERN = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/;
|
||||||
|
|
||||||
function deepFreeze<T>(value: T): T {
|
function deepFreeze<T>(value: T): T {
|
||||||
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
||||||
@@ -43,13 +57,16 @@ export class OperationRegistry {
|
|||||||
readonly #operations: readonly RuntimeOperationDescriptor[];
|
readonly #operations: readonly RuntimeOperationDescriptor[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
operations: readonly UpstreamOperation[],
|
operations: readonly Readonly<Record<string, unknown>>[],
|
||||||
acceptanceOverrides: Readonly<Record<string, AcceptanceOverride>>,
|
acceptanceOverrides: Readonly<Record<string, AcceptanceOverride>>,
|
||||||
|
metadata: Readonly<Record<string, OperationCatalogMetadata>>,
|
||||||
) {
|
) {
|
||||||
const sourceById = new Map<string, UpstreamOperation>();
|
const sourceById = new Map<string, Readonly<Record<string, unknown>>>();
|
||||||
for (const operation of operations) {
|
for (const operation of operations) {
|
||||||
const id = operation.operationId;
|
const id = operation.operationId;
|
||||||
if (!OPERATION_ID_PATTERN.test(id)) throw new Error(`invalid operationId in registry: ${id}`);
|
if (typeof id !== 'string' || !OPERATION_ID_PATTERN.test(id)) {
|
||||||
|
throw new Error(`invalid operationId in registry: ${String(id)}`);
|
||||||
|
}
|
||||||
if (sourceById.has(id)) throw new Error(`duplicate operationId: ${id}`);
|
if (sourceById.has(id)) throw new Error(`duplicate operationId: ${id}`);
|
||||||
sourceById.set(id, operation);
|
sourceById.set(id, operation);
|
||||||
}
|
}
|
||||||
@@ -58,6 +75,9 @@ export class OperationRegistry {
|
|||||||
`runtime registry must contain exactly ${EXPECTED_OPERATION_COUNT} operations`,
|
`runtime registry must contain exactly ${EXPECTED_OPERATION_COUNT} operations`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for (const id of Object.keys(metadata)) {
|
||||||
|
if (!sourceById.has(id)) throw new Error(`extra operation metadata: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
const descriptors: RuntimeOperationDescriptor[] = [];
|
const descriptors: RuntimeOperationDescriptor[] = [];
|
||||||
for (const id of [...sourceById.keys()].sort()) {
|
for (const id of [...sourceById.keys()].sort()) {
|
||||||
@@ -69,10 +89,23 @@ export class OperationRegistry {
|
|||||||
throw new Error(`missing acceptance override: ${id}`);
|
throw new Error(`missing acceptance override: ${id}`);
|
||||||
}
|
}
|
||||||
const operation = sourceById.get(id)!;
|
const operation = sourceById.get(id)!;
|
||||||
|
if (!Object.hasOwn(metadata, id)) throw new Error(`missing operation metadata: ${id}`);
|
||||||
|
const catalog = metadata[id]!;
|
||||||
|
if (typeof catalog.title !== 'string' || catalog.title.trim().length === 0) {
|
||||||
|
throw new Error(`blank operation title: ${id}`);
|
||||||
|
}
|
||||||
|
if (!PARAMETER_SCHEMA_ID_PATTERN.test(catalog.parameterSchemaId)) {
|
||||||
|
throw new Error(`invalid parameter schema id: ${id}`);
|
||||||
|
}
|
||||||
|
if (typeof operation.upstreamDomain !== 'string') {
|
||||||
|
throw new Error(`invalid upstream domain: ${id}`);
|
||||||
|
}
|
||||||
const descriptor = structuredClone({
|
const descriptor = structuredClone({
|
||||||
...operation,
|
...operation,
|
||||||
module: operation.upstreamDomain,
|
module: operation.upstreamDomain,
|
||||||
uiStrategy: acceptance.uiStrategy,
|
uiStrategy: acceptance.uiStrategy,
|
||||||
|
title: catalog.title,
|
||||||
|
parameterSchemaId: catalog.parameterSchemaId,
|
||||||
}) as RuntimeOperationDescriptor;
|
}) as RuntimeOperationDescriptor;
|
||||||
descriptors.push(deepFreeze(descriptor));
|
descriptors.push(deepFreeze(descriptor));
|
||||||
}
|
}
|
||||||
@@ -98,20 +131,20 @@ export class OperationRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requireExecutableOperation(operationId: string): RuntimeOperationDescriptor {
|
requireExecutableOperation(operationId: string): RuntimeOperationDescriptor {
|
||||||
const operation = this.requireOperation(operationId);
|
const descriptor = this.requireOperation(operationId);
|
||||||
if (operation.executionPolicy === 'dedicatedFlow') {
|
if (descriptor.executionPolicy !== 'registeredOperation') {
|
||||||
throw new RuntimeRegistryError(
|
throw new RuntimeRegistryError(
|
||||||
'DEDICATED_FLOW_REQUIRED',
|
'DEDICATED_FLOW_REQUIRED',
|
||||||
`Operation requires a dedicated flow: ${operationId}`,
|
`Operation is not executable through the registered-operation boundary: ${operationId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return operation;
|
return descriptor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const operationRegistry = new OperationRegistry(
|
export const operationRegistry = new OperationRegistry(
|
||||||
upstream58e2204Operations,
|
upstream58e2204Operations,
|
||||||
operationAcceptanceOverrides58e2204,
|
operationAcceptanceOverrides58e2204,
|
||||||
|
operationMetadata58e2204,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const listOperations = (): readonly RuntimeOperationDescriptor[] =>
|
export const listOperations = (): readonly RuntimeOperationDescriptor[] =>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { OperationRegistry, operationMetadata58e2204, requireOperation } from '../src/index.ts';
|
||||||
|
import { operationAcceptanceOverrides58e2204 } from '../src/acceptance-58e2204.ts';
|
||||||
|
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
||||||
|
|
||||||
|
test('explicit reviewed metadata covers every operation and preserves domain acronyms', () => {
|
||||||
|
assert.equal(Object.keys(operationMetadata58e2204).length, 117);
|
||||||
|
assert.deepEqual(
|
||||||
|
Object.keys(operationMetadata58e2204).toSorted(),
|
||||||
|
upstream58e2204Operations.map(({ operationId }) => operationId).toSorted(),
|
||||||
|
);
|
||||||
|
for (const [operationId, metadata] of Object.entries(operationMetadata58e2204)) {
|
||||||
|
assert.match(metadata.title, /\S/);
|
||||||
|
assert.equal(metadata.parameterSchemaId, `simadmin.58e2204.${operationId}.parameters.v1`);
|
||||||
|
}
|
||||||
|
const titles = Object.values(operationMetadata58e2204)
|
||||||
|
.map(({ title }) => title)
|
||||||
|
.join(' ');
|
||||||
|
for (const acronym of ['SMS', 'APN', 'DDNS', 'WLAN', 'IMS', 'OTA', 'LPAC', 'eUICC', 'ICCID']) {
|
||||||
|
assert.match(titles, new RegExp(`\\b${acronym}\\b`), acronym);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructor rejects missing/extra/blank/malformed metadata and freezes metadata fields', () => {
|
||||||
|
const rows = upstream58e2204Operations;
|
||||||
|
const missing = { ...operationMetadata58e2204 } as Record<string, unknown>;
|
||||||
|
delete missing.getDevice;
|
||||||
|
assert.throws(
|
||||||
|
() => new OperationRegistry(rows, operationAcceptanceOverrides58e2204, missing as never),
|
||||||
|
/missing operation metadata: getDevice/,
|
||||||
|
);
|
||||||
|
const extra = {
|
||||||
|
...operationMetadata58e2204,
|
||||||
|
inventedOperation: {
|
||||||
|
title: 'Invented',
|
||||||
|
parameterSchemaId: 'simadmin.58e2204.inventedOperation.parameters.v1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => new OperationRegistry(rows, operationAcceptanceOverrides58e2204, extra),
|
||||||
|
/extra operation metadata: inventedOperation/,
|
||||||
|
);
|
||||||
|
const blank = {
|
||||||
|
...operationMetadata58e2204,
|
||||||
|
getDevice: { ...operationMetadata58e2204.getDevice, title: ' ' },
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => new OperationRegistry(rows, operationAcceptanceOverrides58e2204, blank),
|
||||||
|
/blank operation title: getDevice/,
|
||||||
|
);
|
||||||
|
const malformed = {
|
||||||
|
...operationMetadata58e2204,
|
||||||
|
getDevice: { ...operationMetadata58e2204.getDevice, parameterSchemaId: '../schema' },
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => new OperationRegistry(rows, operationAcceptanceOverrides58e2204, malformed),
|
||||||
|
/invalid parameter schema id: getDevice/,
|
||||||
|
);
|
||||||
|
const descriptor = requireOperation('getDevice');
|
||||||
|
assert.equal(descriptor.title, 'Get Device');
|
||||||
|
assert.equal(descriptor.parameterSchemaId, 'simadmin.58e2204.getDevice.parameters.v1');
|
||||||
|
assert.ok(Object.isFrozen(descriptor));
|
||||||
|
});
|
||||||
@@ -10,7 +10,25 @@ import {
|
|||||||
} from '../src/index.ts';
|
} from '../src/index.ts';
|
||||||
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
||||||
import { operationAcceptanceOverrides58e2204 } from '../src/acceptance-58e2204.ts';
|
import { operationAcceptanceOverrides58e2204 } from '../src/acceptance-58e2204.ts';
|
||||||
|
import { operationMetadata58e2204 } from '../src/catalog-metadata-58e2204.ts';
|
||||||
|
|
||||||
|
test('generated API catalog is an exact safe projection of the runtime registry', async () => {
|
||||||
|
const { default: generated } = await import(
|
||||||
|
'../../../apps/api/src/application/operations/operation-catalog-58e2204.json',
|
||||||
|
{ with: { type: 'json' } }
|
||||||
|
);
|
||||||
|
const expected = listOperations().map(
|
||||||
|
({ operationId, title, riskLevel, capability, batchable, parameterSchemaId }) => ({
|
||||||
|
operationId,
|
||||||
|
title,
|
||||||
|
riskLevel,
|
||||||
|
capability,
|
||||||
|
batchable,
|
||||||
|
parameterSchemaId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.deepEqual(generated, expected);
|
||||||
|
});
|
||||||
test('runtime registry projects exactly 117 descriptors in stable operationId order', () => {
|
test('runtime registry projects exactly 117 descriptors in stable operationId order', () => {
|
||||||
const listed = listOperations();
|
const listed = listOperations();
|
||||||
assert.equal(listed.length, 117);
|
assert.equal(listed.length, 117);
|
||||||
@@ -34,13 +52,18 @@ test('runtime registry projects exactly 117 descriptors in stable operationId or
|
|||||||
test('runtime construction rejects duplicate rows and missing acceptance overrides', () => {
|
test('runtime construction rejects duplicate rows and missing acceptance overrides', () => {
|
||||||
const rows = upstream58e2204Operations as readonly (typeof upstream58e2204Operations)[number][];
|
const rows = upstream58e2204Operations as readonly (typeof upstream58e2204Operations)[number][];
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => new OperationRegistry([...rows, rows[0]], operationAcceptanceOverrides58e2204),
|
() =>
|
||||||
|
new OperationRegistry(
|
||||||
|
[...rows, rows[0]],
|
||||||
|
operationAcceptanceOverrides58e2204,
|
||||||
|
operationMetadata58e2204,
|
||||||
|
),
|
||||||
/duplicate operationId: getDevice/,
|
/duplicate operationId: getDevice/,
|
||||||
);
|
);
|
||||||
const missing = { ...operationAcceptanceOverrides58e2204 };
|
const missing = { ...operationAcceptanceOverrides58e2204 };
|
||||||
delete (missing as Record<string, unknown>).getDevice;
|
delete (missing as Record<string, unknown>).getDevice;
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => new OperationRegistry(rows, missing),
|
() => new OperationRegistry(rows, missing, operationMetadata58e2204),
|
||||||
/missing acceptance override: getDevice/,
|
/missing acceptance override: getDevice/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -86,6 +109,18 @@ test('dedicated auth flows are catalogued but cannot cross executable boundary',
|
|||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const malformed = new OperationRegistry(
|
||||||
|
upstream58e2204Operations.map((row) =>
|
||||||
|
row.operationId === 'getDevice' ? { ...row, executionPolicy: 'futurePolicy' } : row,
|
||||||
|
),
|
||||||
|
operationAcceptanceOverrides58e2204,
|
||||||
|
operationMetadata58e2204,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => malformed.requireExecutableOperation('getDevice'),
|
||||||
|
(error: unknown) =>
|
||||||
|
error instanceof RuntimeRegistryError && error.code === 'DEDICATED_FLOW_REQUIRED',
|
||||||
|
);
|
||||||
assert.strictEqual(requireExecutableOperation('getDevice'), requireOperation('getDevice'));
|
assert.strictEqual(requireExecutableOperation('getDevice'), requireOperation('getDevice'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+3
@@ -39,6 +39,9 @@ importers:
|
|||||||
'@multi-simadmin/contracts':
|
'@multi-simadmin/contracts':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/contracts
|
version: link:../../packages/contracts
|
||||||
|
'@multi-simadmin/operation-registry':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/operation-registry
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: 12.11.1
|
specifier: 12.11.1
|
||||||
version: 12.11.1
|
version: 12.11.1
|
||||||
|
|||||||
Reference in New Issue
Block a user