feat(operations): add audited runtime catalog

This commit is contained in:
chick
2026-07-17 09:07:50 +08:00
parent 243565a75e
commit 8018b36adf
15 changed files with 1617 additions and 21 deletions
@@ -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`,
}),
]),
),
);
+28 -1
View File
@@ -1,3 +1,5 @@
import { listOperations as listRuntimeOperations } from './runtime-registry.ts';
export {
OperationRegistry,
RuntimeRegistryError,
@@ -6,4 +8,29 @@ export {
requireExecutableOperation,
requireOperation,
} 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 {
operationMetadata58e2204,
type OperationCatalogMetadata,
} from './catalog-metadata-58e2204.ts';
import { upstream58e2204Operations } from './upstream-58e2204.ts';
export type RuntimeRegistryErrorCode = 'UNKNOWN_OPERATION' | 'DEDICATED_FLOW_REQUIRED';
@@ -13,18 +17,28 @@ export class RuntimeRegistryError extends Error {
}
}
type UpstreamOperation = (typeof upstream58e2204Operations)[number];
type AcceptanceOverride = Record<string, unknown> & { uiStrategy?: unknown };
export interface RuntimeOperationDescriptor {
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<
UpstreamOperation & {
module: UpstreamOperation['upstreamDomain'];
uiStrategy: string;
}
>;
type AcceptanceOverride = Readonly<Record<string, unknown>> & { readonly uiStrategy?: unknown };
const OPERATION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
const EXPECTED_OPERATION_COUNT = 117;
const PARAMETER_SCHEMA_ID_PATTERN = /^[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)+$/;
function deepFreeze<T>(value: T): T {
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
@@ -43,13 +57,16 @@ export class OperationRegistry {
readonly #operations: readonly RuntimeOperationDescriptor[];
constructor(
operations: readonly UpstreamOperation[],
operations: readonly Readonly<Record<string, unknown>>[],
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) {
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}`);
sourceById.set(id, operation);
}
@@ -58,6 +75,9 @@ export class OperationRegistry {
`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[] = [];
for (const id of [...sourceById.keys()].sort()) {
@@ -69,10 +89,23 @@ export class OperationRegistry {
throw new Error(`missing acceptance override: ${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({
...operation,
module: operation.upstreamDomain,
uiStrategy: acceptance.uiStrategy,
title: catalog.title,
parameterSchemaId: catalog.parameterSchemaId,
}) as RuntimeOperationDescriptor;
descriptors.push(deepFreeze(descriptor));
}
@@ -98,20 +131,20 @@ export class OperationRegistry {
}
requireExecutableOperation(operationId: string): RuntimeOperationDescriptor {
const operation = this.requireOperation(operationId);
if (operation.executionPolicy === 'dedicatedFlow') {
const descriptor = this.requireOperation(operationId);
if (descriptor.executionPolicy !== 'registeredOperation') {
throw new RuntimeRegistryError(
'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(
upstream58e2204Operations,
operationAcceptanceOverrides58e2204,
operationMetadata58e2204,
);
export const listOperations = (): readonly RuntimeOperationDescriptor[] =>