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.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/console-dist/assets/activity-DsWeRq4h.js +1 -0
  3. package/console-dist/assets/api-4AqKvsm9.js +1 -0
  4. package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
  5. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
  6. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
  7. package/console-dist/assets/box-C1WaoOMF.js +1 -0
  8. package/console-dist/assets/card-BjeP32PN.js +1 -0
  9. package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
  10. package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
  11. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
  12. package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
  13. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
  14. package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
  15. package/console-dist/assets/control-CPhvwx9o.js +1 -0
  16. package/console-dist/assets/control-Cg2up9NA.js +1 -0
  17. package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
  18. package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
  19. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
  20. package/console-dist/assets/dist-BhbDML0a.js +1 -0
  21. package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
  22. package/console-dist/assets/download-DQ6NH16U.js +1 -0
  23. package/console-dist/assets/engine-store-CrLnz692.js +1 -0
  24. package/console-dist/assets/events-CXm29Ok4.js +1 -0
  25. package/console-dist/assets/events-D01IxhnP.js +1 -0
  26. package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
  27. package/console-dist/assets/fleet-lotMGVqe.js +1 -0
  28. package/console-dist/assets/format-kXTonpDJ.js +1 -0
  29. package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
  30. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
  31. package/console-dist/assets/index-CVPKSDvJ.js +12 -0
  32. package/console-dist/assets/index-DJkgDeun.css +2 -0
  33. package/console-dist/assets/input-BWEQFptx.js +1 -0
  34. package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
  35. package/console-dist/assets/label-uephX5ZG.js +1 -0
  36. package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
  37. package/console-dist/assets/login-C3RJli0m.js +126 -0
  38. package/console-dist/assets/models-B_UioWQ5.js +1 -0
  39. package/console-dist/assets/models-_IPlPiZ7.js +1 -0
  40. package/console-dist/assets/not-found-B8gAm08S.js +1 -0
  41. package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
  42. package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
  43. package/console-dist/assets/platforms-DStNbtVc.js +1 -0
  44. package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
  45. package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
  46. package/console-dist/assets/radio-CoHegOv5.js +1 -0
  47. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
  48. package/console-dist/assets/select-CeO4vP0G.js +1 -0
  49. package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
  50. package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
  51. package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
  52. package/console-dist/assets/tags-CiXOIHtY.js +4 -0
  53. package/console-dist/assets/tags-FQCXIxrP.js +1 -0
  54. package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
  55. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
  56. package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
  57. package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
  58. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
  59. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
  60. package/console-dist/assets/validation-Blvud9_O.js +1 -0
  61. package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
  62. package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
  63. package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
  64. package/console-dist/index.html +70 -2
  65. package/package.json +9 -8
  66. package/src/__tests__/integration/executable-methods-runner.ts +7 -6
  67. package/src/__tests__/integration/executable-methods.integration.ts +214 -122
  68. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
  69. package/src/commands/adapter.ts +180 -7
  70. package/src/commands/apply.test.ts +62 -0
  71. package/src/commands/apply.ts +216 -0
  72. package/src/commands/cleanup.test.ts +34 -0
  73. package/src/commands/cleanup.ts +201 -0
  74. package/src/commands/export.test.ts +51 -0
  75. package/src/commands/export.ts +404 -0
  76. package/src/commands/fork.ts +2 -2
  77. package/src/commands/inject.ts +2 -2
  78. package/src/commands/load.ts +1 -1
  79. package/src/commands/logs.ts +13 -15
  80. package/src/commands/models.test.ts +1 -1
  81. package/src/commands/models.ts +1 -1
  82. package/src/commands/pause.ts +3 -3
  83. package/src/commands/platforms.test.ts +186 -0
  84. package/src/commands/platforms.ts +371 -0
  85. package/src/commands/remove.ts +1 -1
  86. package/src/commands/reset.ts +2 -2
  87. package/src/commands/spawn.ts +2 -2
  88. package/src/commands/speed.ts +2 -2
  89. package/src/commands/start/adapter-config.ts +157 -0
  90. package/src/commands/start/catalog-le-dt.test.ts +50 -0
  91. package/src/commands/start/catalog-le-dt.ts +187 -0
  92. package/src/commands/start/catalog-model-schema.ts +89 -0
  93. package/src/commands/start/catalog-template-detail.ts +54 -0
  94. package/src/commands/start/daemon.ts +255 -0
  95. package/src/commands/start/display.ts +150 -0
  96. package/src/commands/start/engine-setup.ts +247 -0
  97. package/src/commands/start/events.ts +126 -0
  98. package/src/commands/start/index.ts +897 -0
  99. package/src/commands/start/protocol-plans.ts +239 -0
  100. package/src/commands/start/template-loader.ts +197 -0
  101. package/src/commands/start/template-resolver.ts +185 -0
  102. package/src/commands/start/types.ts +73 -0
  103. package/src/commands/start.test.ts +137 -16
  104. package/src/commands/status.ts +1 -1
  105. package/src/commands/stop.ts +19 -8
  106. package/src/commands/templates.test.ts +1 -1
  107. package/src/commands/templates.ts +1 -3
  108. package/src/index.ts +11 -3
  109. package/src/lib/admin-auth.ts +157 -0
  110. package/src/lib/admin-client.test.ts +31 -15
  111. package/src/lib/admin-client.ts +106 -6
  112. package/src/lib/admin-platform-routes.ts +1017 -0
  113. package/src/lib/admin-platform-stream.test.ts +434 -0
  114. package/src/lib/admin-platform-stream.ts +788 -0
  115. package/src/lib/admin-responses.ts +211 -0
  116. package/src/lib/admin-server.test.ts +274 -55
  117. package/src/lib/admin-server.ts +387 -248
  118. package/src/lib/auth-store.ts +87 -0
  119. package/src/lib/daemon-registry.ts +12 -14
  120. package/src/lib/le/le-apply.test.ts +591 -0
  121. package/src/lib/le/le-apply.ts +832 -0
  122. package/src/lib/le/le-auth.ts +89 -0
  123. package/src/lib/le/le-cleanup.test.ts +168 -0
  124. package/src/lib/le/le-cleanup.ts +399 -0
  125. package/src/lib/le/le-cli-helpers.ts +77 -0
  126. package/src/lib/le/le-client.ts +184 -0
  127. package/src/lib/le/le-concurrency.test.ts +157 -0
  128. package/src/lib/le/le-concurrency.ts +78 -0
  129. package/src/lib/le/le-connection-test.ts +42 -0
  130. package/src/lib/le/le-devicehub-properties.test.ts +53 -0
  131. package/src/lib/le/le-devicehub-properties.ts +33 -0
  132. package/src/lib/le/le-devicehub.ts +213 -0
  133. package/src/lib/le/le-digital-twins.ts +285 -0
  134. package/src/lib/le/le-drivers.ts +23 -0
  135. package/src/lib/le/le-endpoint.ts +40 -0
  136. package/src/lib/le/le-environments.test.ts +73 -0
  137. package/src/lib/le/le-environments.ts +156 -0
  138. package/src/lib/le/le-markers.ts +87 -0
  139. package/src/lib/le/le-plan.test.ts +26 -0
  140. package/src/lib/le/le-plan.ts +109 -0
  141. package/src/lib/le/le-platform-operations.test.ts +198 -0
  142. package/src/lib/le/le-platform-operations.ts +340 -0
  143. package/src/lib/le/le-static-model-schema.ts +78 -0
  144. package/src/lib/le/le-static-planner.test.ts +255 -0
  145. package/src/lib/le/le-static-planner.ts +442 -0
  146. package/src/lib/le/le-template-exporter.test.ts +527 -0
  147. package/src/lib/le/le-template-exporter.ts +459 -0
  148. package/src/lib/le/le-value-types.ts +23 -0
  149. package/src/lib/mqtt-client.ts +1 -2
  150. package/src/lib/resolve-simulation.ts +1 -6
  151. package/src/lib/session-store.ts +110 -0
  152. package/src/lib/shutdown.test.ts +184 -2
  153. package/src/lib/shutdown.ts +104 -34
  154. package/src/lib/template-category.ts +3 -3
  155. package/src/lib/template-locator.ts +8 -2
  156. package/src/lib/ws-hub.ts +72 -46
  157. package/src/logger.ts +39 -14
  158. package/src/output.ts +3 -0
  159. package/console-dist/assets/index-C3XMYq26.css +0 -2
  160. package/console-dist/assets/index-hUJSpKtB.js +0 -119
  161. package/src/commands/start.ts +0 -2023
@@ -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-hUJSpKtB.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C3XMYq26.css">
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.4.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.4.0",
36
- "@miravo/mqtt": "0.4.0",
37
- "@miravo/opcua": "0.4.0",
38
- "@miravo/shared": "0.4.0",
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": "^4.3.6"
44
+ "zod": "^3.24.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@miravo/console": "0.0.0",
47
- "node-opcua-client": "^2.163.1",
48
- "node-opcua-variant": "^2.163.0"
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
- getEngineState: () => engine.getMetrics().core.state,
198
- getTickInfo: () => ({
199
- tick: engine.getMetrics().core.tickCount,
200
- simulationTime: engine.getMetrics().core.simulationTimeSeconds,
201
- instanceCount: engine.getMetrics().core.instanceCount,
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 fetch(new URL(path, `${runtime.ready.adminUrl}/`));
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 fetch(new URL(path, `${runtime.ready.adminUrl}/`));
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 fetch(new URL("/commands", `${runtime.ready.adminUrl}/`), {
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 server exposes runtime state, executes core commands, and drives published twin behavior", async () => {
1526
+ test("admin HTTP auth guards protected endpoints and login session unlocks state and commands", async () => {
1470
1527
  const runtime = await createSmokeRuntime();
1471
- let client: MqttClient | undefined;
1472
- let stage = "connect MQTT observer";
1528
+ let stage = "verify unauthorized access is rejected";
1473
1529
 
1474
1530
  try {
1475
- const { ready } = runtime;
1476
- const topicPrefix = ready.instancePath;
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 waitForMQTTConnect(client);
1480
- await subscribeMQTT(client, [`${topicPrefix}/#`]);
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
- const recorder = createMQTTRecorder(client);
1483
- try {
1484
- stage = "read admin health and readiness";
1485
- const health = await readAdminHealth(runtime, "/healthz");
1486
- const readyz = await readAdminHealth(runtime, "/readyz");
1487
- expect(health.response.status).toBe(200);
1488
- expect(health.payload.status).toBe("ok");
1489
- expect(readyz.response.status).toBe(200);
1490
- expect(readyz.payload.status).toBe("ok");
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
- stage = "invoke method through admin server";
1515
- const startResult = await postAdminCommand(runtime, {
1516
- type: "invokeMethod",
1517
- instanceId: ready.instanceId,
1518
- method: "Start",
1519
- });
1520
- expect(startResult.success).toBe(true);
1521
- expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/run_command`, true)).toBe(
1522
- true,
1523
- );
1524
- expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/speed_feedback`, 1200)).toBe(
1525
- 1200,
1526
- );
1527
- expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/running`, true)).toBe(true);
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
- stage = "pause and resume through admin server";
1530
- const pauseResult = await postAdminCommand(runtime, { type: "pause" });
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
- stage = "spawn and remove instances through admin server";
1554
- const spawnResult = await postAdminCommand(runtime, {
1555
- type: "spawn",
1556
- model: "method-smoke-pump",
1557
- count: 1,
1558
- variation: 0,
1559
- unsBasePath: "method-smoke/validation/protocols",
1560
- });
1561
- expect(spawnResult.success).toBe(true);
1562
- const spawnedIds = Array.isArray(spawnResult.data)
1563
- ? spawnResult.data.filter((value): value is string => typeof value === "string")
1564
- : [];
1565
- expect(spawnedIds).toHaveLength(1);
1566
- const spawnedState = await waitForAdminInstanceCount(runtime, 2);
1567
- expect(spawnedState.instances.some((instance) => instance.id === spawnedIds[0])).toBe(true);
1568
-
1569
- const removeResult = await postAdminCommand(runtime, {
1570
- type: "removeInstance",
1571
- instanceId: spawnedIds[0],
1572
- });
1573
- expect(removeResult.success).toBe(true);
1574
- const removedState = await waitForAdminInstanceCount(runtime, 1);
1575
- expect(removedState.instances.some((instance) => instance.id === spawnedIds[0])).toBe(
1576
- false,
1577
- );
1578
- } finally {
1579
- recorder.dispose();
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 has been ticking since engine.start(). With fast
2009
- // time_multiplier the lifecycle may have already advanced past healthy
2010
- // by the time we connect. Instead of racing to catch each stage,
2011
- // verify that lifecycle reaches critical and that the pressure value
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
  }