From 2fa1951f51c9515a06eb78a6b4274156e618c24d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 12 Aug 2026 22:57:20 +0300 Subject: [PATCH] feat(device-manager): add safe B2 service ping command --- .../server/device-core-client.mjs | 47 ++++++++- .../server/device-core-client.test.mjs | 2 +- .../server/device-manager-server.mjs | 1 + .../device-manager/src/DeviceControlViews.tsx | 95 +++++++++++++++++-- .../src/DeviceInventoryView.tsx | 2 +- apps/device-manager/src/api.ts | 9 ++ apps/device-manager/src/styles.css | 15 +++ apps/device-manager/src/types.ts | 2 +- 8 files changed, 161 insertions(+), 12 deletions(-) diff --git a/apps/device-manager/server/device-core-client.mjs b/apps/device-manager/server/device-core-client.mjs index 2216297..c097f16 100644 --- a/apps/device-manager/server/device-core-client.mjs +++ b/apps/device-manager/server/device-core-client.mjs @@ -22,6 +22,7 @@ const commandRoutes = new Map([ "device-configurations:set-desired", "/internal/v1/management/device-configurations:set-desired", ], + ["commands:service-ping", "/internal/v1/commands:service-ping"], ]); export function createDeviceCoreClient({ baseUrl, token, fetchImpl = fetch } = {}) { @@ -97,6 +98,7 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) { const configurationRevisions = new Map(); const configurationStates = new Map(); const auditEvents = []; + const commands = new Map(); function now() { return new Date().toISOString(); @@ -152,12 +154,14 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) { bindings: projectValues(bindings, projectRef), configurationRevisions: projectValues(configurationRevisions, projectRef), configurationStates: projectValues(configurationStates, projectRef), - commands: [], + commands: projectValues(commands, projectRef), auditEvents: auditEvents.filter((event) => event.projectRef === projectRef), grants: projectValues(grants, projectRef), policies: { - commandTransport: "disabled", - commandPlanningApi: "disabled", + commandTransport: fixture === "arusnavi-b2" + ? "typed-service-ping-v1" + : "disabled", + commandPlanningApi: fixture === "arusnavi-b2" ? "enabled" : "disabled", identifierProjection: "masked-only", auditPayloadProjection: "metadata-only", }, @@ -187,6 +191,42 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) { return workspace(projectRef); }, async execute(command, actor, input) { + if (command === "commands:service-ping") { + if (fixture !== "arusnavi-b2") { + throw serviceError("device_command_transport_disabled", 409); + } + const device = devices.get(input.deviceRef); + if (!device || device.projectRef !== input.projectRef) { + throw serviceError("device_command_route_unavailable", 409); + } + if (typeof input.accessCode !== "string" || !/^\d{6}$/.test(input.accessCode)) { + throw serviceError("device_service_ping_access_code_invalid", 400); + } + const commandRef = `command:${randomUUID()}`; + const at = now(); + const view = { + commandRef, + projectRef: input.projectRef, + deviceRef: input.deviceRef, + deviceName: device.displayName, + commandKey: `preview-service-ping-${randomUUID()}`, + commandCatalogRef: "arusnavi.b2.internal.v1:service-ping", + commandType: "service.ping", + riskClass: "low", + lifecycleState: "queued", + plannedAt: at, + expiresAt: new Date(Date.now() + Number(input.expiresInSeconds) * 1000).toISOString(), + confirmedAt: null, + dispatchedAt: null, + acknowledgedAt: null, + terminalAt: null, + terminalReasonCode: null, + createdAt: at, + updatedAt: at, + }; + commands.set(commandRef, view); + return { replayed: false, result: view }; + } if (command === "owner-scopes:ensure") { const key = `${input.scopeKind}:${input.ownerRef}`; const created = !ownerScopes.has(key); @@ -580,6 +620,7 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) { grants, configurationRevisions, configurationStates, + commands, auditEvents, }; }, diff --git a/apps/device-manager/server/device-core-client.test.mjs b/apps/device-manager/server/device-core-client.test.mjs index cd59f22..eed6595 100644 --- a/apps/device-manager/server/device-core-client.test.mjs +++ b/apps/device-manager/server/device-core-client.test.mjs @@ -229,7 +229,7 @@ test("explicit B2 preview fixture is isolated from the empty canonical preview", assert.equal(workspace.devices[0].modelProfileRef, "arusnavi.b2.internal.v1"); assert.equal(workspace.devices[0].identifier.masked, "***********0001"); assert.equal(workspace.sessions[0].lifecycleState, "online"); - assert.equal(workspace.policies.commandTransport, "disabled"); + assert.equal(workspace.policies.commandTransport, "typed-service-ping-v1"); assert.equal(JSON.stringify(workspace).includes("123456789012345"), false); assert.throws( diff --git a/apps/device-manager/server/device-manager-server.mjs b/apps/device-manager/server/device-manager-server.mjs index 556fd0a..3fd5858 100644 --- a/apps/device-manager/server/device-manager-server.mjs +++ b/apps/device-manager/server/device-manager-server.mjs @@ -33,6 +33,7 @@ const mutationRoutes = new Map([ "/api/device-manager/device-configurations:set-desired", "device-configurations:set-desired", ], + ["/api/device-manager/commands:service-ping", "commands:service-ping"], ]); export function createDeviceManagerServer({ diff --git a/apps/device-manager/src/DeviceControlViews.tsx b/apps/device-manager/src/DeviceControlViews.tsx index 7c4a137..a7eb2be 100644 --- a/apps/device-manager/src/DeviceControlViews.tsx +++ b/apps/device-manager/src/DeviceControlViews.tsx @@ -21,6 +21,7 @@ import { registerAdapterVersion, registerModelProfile, revokeDeviceBinding, + sendServicePing, setDesiredConfiguration, upsertProjectGrant, } from "./api"; @@ -157,7 +158,14 @@ export function DeviceControlView({ }).then(onRefresh).catch(onError)} /> ) : null} - {view === "commands" ? : null} + {view === "commands" ? ( + + ) : null} {view === "audit" ? : null} {view === "access" ? ( Promise; + onError: (reason: unknown) => void; +}) { + const supportedDevices = workspace.devices.filter( + (device) => device.modelProfileRef === "arusnavi.b2.internal.v1" + && !["suspended", "retired"].includes(device.lifecycleState), + ); + const [deviceRef, setDeviceRef] = useState(supportedDevices[0]?.deviceRef ?? ""); + const [accessCode, setAccessCode] = useState(""); + const [submitting, setSubmitting] = useState(false); + const enabled = workspace.policies.commandTransport === "typed-service-ping-v1"; + useEffect(() => { + if (!supportedDevices.some((device) => device.deviceRef === deviceRef)) { + setDeviceRef(supportedDevices[0]?.deviceRef ?? ""); + } + }, [deviceRef, supportedDevices]); + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (!enabled || !canDispatch || !deviceRef || !/^\d{6}$/.test(accessCode)) return; + setSubmitting(true); + try { + await sendServicePing({ + projectRef: workspace.project.projectRef, + deviceRef, + accessCode, + expiresInSeconds: 300, + }); + setAccessCode(""); + await onRefresh(); + } catch (reason) { + onError(reason); + } finally { + setSubmitting(false); + } + }; return (
- +
- Command transport выключен -

Ни UI, ни BFF не имеют raw command builder. acknowledged означает подтверждение протокола, verified — отдельное доказательство состояния.

+ {enabled ? "Типизированный командный канал активен" : "Command transport выключен"} +

{enabled + ? "Доступна только безопасная проверка сервиса. Произвольные команды, прошивка, очистка памяти и перезагрузка отсутствуют. Код устройства существует только в памяти Core до отправки или истечения TTL." + : "Ни UI, ни BFF не имеют raw command builder. acknowledged означает подтверждение протокола, verified — отдельное доказательство состояния."}

- {workspace.policies.commandTransport} + {workspace.policies.commandTransport}
+ {enabled ? ( +
+