miravo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/console-dist/assets/activity-DsWeRq4h.js +1 -0
- package/console-dist/assets/api-4AqKvsm9.js +1 -0
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
- package/console-dist/assets/box-C1WaoOMF.js +1 -0
- package/console-dist/assets/card-BjeP32PN.js +1 -0
- package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
- package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
- package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
- package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
- package/console-dist/assets/control-CPhvwx9o.js +1 -0
- package/console-dist/assets/control-Cg2up9NA.js +1 -0
- package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
- package/console-dist/assets/dist-BhbDML0a.js +1 -0
- package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
- package/console-dist/assets/download-DQ6NH16U.js +1 -0
- package/console-dist/assets/engine-store-CrLnz692.js +1 -0
- package/console-dist/assets/events-CXm29Ok4.js +1 -0
- package/console-dist/assets/events-D01IxhnP.js +1 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
- package/console-dist/assets/fleet-lotMGVqe.js +1 -0
- package/console-dist/assets/format-kXTonpDJ.js +1 -0
- package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
- package/console-dist/assets/index-CVPKSDvJ.js +12 -0
- package/console-dist/assets/index-DJkgDeun.css +2 -0
- package/console-dist/assets/input-BWEQFptx.js +1 -0
- package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
- package/console-dist/assets/label-uephX5ZG.js +1 -0
- package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
- package/console-dist/assets/login-C3RJli0m.js +126 -0
- package/console-dist/assets/models-B_UioWQ5.js +1 -0
- package/console-dist/assets/models-_IPlPiZ7.js +1 -0
- package/console-dist/assets/not-found-B8gAm08S.js +1 -0
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
- package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
- package/console-dist/assets/platforms-DStNbtVc.js +1 -0
- package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
- package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
- package/console-dist/assets/radio-CoHegOv5.js +1 -0
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
- package/console-dist/assets/select-CeO4vP0G.js +1 -0
- package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
- package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
- package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
- package/console-dist/assets/tags-CiXOIHtY.js +4 -0
- package/console-dist/assets/tags-FQCXIxrP.js +1 -0
- package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
- package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
- package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
- package/console-dist/assets/validation-Blvud9_O.js +1 -0
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
- package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
- package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
- package/console-dist/index.html +70 -2
- package/package.json +9 -8
- package/src/__tests__/integration/executable-methods-runner.ts +7 -6
- package/src/__tests__/integration/executable-methods.integration.ts +214 -122
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
- package/src/commands/adapter.ts +180 -7
- package/src/commands/apply.test.ts +62 -0
- package/src/commands/apply.ts +216 -0
- package/src/commands/cleanup.test.ts +34 -0
- package/src/commands/cleanup.ts +201 -0
- package/src/commands/export.test.ts +51 -0
- package/src/commands/export.ts +404 -0
- package/src/commands/fork.ts +2 -2
- package/src/commands/inject.ts +2 -2
- package/src/commands/load.ts +1 -1
- package/src/commands/logs.ts +13 -15
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +1 -1
- package/src/commands/pause.ts +3 -3
- package/src/commands/platforms.test.ts +186 -0
- package/src/commands/platforms.ts +371 -0
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +2 -2
- package/src/commands/spawn.ts +2 -2
- package/src/commands/speed.ts +2 -2
- package/src/commands/start/adapter-config.ts +157 -0
- package/src/commands/start/catalog-le-dt.test.ts +50 -0
- package/src/commands/start/catalog-le-dt.ts +187 -0
- package/src/commands/start/catalog-model-schema.ts +89 -0
- package/src/commands/start/catalog-template-detail.ts +54 -0
- package/src/commands/start/daemon.ts +255 -0
- package/src/commands/start/display.ts +150 -0
- package/src/commands/start/engine-setup.ts +247 -0
- package/src/commands/start/events.ts +126 -0
- package/src/commands/start/index.ts +897 -0
- package/src/commands/start/protocol-plans.ts +239 -0
- package/src/commands/start/template-loader.ts +197 -0
- package/src/commands/start/template-resolver.ts +185 -0
- package/src/commands/start/types.ts +73 -0
- package/src/commands/start.test.ts +137 -16
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +19 -8
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +1 -3
- package/src/index.ts +11 -3
- package/src/lib/admin-auth.ts +157 -0
- package/src/lib/admin-client.test.ts +31 -15
- package/src/lib/admin-client.ts +106 -6
- package/src/lib/admin-platform-routes.ts +1017 -0
- package/src/lib/admin-platform-stream.test.ts +434 -0
- package/src/lib/admin-platform-stream.ts +788 -0
- package/src/lib/admin-responses.ts +211 -0
- package/src/lib/admin-server.test.ts +274 -55
- package/src/lib/admin-server.ts +387 -248
- package/src/lib/auth-store.ts +87 -0
- package/src/lib/daemon-registry.ts +12 -14
- package/src/lib/le/le-apply.test.ts +591 -0
- package/src/lib/le/le-apply.ts +832 -0
- package/src/lib/le/le-auth.ts +89 -0
- package/src/lib/le/le-cleanup.test.ts +168 -0
- package/src/lib/le/le-cleanup.ts +399 -0
- package/src/lib/le/le-cli-helpers.ts +77 -0
- package/src/lib/le/le-client.ts +184 -0
- package/src/lib/le/le-concurrency.test.ts +157 -0
- package/src/lib/le/le-concurrency.ts +78 -0
- package/src/lib/le/le-connection-test.ts +42 -0
- package/src/lib/le/le-devicehub-properties.test.ts +53 -0
- package/src/lib/le/le-devicehub-properties.ts +33 -0
- package/src/lib/le/le-devicehub.ts +213 -0
- package/src/lib/le/le-digital-twins.ts +285 -0
- package/src/lib/le/le-drivers.ts +23 -0
- package/src/lib/le/le-endpoint.ts +40 -0
- package/src/lib/le/le-environments.test.ts +73 -0
- package/src/lib/le/le-environments.ts +156 -0
- package/src/lib/le/le-markers.ts +87 -0
- package/src/lib/le/le-plan.test.ts +26 -0
- package/src/lib/le/le-plan.ts +109 -0
- package/src/lib/le/le-platform-operations.test.ts +198 -0
- package/src/lib/le/le-platform-operations.ts +340 -0
- package/src/lib/le/le-static-model-schema.ts +78 -0
- package/src/lib/le/le-static-planner.test.ts +255 -0
- package/src/lib/le/le-static-planner.ts +442 -0
- package/src/lib/le/le-template-exporter.test.ts +527 -0
- package/src/lib/le/le-template-exporter.ts +459 -0
- package/src/lib/le/le-value-types.ts +23 -0
- package/src/lib/mqtt-client.ts +1 -2
- package/src/lib/resolve-simulation.ts +1 -6
- package/src/lib/session-store.ts +110 -0
- package/src/lib/shutdown.test.ts +184 -2
- package/src/lib/shutdown.ts +104 -34
- package/src/lib/template-category.ts +3 -3
- package/src/lib/template-locator.ts +8 -2
- package/src/lib/ws-hub.ts +72 -46
- package/src/logger.ts +39 -14
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/console-dist/assets/index-hUJSpKtB.js +0 -119
- package/src/commands/start.ts +0 -2023
package/console-dist/index.html
CHANGED
|
@@ -1,12 +1,80 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
+
<link rel="preload" href="/assets/fira-code-latin-wght-normal-CHoedHDv.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
|
5
|
+
<link rel="preload" href="/assets/karla-latin-wght-normal-C3-ma4ov.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
|
6
|
+
<link rel="preload" href="/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
|
7
|
+
|
|
4
8
|
<meta charset="UTF-8" />
|
|
5
9
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
10
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11
|
+
<meta name="theme-color" content="#0b0f12" />
|
|
12
|
+
<style>
|
|
13
|
+
html,
|
|
14
|
+
body,
|
|
15
|
+
#root {
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
html,
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
background: #0b0f12;
|
|
22
|
+
color-scheme: dark;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
7
25
|
<title>Miravo</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="
|
|
26
|
+
<script type="module" crossorigin src="/assets/index-CVPKSDvJ.js"></script>
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-COnpUsM8.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-charts-CAZdaPlq.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-OE4NN4TA-CEjpfhS6.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-ui-ByahnaY0.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="/assets/api-4AqKvsm9.js">
|
|
32
|
+
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-Bwxw859K.js">
|
|
33
|
+
<link rel="modulepreload" crossorigin href="/assets/query-keys-BvtE7BMs.js">
|
|
34
|
+
<link rel="modulepreload" crossorigin href="/assets/input-BWEQFptx.js">
|
|
35
|
+
<link rel="modulepreload" crossorigin href="/assets/proxy-CsnYqnPp.js">
|
|
36
|
+
<link rel="modulepreload" crossorigin href="/assets/activity-DsWeRq4h.js">
|
|
37
|
+
<link rel="modulepreload" crossorigin href="/assets/arrow-right-Ea_UhTNi.js">
|
|
38
|
+
<link rel="modulepreload" crossorigin href="/assets/select-CeO4vP0G.js">
|
|
39
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-targets-R3T-4eJ7.js">
|
|
40
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-BhbDML0a.js">
|
|
41
|
+
<link rel="modulepreload" crossorigin href="/assets/tooltip-HGLF366Q.js">
|
|
42
|
+
<link rel="modulepreload" crossorigin href="/assets/copy-button-WkbNkMcd.js">
|
|
43
|
+
<link rel="modulepreload" crossorigin href="/assets/checkbox-DZlJXTE4.js">
|
|
44
|
+
<link rel="modulepreload" crossorigin href="/assets/radio-CoHegOv5.js">
|
|
45
|
+
<link rel="modulepreload" crossorigin href="/assets/triangle-alert-adNk_sea.js">
|
|
46
|
+
<link rel="modulepreload" crossorigin href="/assets/format-kXTonpDJ.js">
|
|
47
|
+
<link rel="modulepreload" crossorigin href="/assets/label-uephX5ZG.js">
|
|
48
|
+
<link rel="modulepreload" crossorigin href="/assets/use-platform-stream-DGbK1k2L.js">
|
|
49
|
+
<link rel="modulepreload" crossorigin href="/assets/catalog-template-icons-BrUeHaCY.js">
|
|
50
|
+
<link rel="modulepreload" crossorigin href="/assets/layers-CUsLQ1fq.js">
|
|
51
|
+
<link rel="modulepreload" crossorigin href="/assets/server-D9Kfn8fQ.js">
|
|
52
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-Cy8XTNFT.js">
|
|
53
|
+
<link rel="modulepreload" crossorigin href="/assets/platforms-BWh0BQ3Y.js">
|
|
54
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-table-wd8JuMmT.js">
|
|
55
|
+
<link rel="modulepreload" crossorigin href="/assets/arrow-up-down-DFSjzzl2.js">
|
|
56
|
+
<link rel="modulepreload" crossorigin href="/assets/download-DQ6NH16U.js">
|
|
57
|
+
<link rel="modulepreload" crossorigin href="/assets/engine-store-CrLnz692.js">
|
|
58
|
+
<link rel="modulepreload" crossorigin href="/assets/get-error-message-3Q5Q-2G5.js">
|
|
59
|
+
<link rel="modulepreload" crossorigin href="/assets/card-BjeP32PN.js">
|
|
60
|
+
<link rel="modulepreload" crossorigin href="/assets/combobox-BGnuCYxO.js">
|
|
61
|
+
<link rel="modulepreload" crossorigin href="/assets/tags-CiXOIHtY.js">
|
|
62
|
+
<link rel="modulepreload" crossorigin href="/assets/box-C1WaoOMF.js">
|
|
63
|
+
<link rel="modulepreload" crossorigin href="/assets/blueprint-card-classes-DBFCDy0M.js">
|
|
64
|
+
<link rel="modulepreload" crossorigin href="/assets/shallow-6lGjQhtJ.js">
|
|
65
|
+
<link rel="modulepreload" crossorigin href="/assets/use-transition-navigate-CCpcWXB0.js">
|
|
66
|
+
<link rel="modulepreload" crossorigin href="/assets/transition-nav-link-Df4Rk6vV.js">
|
|
67
|
+
<link rel="modulepreload" crossorigin href="/assets/validation-Blvud9_O.js">
|
|
68
|
+
<link rel="modulepreload" crossorigin href="/assets/control-CPhvwx9o.js">
|
|
69
|
+
<link rel="modulepreload" crossorigin href="/assets/deploy-tab-JeWDwPoZ.js">
|
|
70
|
+
<link rel="modulepreload" crossorigin href="/assets/models-B_UioWQ5.js">
|
|
71
|
+
<link rel="modulepreload" crossorigin href="/assets/catalog-BJzyzbLR.js">
|
|
72
|
+
<link rel="modulepreload" crossorigin href="/assets/gauge-Ci8zNZlk.js">
|
|
73
|
+
<link rel="modulepreload" crossorigin href="/assets/select-display-items-CIzY8953.js">
|
|
74
|
+
<link rel="modulepreload" crossorigin href="/assets/transition-types-CPlGtM8w.js">
|
|
75
|
+
<link rel="modulepreload" crossorigin href="/assets/fleet-CfxbKXt9.js">
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/events-D01IxhnP.js">
|
|
77
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DJkgDeun.css">
|
|
10
78
|
</head>
|
|
11
79
|
<body>
|
|
12
80
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miravo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Industrial environment platform with canonical asset models and MQTT + OPC UA protocol adapters",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,19 +32,20 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.1.0",
|
|
35
|
-
"@miravo/core": "0.
|
|
36
|
-
"@miravo/
|
|
37
|
-
"@miravo/
|
|
38
|
-
"@miravo/
|
|
35
|
+
"@miravo/core": "0.6.0",
|
|
36
|
+
"@miravo/modbus": "0.6.0",
|
|
37
|
+
"@miravo/mqtt": "0.6.0",
|
|
38
|
+
"@miravo/opcua": "0.6.0",
|
|
39
|
+
"@miravo/shared": "0.6.0",
|
|
39
40
|
"commander": "^13.0.0",
|
|
40
41
|
"consola": "^3.4.0",
|
|
41
42
|
"mqtt": "^5.15.0",
|
|
42
43
|
"yaml": "^2.8.1",
|
|
43
|
-
"zod": "^
|
|
44
|
+
"zod": "^3.24.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@miravo/console": "0.0.0",
|
|
47
|
-
"node-opcua-client": "^2.
|
|
48
|
-
"node-opcua-variant": "^2.
|
|
48
|
+
"node-opcua-client": "^2.169.0",
|
|
49
|
+
"node-opcua-variant": "^2.169.0"
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -9,6 +9,7 @@ import { MQTTAdapter } from "@miravo/mqtt";
|
|
|
9
9
|
import { OPCUAAdapter } from "@miravo/opcua";
|
|
10
10
|
import type { MiravoTemplate, TwinModel } from "@miravo/shared";
|
|
11
11
|
import { startAdminServer } from "../../lib/admin-server.ts";
|
|
12
|
+
import { createSessionStore } from "../../lib/session-store.ts";
|
|
12
13
|
import { buildUNSBasePath, forEachTwin } from "../../lib/template-utils.ts";
|
|
13
14
|
|
|
14
15
|
interface RunnerArgs {
|
|
@@ -194,12 +195,12 @@ async function main(): Promise<void> {
|
|
|
194
195
|
},
|
|
195
196
|
ws: {
|
|
196
197
|
bus: engine.bus,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
198
|
+
},
|
|
199
|
+
auth: {
|
|
200
|
+
verifyPassword: async () => true,
|
|
201
|
+
isDefaultPassword: false,
|
|
202
|
+
sessionStore: createSessionStore(),
|
|
203
|
+
changePassword: async () => {},
|
|
203
204
|
},
|
|
204
205
|
});
|
|
205
206
|
|
|
@@ -49,6 +49,7 @@ const processOutputCache = new WeakMap<
|
|
|
49
49
|
SmokeRuntimeHandle["process"],
|
|
50
50
|
Promise<{ readonly stdout: string; readonly stderr: string }>
|
|
51
51
|
>();
|
|
52
|
+
const adminSessionCookies = new Map<string, string>();
|
|
52
53
|
|
|
53
54
|
interface InvokeMethodCommandPayload {
|
|
54
55
|
readonly type: "invokeMethod";
|
|
@@ -536,6 +537,62 @@ async function waitForAsync<T>(
|
|
|
536
537
|
throw new Error(`Timed out waiting for ${label}.`);
|
|
537
538
|
}
|
|
538
539
|
|
|
540
|
+
function normalizeCookieHeader(value: string): string {
|
|
541
|
+
const firstSegment = value.split(",")[0] ?? value;
|
|
542
|
+
return (firstSegment.split(";")[0] ?? firstSegment).trim();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function loginAdminSession(adminUrl: string): Promise<string> {
|
|
546
|
+
const password = Bun.env["MIRAVO_ADMIN_PASSWORD"]?.trim() || "admin";
|
|
547
|
+
const response = await fetch(new URL("/auth/login", `${adminUrl}/`), {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: {
|
|
550
|
+
"content-type": "application/json",
|
|
551
|
+
},
|
|
552
|
+
body: JSON.stringify({ username: "admin", password }),
|
|
553
|
+
});
|
|
554
|
+
if (!response.ok) {
|
|
555
|
+
throw new Error(`Admin auth login failed with HTTP ${response.status}.`);
|
|
556
|
+
}
|
|
557
|
+
const setCookie = response.headers.get("set-cookie");
|
|
558
|
+
if (!setCookie) {
|
|
559
|
+
throw new Error("Admin auth login succeeded but no session cookie was returned.");
|
|
560
|
+
}
|
|
561
|
+
const cookie = normalizeCookieHeader(setCookie);
|
|
562
|
+
adminSessionCookies.set(adminUrl, cookie);
|
|
563
|
+
return cookie;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function fetchAdmin(
|
|
567
|
+
runtime: SmokeRuntimeHandle,
|
|
568
|
+
path: string,
|
|
569
|
+
init?: Omit<RequestInit, "headers"> & {
|
|
570
|
+
readonly headers?: Record<string, string>;
|
|
571
|
+
},
|
|
572
|
+
): Promise<Response> {
|
|
573
|
+
const adminUrl = runtime.ready.adminUrl;
|
|
574
|
+
const cookie = adminSessionCookies.get(adminUrl) ?? (await loginAdminSession(adminUrl));
|
|
575
|
+
const response = await fetch(new URL(path, `${adminUrl}/`), {
|
|
576
|
+
...init,
|
|
577
|
+
headers: {
|
|
578
|
+
...(init?.headers ?? {}),
|
|
579
|
+
cookie,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
if (response.status !== 401) {
|
|
583
|
+
return response;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const refreshedCookie = await loginAdminSession(adminUrl);
|
|
587
|
+
return fetch(new URL(path, `${adminUrl}/`), {
|
|
588
|
+
...init,
|
|
589
|
+
headers: {
|
|
590
|
+
...(init?.headers ?? {}),
|
|
591
|
+
cookie: refreshedCookie,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
539
596
|
async function readAdminJson<T>(
|
|
540
597
|
runtime: SmokeRuntimeHandle,
|
|
541
598
|
path: string,
|
|
@@ -543,7 +600,7 @@ async function readAdminJson<T>(
|
|
|
543
600
|
safeParse(value: unknown): { success: boolean; data?: T };
|
|
544
601
|
},
|
|
545
602
|
): Promise<T> {
|
|
546
|
-
const response = await
|
|
603
|
+
const response = await fetchAdmin(runtime, path);
|
|
547
604
|
if (!response.ok) {
|
|
548
605
|
throw new Error(`Admin request ${path} failed with HTTP ${response.status}.`);
|
|
549
606
|
}
|
|
@@ -556,7 +613,7 @@ async function readAdminJson<T>(
|
|
|
556
613
|
}
|
|
557
614
|
|
|
558
615
|
async function readAdminHealth(runtime: SmokeRuntimeHandle, path: "/healthz" | "/readyz") {
|
|
559
|
-
const response = await
|
|
616
|
+
const response = await fetchAdmin(runtime, path);
|
|
560
617
|
const payload = (await response.json()) as AdminHealthPayload;
|
|
561
618
|
return { response, payload };
|
|
562
619
|
}
|
|
@@ -565,7 +622,7 @@ async function postAdminCommand(
|
|
|
565
622
|
runtime: SmokeRuntimeHandle,
|
|
566
623
|
payload: Readonly<Record<string, unknown>>,
|
|
567
624
|
): Promise<CommandResult> {
|
|
568
|
-
const response = await
|
|
625
|
+
const response = await fetchAdmin(runtime, "/commands", {
|
|
569
626
|
method: "POST",
|
|
570
627
|
headers: {
|
|
571
628
|
"content-type": "application/json",
|
|
@@ -1466,125 +1523,144 @@ describeIfTcp("executable methods integration", () => {
|
|
|
1466
1523
|
}
|
|
1467
1524
|
}, 45000);
|
|
1468
1525
|
|
|
1469
|
-
test("admin HTTP
|
|
1526
|
+
test("admin HTTP auth guards protected endpoints and login session unlocks state and commands", async () => {
|
|
1470
1527
|
const runtime = await createSmokeRuntime();
|
|
1471
|
-
let
|
|
1472
|
-
let stage = "connect MQTT observer";
|
|
1528
|
+
let stage = "verify unauthorized access is rejected";
|
|
1473
1529
|
|
|
1474
1530
|
try {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
client = connect(ready.mqttBrokerUrl, { reconnectPeriod: 0 });
|
|
1531
|
+
const unauthorizedState = await fetch(new URL("/state", `${runtime.ready.adminUrl}/`));
|
|
1532
|
+
expect(unauthorizedState.status).toBe(401);
|
|
1478
1533
|
|
|
1479
|
-
await
|
|
1480
|
-
|
|
1534
|
+
const unauthorizedCommand = await fetch(new URL("/commands", `${runtime.ready.adminUrl}/`), {
|
|
1535
|
+
method: "POST",
|
|
1536
|
+
headers: { "content-type": "application/json" },
|
|
1537
|
+
body: JSON.stringify({ type: "pause" }),
|
|
1538
|
+
});
|
|
1539
|
+
expect(unauthorizedCommand.status).toBe(401);
|
|
1481
1540
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
stage = "read admin metrics and state";
|
|
1493
|
-
const initialMetrics = await readAdminJson(
|
|
1494
|
-
runtime,
|
|
1495
|
-
"/metrics",
|
|
1496
|
-
EngineMetricsSnapshotSchema,
|
|
1497
|
-
);
|
|
1498
|
-
const initialState = await readAdminJson(runtime, "/state", EngineStateSnapshotSchema);
|
|
1499
|
-
expect(initialMetrics.simulationName).toBe(ready.simulationName);
|
|
1500
|
-
expect(initialMetrics.core.state).toBe("running");
|
|
1501
|
-
expect(initialState.simulationName).toBe(ready.simulationName);
|
|
1502
|
-
expect(initialState.instances).toHaveLength(1);
|
|
1503
|
-
expect(initialState.config.adapters).toContain("mqtt");
|
|
1504
|
-
expect(initialState.config.adapters).toContain("opcua");
|
|
1505
|
-
|
|
1506
|
-
stage = "reject malformed admin command";
|
|
1507
|
-
const invalidResponse = await fetch(new URL("/commands", `${ready.adminUrl}/`), {
|
|
1508
|
-
method: "POST",
|
|
1509
|
-
headers: { "content-type": "application/json" },
|
|
1510
|
-
body: JSON.stringify({ type: "setSpeed" }),
|
|
1511
|
-
});
|
|
1512
|
-
expect(invalidResponse.status).toBe(400);
|
|
1541
|
+
stage = "log in and use session cookie on protected endpoints";
|
|
1542
|
+
const cookie = await loginAdminSession(runtime.ready.adminUrl);
|
|
1543
|
+
const authorizedState = await fetch(new URL("/state", `${runtime.ready.adminUrl}/`), {
|
|
1544
|
+
headers: { cookie },
|
|
1545
|
+
});
|
|
1546
|
+
expect(authorizedState.status).toBe(200);
|
|
1547
|
+
const authorizedStatePayload = EngineStateSnapshotSchema.safeParse(
|
|
1548
|
+
(await authorizedState.json()) as unknown,
|
|
1549
|
+
);
|
|
1550
|
+
expect(authorizedStatePayload.success).toBe(true);
|
|
1513
1551
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1552
|
+
const pauseResult = await fetch(new URL("/commands", `${runtime.ready.adminUrl}/`), {
|
|
1553
|
+
method: "POST",
|
|
1554
|
+
headers: {
|
|
1555
|
+
"content-type": "application/json",
|
|
1556
|
+
cookie,
|
|
1557
|
+
},
|
|
1558
|
+
body: JSON.stringify({ type: "pause" }),
|
|
1559
|
+
});
|
|
1560
|
+
expect(pauseResult.status).toBe(200);
|
|
1561
|
+
const pausePayload = (await pauseResult.json()) as CommandResult;
|
|
1562
|
+
expect(pausePayload.success).toBe(true);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
await stopSmokeRuntimeWithFailure(
|
|
1565
|
+
runtime,
|
|
1566
|
+
new Error(
|
|
1567
|
+
`Admin auth integration failure during "${stage}": ${error instanceof Error ? error.message : String(error)}`,
|
|
1568
|
+
),
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1528
1571
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
expect(pauseResult.success).toBe(true);
|
|
1532
|
-
expect((await waitForAdminMetricsState(runtime, "paused")).core.state).toBe("paused");
|
|
1533
|
-
const pausedReady = await readAdminHealth(runtime, "/readyz");
|
|
1534
|
-
expect(pausedReady.response.status).toBe(503);
|
|
1535
|
-
expect(pausedReady.payload.status).toBe("fail");
|
|
1536
|
-
|
|
1537
|
-
const resumeResult = await postAdminCommand(runtime, { type: "resume" });
|
|
1538
|
-
expect(resumeResult.success).toBe(true);
|
|
1539
|
-
expect((await waitForAdminMetricsState(runtime, "running")).core.state).toBe("running");
|
|
1540
|
-
const resumedReady = await readAdminHealth(runtime, "/readyz");
|
|
1541
|
-
expect(resumedReady.response.status).toBe(200);
|
|
1542
|
-
expect(resumedReady.payload.status).toBe("ok");
|
|
1543
|
-
|
|
1544
|
-
stage = "change simulation speed through admin server";
|
|
1545
|
-
const speedResult = await postAdminCommand(runtime, { type: "setSpeed", multiplier: 5 });
|
|
1546
|
-
expect(speedResult.success).toBe(true);
|
|
1547
|
-
const updatedState = await waitForAsync("admin time multiplier 5", async () => {
|
|
1548
|
-
const state = await readAdminJson(runtime, "/state", EngineStateSnapshotSchema);
|
|
1549
|
-
return state.config.timeMultiplier === 5 ? state : undefined;
|
|
1550
|
-
});
|
|
1551
|
-
expect(updatedState.config.timeMultiplier).toBe(5);
|
|
1572
|
+
await stopSmokeRuntime(runtime);
|
|
1573
|
+
}, 30000);
|
|
1552
1574
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1575
|
+
test("admin HTTP server exposes runtime state and executes core commands", async () => {
|
|
1576
|
+
const runtime = await createSmokeRuntime();
|
|
1577
|
+
let stage = "read admin health and readiness";
|
|
1578
|
+
|
|
1579
|
+
try {
|
|
1580
|
+
const { ready } = runtime;
|
|
1581
|
+
const health = await readAdminHealth(runtime, "/healthz");
|
|
1582
|
+
const readyz = await readAdminHealth(runtime, "/readyz");
|
|
1583
|
+
expect(health.response.status).toBe(200);
|
|
1584
|
+
expect(health.payload.status).toBe("ok");
|
|
1585
|
+
expect(readyz.response.status).toBe(200);
|
|
1586
|
+
expect(readyz.payload.status).toBe("ok");
|
|
1587
|
+
|
|
1588
|
+
stage = "read admin metrics and state";
|
|
1589
|
+
const initialMetrics = await readAdminJson(runtime, "/metrics", EngineMetricsSnapshotSchema);
|
|
1590
|
+
const initialState = await readAdminJson(runtime, "/state", EngineStateSnapshotSchema);
|
|
1591
|
+
expect(initialMetrics.simulationName).toBe(ready.simulationName);
|
|
1592
|
+
expect(initialMetrics.core.state).toBe("running");
|
|
1593
|
+
expect(initialState.simulationName).toBe(ready.simulationName);
|
|
1594
|
+
expect(initialState.instances).toHaveLength(1);
|
|
1595
|
+
expect(initialState.config.adapters).toContain("mqtt");
|
|
1596
|
+
expect(initialState.config.adapters).toContain("opcua");
|
|
1597
|
+
|
|
1598
|
+
stage = "reject malformed admin command";
|
|
1599
|
+
const invalidResponse = await fetchAdmin(runtime, "/commands", {
|
|
1600
|
+
method: "POST",
|
|
1601
|
+
headers: { "content-type": "application/json" },
|
|
1602
|
+
body: JSON.stringify({ type: "setSpeed" }),
|
|
1603
|
+
});
|
|
1604
|
+
expect(invalidResponse.status).toBe(400);
|
|
1605
|
+
|
|
1606
|
+
stage = "invoke method through admin server";
|
|
1607
|
+
const startResult = await postAdminCommand(runtime, {
|
|
1608
|
+
type: "invokeMethod",
|
|
1609
|
+
instanceId: ready.instanceId,
|
|
1610
|
+
method: "Start",
|
|
1611
|
+
});
|
|
1612
|
+
expect(startResult.success).toBe(true);
|
|
1613
|
+
|
|
1614
|
+
stage = "pause and resume through admin server";
|
|
1615
|
+
const pauseResult = await postAdminCommand(runtime, { type: "pause" });
|
|
1616
|
+
expect(pauseResult.success).toBe(true);
|
|
1617
|
+
expect((await waitForAdminMetricsState(runtime, "paused")).core.state).toBe("paused");
|
|
1618
|
+
const pausedReady = await readAdminHealth(runtime, "/readyz");
|
|
1619
|
+
expect(pausedReady.response.status).toBe(503);
|
|
1620
|
+
expect(pausedReady.payload.status).toBe("fail");
|
|
1621
|
+
|
|
1622
|
+
const resumeResult = await postAdminCommand(runtime, { type: "resume" });
|
|
1623
|
+
expect(resumeResult.success).toBe(true);
|
|
1624
|
+
expect((await waitForAdminMetricsState(runtime, "running")).core.state).toBe("running");
|
|
1625
|
+
const resumedReady = await readAdminHealth(runtime, "/readyz");
|
|
1626
|
+
expect(resumedReady.response.status).toBe(200);
|
|
1627
|
+
expect(resumedReady.payload.status).toBe("ok");
|
|
1628
|
+
|
|
1629
|
+
stage = "change simulation speed through admin server";
|
|
1630
|
+
const speedResult = await postAdminCommand(runtime, { type: "setSpeed", multiplier: 5 });
|
|
1631
|
+
expect(speedResult.success).toBe(true);
|
|
1632
|
+
const updatedState = await waitForAsync("admin time multiplier 5", async () => {
|
|
1633
|
+
const state = await readAdminJson(runtime, "/state", EngineStateSnapshotSchema);
|
|
1634
|
+
return state.config.timeMultiplier === 5 ? state : undefined;
|
|
1635
|
+
});
|
|
1636
|
+
expect(updatedState.config.timeMultiplier).toBe(5);
|
|
1637
|
+
|
|
1638
|
+
stage = "spawn and remove instances through admin server";
|
|
1639
|
+
const spawnResult = await postAdminCommand(runtime, {
|
|
1640
|
+
type: "spawn",
|
|
1641
|
+
model: "method-smoke-pump",
|
|
1642
|
+
count: 1,
|
|
1643
|
+
variation: 0,
|
|
1644
|
+
unsBasePath: "method-smoke/validation/protocols",
|
|
1645
|
+
});
|
|
1646
|
+
expect(spawnResult.success).toBe(true);
|
|
1647
|
+
const spawnedIds = Array.isArray(spawnResult.data)
|
|
1648
|
+
? spawnResult.data.filter((value): value is string => typeof value === "string")
|
|
1649
|
+
: [];
|
|
1650
|
+
expect(spawnedIds).toHaveLength(1);
|
|
1651
|
+
const spawnedState = await waitForAdminInstanceCount(runtime, 2);
|
|
1652
|
+
expect(spawnedState.instances.some((instance) => instance.id === spawnedIds[0])).toBe(true);
|
|
1653
|
+
|
|
1654
|
+
const removeResult = await postAdminCommand(runtime, {
|
|
1655
|
+
type: "removeInstance",
|
|
1656
|
+
instanceId: spawnedIds[0],
|
|
1657
|
+
});
|
|
1658
|
+
expect(removeResult.success).toBe(true);
|
|
1659
|
+
const removedState = await waitForAdminInstanceCount(runtime, 1);
|
|
1660
|
+
expect(removedState.instances.some((instance) => instance.id === spawnedIds[0])).toBe(false);
|
|
1581
1661
|
|
|
1582
|
-
await stopMQTTClient(client);
|
|
1583
1662
|
await stopSmokeRuntime(runtime);
|
|
1584
1663
|
} catch (error) {
|
|
1585
|
-
if (client) {
|
|
1586
|
-
await stopMQTTClient(client).catch(() => {});
|
|
1587
|
-
}
|
|
1588
1664
|
await stopSmokeRuntimeWithFailure(
|
|
1589
1665
|
runtime,
|
|
1590
1666
|
new Error(
|
|
@@ -1994,6 +2070,8 @@ describeIfTcp("executable methods integration", () => {
|
|
|
1994
2070
|
templateSettings: { tick_interval_ms: 100, time_multiplier: 60, seed: 42 },
|
|
1995
2071
|
});
|
|
1996
2072
|
let client: MqttClient | undefined;
|
|
2073
|
+
let opcuaClient: ReturnType<typeof OPCUAClient.create> | undefined;
|
|
2074
|
+
let session: ClientSession | undefined;
|
|
1997
2075
|
let stage = "connect MQTT observer";
|
|
1998
2076
|
|
|
1999
2077
|
try {
|
|
@@ -2002,20 +2080,20 @@ describeIfTcp("executable methods integration", () => {
|
|
|
2002
2080
|
client = connect(ready.mqttBrokerUrl, { reconnectPeriod: 0 });
|
|
2003
2081
|
await waitForMQTTConnect(client);
|
|
2004
2082
|
await subscribeMQTT(client, [`${topicPrefix}/#`]);
|
|
2083
|
+
opcuaClient = OPCUAClient.create({
|
|
2084
|
+
securityMode: MessageSecurityMode.None,
|
|
2085
|
+
securityPolicy: SecurityPolicy.None,
|
|
2086
|
+
endpointMustExist: false,
|
|
2087
|
+
});
|
|
2088
|
+
await opcuaClient.connect(ready.opcuaEndpointUrl);
|
|
2089
|
+
session = await opcuaClient.createSession();
|
|
2005
2090
|
|
|
2006
2091
|
const recorder = createMQTTRecorder(client);
|
|
2007
2092
|
try {
|
|
2008
|
-
// The scheduler
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
|
|
2012
|
-
// matches the critical-stage effect (multiplier 0.5, offset -20):
|
|
2013
|
-
// pressure = 100 * 0.5 - 20 = 30.
|
|
2014
|
-
stage = "wait for critical stage (pressure = 100 * 0.5 - 20 = 30)";
|
|
2015
|
-
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/pressure`, 30, 15000)).toBe(
|
|
2016
|
-
30,
|
|
2017
|
-
);
|
|
2018
|
-
|
|
2093
|
+
// The scheduler may reach critical before a late MQTT subscriber gets a
|
|
2094
|
+
// pressure update when retain is disabled. Verify pressure over OPC UA,
|
|
2095
|
+
// which reads current runtime state directly.
|
|
2096
|
+
stage = "wait for critical stage";
|
|
2019
2097
|
const criticalMetrics = await waitForAsync(
|
|
2020
2098
|
"fleet lifecycle critical",
|
|
2021
2099
|
async () => {
|
|
@@ -2025,15 +2103,29 @@ describeIfTcp("executable methods integration", () => {
|
|
|
2025
2103
|
15000,
|
|
2026
2104
|
);
|
|
2027
2105
|
expect(criticalMetrics.fleet.lifecycleBreakdown["critical"]).toBe(1);
|
|
2106
|
+
stage = "verify critical pressure";
|
|
2107
|
+
expect(await waitForOPCUAValue<number>(session, `ns=1;s=${topicPrefix}/pressure`, 30)).toBe(
|
|
2108
|
+
30,
|
|
2109
|
+
);
|
|
2028
2110
|
// Verify lifecycle effects applied correctly: multiplier then offset
|
|
2029
2111
|
expect(criticalMetrics.fleet.activeFaultCount).toBe(0);
|
|
2030
2112
|
} finally {
|
|
2031
2113
|
recorder.dispose();
|
|
2032
2114
|
}
|
|
2033
2115
|
|
|
2116
|
+
if (session) {
|
|
2117
|
+
await session.close();
|
|
2118
|
+
session = undefined;
|
|
2119
|
+
}
|
|
2120
|
+
await opcuaClient.disconnect();
|
|
2121
|
+
opcuaClient = undefined;
|
|
2034
2122
|
await stopMQTTClient(client);
|
|
2035
2123
|
await stopSmokeRuntime(runtime);
|
|
2036
2124
|
} catch (error) {
|
|
2125
|
+
if (session) {
|
|
2126
|
+
await session.close().catch(() => {});
|
|
2127
|
+
}
|
|
2128
|
+
await opcuaClient?.disconnect().catch(() => {});
|
|
2037
2129
|
if (client) {
|
|
2038
2130
|
await stopMQTTClient(client).catch(() => {});
|
|
2039
2131
|
}
|