miravo 0.6.0 → 0.6.2

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 (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,123 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { InvalidArgumentError } from "commander";
3
- import {
4
- parseDurationMs,
5
- parseNonNegativeInt,
6
- parsePort,
7
- parsePositiveFloat,
8
- parsePositiveInt,
9
- } from "./parse.ts";
10
-
11
- describe("parsePositiveInt", () => {
12
- test("parses valid positive integers", () => {
13
- expect(parsePositiveInt("1")).toBe(1);
14
- expect(parsePositiveInt("42")).toBe(42);
15
- expect(parsePositiveInt("1883")).toBe(1883);
16
- });
17
-
18
- test("rejects zero", () => {
19
- expect(() => parsePositiveInt("0")).toThrow(InvalidArgumentError);
20
- });
21
-
22
- test("rejects negative numbers", () => {
23
- expect(() => parsePositiveInt("-5")).toThrow(InvalidArgumentError);
24
- });
25
-
26
- test("rejects non-numeric strings", () => {
27
- expect(() => parsePositiveInt("abc")).toThrow(InvalidArgumentError);
28
- expect(() => parsePositiveInt("")).toThrow(InvalidArgumentError);
29
- });
30
-
31
- test("rejects decimals and partial strings", () => {
32
- expect(() => parsePositiveInt("1.5")).toThrow(InvalidArgumentError);
33
- expect(() => parsePositiveInt("42.9")).toThrow(InvalidArgumentError);
34
- expect(() => parsePositiveInt("42ms")).toThrow(InvalidArgumentError);
35
- expect(() => parsePositiveInt("1e9")).toThrow(InvalidArgumentError);
36
- expect(() => parsePositiveInt("1.5abc")).toThrow(InvalidArgumentError);
37
- });
38
-
39
- test("rejects Infinity", () => {
40
- expect(() => parsePositiveInt("Infinity")).toThrow(InvalidArgumentError);
41
- });
42
- });
43
-
44
- describe("parseNonNegativeInt", () => {
45
- test("accepts zero", () => {
46
- expect(parseNonNegativeInt("0")).toBe(0);
47
- });
48
-
49
- test("accepts positive integers", () => {
50
- expect(parseNonNegativeInt("42")).toBe(42);
51
- });
52
-
53
- test("rejects negative numbers", () => {
54
- expect(() => parseNonNegativeInt("-1")).toThrow(InvalidArgumentError);
55
- });
56
-
57
- test("rejects non-numeric strings", () => {
58
- expect(() => parseNonNegativeInt("abc")).toThrow(InvalidArgumentError);
59
- });
60
-
61
- test("rejects partial strings and scientific notation", () => {
62
- expect(() => parseNonNegativeInt("42ms")).toThrow(InvalidArgumentError);
63
- expect(() => parseNonNegativeInt("1e9")).toThrow(InvalidArgumentError);
64
- expect(() => parseNonNegativeInt("1.5abc")).toThrow(InvalidArgumentError);
65
- });
66
- });
67
-
68
- describe("parsePositiveFloat", () => {
69
- test("parses valid positive floats", () => {
70
- expect(parsePositiveFloat("1.5")).toBe(1.5);
71
- expect(parsePositiveFloat("0.1")).toBe(0.1);
72
- expect(parsePositiveFloat("10")).toBe(10);
73
- expect(parsePositiveFloat("1e3")).toBe(1000);
74
- });
75
-
76
- test("rejects zero", () => {
77
- expect(() => parsePositiveFloat("0")).toThrow(InvalidArgumentError);
78
- });
79
-
80
- test("rejects negative numbers", () => {
81
- expect(() => parsePositiveFloat("-0.5")).toThrow(InvalidArgumentError);
82
- });
83
-
84
- test("rejects non-numeric strings", () => {
85
- expect(() => parsePositiveFloat("abc")).toThrow(InvalidArgumentError);
86
- });
87
-
88
- test("rejects Infinity", () => {
89
- expect(() => parsePositiveFloat("Infinity")).toThrow(InvalidArgumentError);
90
- });
91
- });
92
-
93
- describe("parsePort", () => {
94
- test("accepts valid ports", () => {
95
- expect(parsePort("1")).toBe(1);
96
- expect(parsePort("80")).toBe(80);
97
- expect(parsePort("1883")).toBe(1883);
98
- expect(parsePort("65535")).toBe(65535);
99
- });
100
-
101
- test("rejects invalid ports", () => {
102
- expect(() => parsePort("0")).toThrow(InvalidArgumentError);
103
- expect(() => parsePort("-1")).toThrow(InvalidArgumentError);
104
- expect(() => parsePort("65536")).toThrow(InvalidArgumentError);
105
- expect(() => parsePort("abc")).toThrow(InvalidArgumentError);
106
- expect(() => parsePort("1.5")).toThrow(InvalidArgumentError);
107
- });
108
- });
109
-
110
- describe("parseDurationMs", () => {
111
- test("accepts positive durations up to one hour", () => {
112
- expect(parseDurationMs("1")).toBe(1);
113
- expect(parseDurationMs("5000")).toBe(5000);
114
- expect(parseDurationMs("3600000")).toBe(3_600_000);
115
- });
116
-
117
- test("rejects out-of-range durations", () => {
118
- expect(() => parseDurationMs("0")).toThrow(InvalidArgumentError);
119
- expect(() => parseDurationMs("3600001")).toThrow(InvalidArgumentError);
120
- expect(() => parseDurationMs("-1")).toThrow(InvalidArgumentError);
121
- expect(() => parseDurationMs("1.5")).toThrow(InvalidArgumentError);
122
- });
123
- });
package/src/lib/parse.ts DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * CLI option parsers that throw Commander's InvalidArgumentError on bad input.
3
- * Commander catches these and shows a user-friendly error with the flag name.
4
- */
5
- import { InvalidArgumentError } from "commander";
6
-
7
- /** Maximum allowed millisecond duration for CLI timer options (1 hour). */
8
- const MAX_DURATION_MS = 3_600_000;
9
-
10
- function parseStrictUnsignedInteger(value: string, label: string): number {
11
- const trimmed = value.trim();
12
- if (!/^\d+$/.test(trimmed)) {
13
- throw new InvalidArgumentError(`Expected ${label}, got "${value}".`);
14
- }
15
-
16
- const n = Number(trimmed);
17
- if (!Number.isFinite(n) || !Number.isInteger(n)) {
18
- throw new InvalidArgumentError(`Expected ${label}, got "${value}".`);
19
- }
20
- return n;
21
- }
22
-
23
- /** Parse a string as a positive integer (> 0). */
24
- export function parsePositiveInt(value: string): number {
25
- const n = parseStrictUnsignedInteger(value, "a positive integer");
26
- if (n <= 0) {
27
- throw new InvalidArgumentError(`Expected a positive integer, got "${value}".`);
28
- }
29
- return n;
30
- }
31
-
32
- /** Parse a string as a non-negative integer (>= 0). Useful for seeds. */
33
- export function parseNonNegativeInt(value: string): number {
34
- const n = parseStrictUnsignedInteger(value, "a non-negative integer");
35
- if (n < 0) {
36
- throw new InvalidArgumentError(`Expected a non-negative integer, got "${value}".`);
37
- }
38
- return n;
39
- }
40
-
41
- /** Parse a string as a positive float (> 0). */
42
- export function parsePositiveFloat(value: string): number {
43
- const trimmed = value.trim();
44
- const n = Number(trimmed);
45
- if (trimmed.length === 0 || !Number.isFinite(n) || n <= 0) {
46
- throw new InvalidArgumentError(`Expected a positive number, got "${value}".`);
47
- }
48
- return n;
49
- }
50
-
51
- /** Parse a string as a valid network port (1–65535). */
52
- export function parsePort(value: string): number {
53
- const n = parseStrictUnsignedInteger(value, "a port number (1–65535)");
54
- if (n < 1 || n > 65_535) {
55
- throw new InvalidArgumentError(`Expected a port number (1–65535), got "${value}".`);
56
- }
57
- return n;
58
- }
59
-
60
- /** Parse a string as a positive millisecond duration up to 1 hour. */
61
- export function parseDurationMs(value: string): number {
62
- const n = parseStrictUnsignedInteger(value, `a duration in milliseconds (1–${MAX_DURATION_MS})`);
63
- if (n < 1 || n > MAX_DURATION_MS) {
64
- throw new InvalidArgumentError(
65
- `Expected a duration in milliseconds (1–${MAX_DURATION_MS}), got "${value}".`,
66
- );
67
- }
68
- return n;
69
- }
70
-
71
- /** Maximum allowed timeout in seconds (1 hour). */
72
- const MAX_TIMEOUT_SECONDS = 3_600;
73
-
74
- /**
75
- * Parse a string as a timeout in seconds and return milliseconds.
76
- * Accepts positive numbers (integers or decimals). The result is rounded to
77
- * the nearest millisecond.
78
- */
79
- export function parseTimeoutSeconds(value: string): number {
80
- const n = parsePositiveFloat(value);
81
- if (n > MAX_TIMEOUT_SECONDS) {
82
- throw new InvalidArgumentError(
83
- `Expected a timeout in seconds (max ${MAX_TIMEOUT_SECONDS}), got "${value}".`,
84
- );
85
- }
86
- return Math.round(n * 1_000);
87
- }
@@ -1,118 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- CONTROL_TOPIC_WILDCARD_METRICS,
4
- controlTopicCmd,
5
- controlTopicCmdResult,
6
- controlTopicMetrics,
7
- controlTopicState,
8
- } from "@miravo/shared";
9
- import type { SimulationResolverClient } from "./resolve-simulation.ts";
10
- import { discoverRunningSimulations, resolveTargetSimulation } from "./resolve-simulation.ts";
11
-
12
- type MessageHandler = Parameters<SimulationResolverClient["on"]>[1];
13
-
14
- class MockResolverClient implements SimulationResolverClient {
15
- readonly subscriptions = new Set<string>();
16
- #handlers = new Set<MessageHandler>();
17
-
18
- subscribe(topic: string, _options: { qos: 0 }, callback?: (error?: Error | null) => void): void {
19
- this.subscriptions.add(topic);
20
- callback?.(null);
21
- }
22
-
23
- unsubscribe(topic: string): void {
24
- this.subscriptions.delete(topic);
25
- }
26
-
27
- on(_event: "message", handler: MessageHandler): void {
28
- this.#handlers.add(handler);
29
- }
30
-
31
- off(_event: "message", handler: MessageHandler): void {
32
- this.#handlers.delete(handler);
33
- }
34
-
35
- emitMessage(topic: string, payload = "{}"): void {
36
- const buffer = Buffer.from(payload, "utf8");
37
- for (const handler of this.#handlers) {
38
- handler(topic, buffer);
39
- }
40
- }
41
- }
42
-
43
- describe("resolveTargetSimulation", () => {
44
- test("returns explicit --name directly without discovery", async () => {
45
- const client = new MockResolverClient();
46
- const resolved = await resolveTargetSimulation(client, {
47
- name: "north-plant",
48
- timeoutMs: 20,
49
- brokerUrl: "mqtt://127.0.0.1:1883",
50
- });
51
-
52
- expect(client.subscriptions.size).toBe(0);
53
- expect(resolved.simulationName).toBe("north-plant");
54
- expect(resolved.topics.metrics).toBe(controlTopicMetrics("north-plant"));
55
- expect(resolved.topics.state).toBe(controlTopicState("north-plant"));
56
- expect(resolved.topics.cmd).toBe(controlTopicCmd("north-plant"));
57
- expect(resolved.topics.cmdResult).toBe(controlTopicCmdResult("north-plant"));
58
- });
59
-
60
- test("fails when no simulation is discovered", async () => {
61
- const client = new MockResolverClient();
62
- await expect(
63
- resolveTargetSimulation(client, {
64
- timeoutMs: 10,
65
- brokerUrl: "mqtt://127.0.0.1:1883",
66
- }),
67
- ).rejects.toThrow("No running simulations found");
68
- });
69
-
70
- test("auto-targets when exactly one simulation is discovered", async () => {
71
- const client = new MockResolverClient();
72
- setTimeout(() => {
73
- client.emitMessage("$miravo/north-plant/metrics", '{"ok":true}');
74
- }, 2);
75
-
76
- const resolved = await resolveTargetSimulation(client, {
77
- timeoutMs: 20,
78
- brokerUrl: "mqtt://127.0.0.1:1883",
79
- });
80
-
81
- expect(resolved.simulationName).toBe("north-plant");
82
- expect(resolved.topics.metrics).toBe("$miravo/north-plant/metrics");
83
- });
84
-
85
- test("fails when multiple simulations are discovered", async () => {
86
- const client = new MockResolverClient();
87
- setTimeout(() => {
88
- client.emitMessage("$miravo/north-plant/metrics", '{"ok":true}');
89
- client.emitMessage("$miravo/south-plant/metrics", '{"ok":true}');
90
- }, 2);
91
-
92
- await expect(
93
- resolveTargetSimulation(client, {
94
- timeoutMs: 20,
95
- brokerUrl: "mqtt://127.0.0.1:1883",
96
- }),
97
- ).rejects.toThrow("Multiple running simulations found");
98
- });
99
- });
100
-
101
- describe("discoverRunningSimulations", () => {
102
- test("subscribes to metrics wildcard and returns discovered simulation names", async () => {
103
- const client = new MockResolverClient();
104
- setTimeout(() => {
105
- client.emitMessage("$miravo/line-a/metrics", '{"ok":true}');
106
- client.emitMessage("$miravo/line-b/metrics", '{"ok":true}');
107
- client.emitMessage("$miravo/line-a/metrics", '{"ok":true}');
108
- }, 2);
109
-
110
- const discovered = await discoverRunningSimulations(client, {
111
- timeoutMs: 20,
112
- brokerUrl: "mqtt://127.0.0.1:1883",
113
- });
114
-
115
- expect(discovered).toEqual(["line-a", "line-b"]);
116
- expect(client.subscriptions.has(CONTROL_TOPIC_WILDCARD_METRICS)).toBe(false);
117
- });
118
- });
@@ -1,181 +0,0 @@
1
- import {
2
- CONTROL_TOPIC_WILDCARD_METRICS,
3
- controlTopicCmd,
4
- controlTopicCmdResult,
5
- controlTopicMetrics,
6
- controlTopicState,
7
- MiravoError,
8
- SimulationNameSchema,
9
- } from "@miravo/shared";
10
-
11
- interface SimulationTopics {
12
- readonly metrics: string;
13
- readonly state: string;
14
- readonly cmd: string;
15
- readonly cmdResult: string;
16
- }
17
-
18
- interface ResolvedTargetSimulation {
19
- readonly simulationName: string;
20
- readonly topics: SimulationTopics;
21
- }
22
-
23
- interface ResolveTargetSimulationOptions {
24
- readonly name?: string;
25
- readonly timeoutMs: number;
26
- readonly brokerUrl: string;
27
- }
28
-
29
- type MessageHandler = (topic: string, payload: Buffer, ...extra: unknown[]) => void;
30
-
31
- interface SimulationResolverClient {
32
- subscribe(topic: string, options: { qos: 0 }, callback?: (error?: Error | null) => void): unknown;
33
- unsubscribe(topic: string): unknown;
34
- on(event: "message", handler: MessageHandler): unknown;
35
- off(event: "message", handler: MessageHandler): unknown;
36
- }
37
-
38
- function buildSimulationTopics(simulationName: string): SimulationTopics {
39
- return {
40
- metrics: controlTopicMetrics(simulationName),
41
- state: controlTopicState(simulationName),
42
- cmd: controlTopicCmd(simulationName),
43
- cmdResult: controlTopicCmdResult(simulationName),
44
- };
45
- }
46
-
47
- function validateSimulationName(name: string, source: string): string {
48
- const parsed = SimulationNameSchema.safeParse(name);
49
- if (!parsed.success) {
50
- throw new MiravoError(
51
- `Invalid simulation name "${name}" from ${source}.`,
52
- "Use lowercase letters, numbers, and hyphens. Example: --name north-plant.",
53
- );
54
- }
55
- return parsed.data;
56
- }
57
-
58
- function extractSimulationNameFromTopic(topic: string): string | null {
59
- const name = topic.split("/")[1];
60
- if (!name || name.length === 0) return null;
61
- const validated = SimulationNameSchema.safeParse(name);
62
- if (!validated.success) return null;
63
- return validated.data;
64
- }
65
-
66
- async function discoverRunningSimulations(
67
- client: SimulationResolverClient,
68
- options: { readonly timeoutMs: number; readonly brokerUrl: string },
69
- ): Promise<readonly string[]> {
70
- const { timeoutMs } = options;
71
- const simulations = new Set<string>();
72
-
73
- return new Promise((resolve, reject) => {
74
- let settled = false;
75
-
76
- const cleanup = (): void => {
77
- if (timer) {
78
- clearTimeout(timer);
79
- }
80
- client.off("message", handler);
81
- try {
82
- client.unsubscribe(CONTROL_TOPIC_WILDCARD_METRICS);
83
- } catch {
84
- // Ignore unsubscribe errors during cleanup.
85
- }
86
- };
87
-
88
- const finish = (fn: () => void): void => {
89
- if (settled) return;
90
- settled = true;
91
- cleanup();
92
- fn();
93
- };
94
-
95
- const handler: MessageHandler = (topic) => {
96
- if (!topic.startsWith("$miravo/")) return;
97
- const name = extractSimulationNameFromTopic(topic);
98
- if (!name) return;
99
- simulations.add(name);
100
- };
101
-
102
- const timer = setTimeout(() => {
103
- finish(() => {
104
- resolve([...simulations].sort((a, b) => a.localeCompare(b)));
105
- });
106
- }, timeoutMs);
107
-
108
- client.on("message", handler);
109
-
110
- try {
111
- client.subscribe(CONTROL_TOPIC_WILDCARD_METRICS, { qos: 0 }, (error) => {
112
- if (!error) return;
113
- finish(() => {
114
- reject(
115
- new MiravoError(
116
- `Failed to subscribe to simulation discovery topic "${CONTROL_TOPIC_WILDCARD_METRICS}".`,
117
- `Check broker ACLs for ${options.brokerUrl} and allow read access to $miravo topics.`,
118
- ),
119
- );
120
- });
121
- });
122
- } catch {
123
- finish(() => {
124
- reject(
125
- new MiravoError(
126
- `Failed to subscribe to simulation discovery topic "${CONTROL_TOPIC_WILDCARD_METRICS}".`,
127
- `Check broker ACLs for ${options.brokerUrl} and allow read access to $miravo topics.`,
128
- ),
129
- );
130
- });
131
- }
132
- });
133
- }
134
-
135
- async function resolveTargetSimulation(
136
- client: SimulationResolverClient,
137
- options: ResolveTargetSimulationOptions,
138
- ): Promise<ResolvedTargetSimulation> {
139
- if (options.name !== undefined) {
140
- const simulationName = validateSimulationName(options.name, "--name");
141
- return {
142
- simulationName,
143
- topics: buildSimulationTopics(simulationName),
144
- };
145
- }
146
-
147
- const discovered = await discoverRunningSimulations(client, {
148
- timeoutMs: options.timeoutMs,
149
- brokerUrl: options.brokerUrl,
150
- });
151
-
152
- if (discovered.length === 0) {
153
- throw new MiravoError(
154
- `No running simulations found on broker ${options.brokerUrl}.`,
155
- "Start a simulation first with 'miravo start', or pass --name if metrics are not retained.",
156
- );
157
- }
158
-
159
- if (discovered.length > 1) {
160
- throw new MiravoError(
161
- `Multiple running simulations found: ${discovered.join(", ")}.`,
162
- "Specify one with --name <name>.",
163
- );
164
- }
165
-
166
- const simulationName = discovered[0];
167
- if (!simulationName) {
168
- throw new MiravoError(
169
- "Failed to resolve simulation name from discovery results.",
170
- "Specify a simulation explicitly with --name <name>.",
171
- );
172
- }
173
-
174
- return {
175
- simulationName,
176
- topics: buildSimulationTopics(simulationName),
177
- };
178
- }
179
-
180
- export type { SimulationResolverClient };
181
- export { discoverRunningSimulations, resolveTargetSimulation };
@@ -1,110 +0,0 @@
1
- /**
2
- * Server-side session store — in-memory sessions with 24h TTL.
3
- *
4
- * Sessions are created on successful login and validated on every request.
5
- * Cookie helpers handle Set-Cookie header construction and parsing.
6
- */
7
-
8
- // ---------------------------------------------------------------------------
9
- // Session types
10
- // ---------------------------------------------------------------------------
11
-
12
- export interface Session {
13
- readonly id: string;
14
- readonly username: string;
15
- readonly createdAt: number;
16
- }
17
-
18
- export interface SessionStore {
19
- /** Create a new session for the given username. */
20
- create(username: string): Session;
21
- /** Validate a session ID. Returns the session or null if expired/missing. */
22
- validate(sessionId: string): Session | null;
23
- /** Destroy a specific session. */
24
- destroy(sessionId: string): void;
25
- /** Clear all sessions (used on server stop). */
26
- clear(): void;
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Constants
31
- // ---------------------------------------------------------------------------
32
-
33
- const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
34
- const SESSION_TTL_SECONDS = 24 * 60 * 60;
35
- const COOKIE_NAME = "miravo_session";
36
- const SWEEP_THRESHOLD = 50;
37
-
38
- // ---------------------------------------------------------------------------
39
- // Session store
40
- // ---------------------------------------------------------------------------
41
-
42
- export function createSessionStore(): SessionStore {
43
- const sessions = new Map<string, Session>();
44
-
45
- function sweepExpired(): void {
46
- const now = Date.now();
47
- for (const [id, s] of sessions) {
48
- if (now - s.createdAt > SESSION_TTL_MS) sessions.delete(id);
49
- }
50
- }
51
-
52
- return {
53
- create(username: string): Session {
54
- if (sessions.size >= SWEEP_THRESHOLD) sweepExpired();
55
- const session: Session = {
56
- id: crypto.randomUUID(),
57
- username,
58
- createdAt: Date.now(),
59
- };
60
- sessions.set(session.id, session);
61
- return session;
62
- },
63
-
64
- validate(sessionId: string): Session | null {
65
- const session = sessions.get(sessionId);
66
- if (!session) return null;
67
- if (Date.now() - session.createdAt > SESSION_TTL_MS) {
68
- sessions.delete(sessionId);
69
- return null;
70
- }
71
- return session;
72
- },
73
-
74
- destroy(sessionId: string): void {
75
- sessions.delete(sessionId);
76
- },
77
-
78
- clear(): void {
79
- sessions.clear();
80
- },
81
- };
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Cookie helpers
86
- // ---------------------------------------------------------------------------
87
-
88
- /** Build a Set-Cookie header value for a new session. */
89
- export function buildSessionCookie(sessionId: string): string {
90
- return `${COOKIE_NAME}=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_TTL_SECONDS}`;
91
- }
92
-
93
- /** Build a Set-Cookie header value that clears the session cookie. */
94
- export function buildClearCookie(): string {
95
- return `${COOKIE_NAME}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`;
96
- }
97
-
98
- /** Extract the session ID from the Cookie header of a request. Returns null if absent. */
99
- export function extractSessionId(request: Request): string | null {
100
- const header = request.headers.get("cookie");
101
- if (!header) return null;
102
- for (const part of header.split(";")) {
103
- const [key, ...rest] = part.split("=");
104
- if (key?.trim() === COOKIE_NAME) {
105
- const value = rest.join("=").trim();
106
- if (value.length > 0) return value;
107
- }
108
- }
109
- return null;
110
- }