feat(operations): add audited runtime catalog
This commit is contained in:
@@ -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 {
|
||||
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[] =>
|
||||
|
||||
@@ -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';
|
||||
import { upstream58e2204Operations } from '../src/upstream-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', () => {
|
||||
const listed = listOperations();
|
||||
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', () => {
|
||||
const rows = upstream58e2204Operations as readonly (typeof upstream58e2204Operations)[number][];
|
||||
assert.throws(
|
||||
() => new OperationRegistry([...rows, rows[0]], operationAcceptanceOverrides58e2204),
|
||||
() =>
|
||||
new OperationRegistry(
|
||||
[...rows, rows[0]],
|
||||
operationAcceptanceOverrides58e2204,
|
||||
operationMetadata58e2204,
|
||||
),
|
||||
/duplicate operationId: getDevice/,
|
||||
);
|
||||
const missing = { ...operationAcceptanceOverrides58e2204 };
|
||||
delete (missing as Record<string, unknown>).getDevice;
|
||||
assert.throws(
|
||||
() => new OperationRegistry(rows, missing),
|
||||
() => new OperationRegistry(rows, missing, operationMetadata58e2204),
|
||||
/missing acceptance override: getDevice/,
|
||||
);
|
||||
});
|
||||
@@ -86,6 +109,18 @@ test('dedicated auth flows are catalogued but cannot cross executable boundary',
|
||||
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'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user