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,797 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import type {
4
- EngineCoreMetrics,
5
- EngineStateSnapshot,
6
- EventLogQuery,
7
- EventLogResult,
8
- InstanceDetail,
9
- Manifest,
10
- ModelSchema,
11
- TemplateRun,
12
- } from "@miravo/core";
13
- import type {
14
- CatalogModel,
15
- CatalogTemplate,
16
- CatalogTemplateDetail,
17
- CommandResult,
18
- ConsoleBootstrapResponse,
19
- EngineCommand,
20
- LoggedEvent,
21
- RuntimeSnapshot,
22
- } from "@miravo/shared";
23
- import { handleAuthStatus, handleLogin, handleLogout, handlePasswordChange } from "./admin-auth.ts";
24
- import { handlePlatformRoute } from "./admin-platform-routes.ts";
25
- import type { HealthResponseBody } from "./admin-responses.ts";
26
- import {
27
- buildHealthBody,
28
- checkContentType,
29
- jsonResponse,
30
- livenessStatus,
31
- parseCommandRequest,
32
- plainResponse,
33
- readinessStatus,
34
- yamlResponse,
35
- } from "./admin-responses.ts";
36
- import type { SessionStore } from "./session-store.ts";
37
- import { extractSessionId } from "./session-store.ts";
38
- import type { WSClientData } from "./ws-hub.ts";
39
- import { ALL_CHANNELS, WSHub } from "./ws-hub.ts";
40
-
41
- /** Valid event types for the /events endpoint, derived from LoggedEvent discriminant. */
42
- const VALID_EVENT_TYPES: ReadonlySet<string> = new Set<LoggedEvent["type"]>([
43
- "fault:triggered",
44
- "fault:cleared",
45
- "lifecycle:changed",
46
- "instance:created",
47
- "instance:removed",
48
- "engine:state-changed",
49
- "platform:resource",
50
- "platform:operation",
51
- ]);
52
-
53
- interface AdminEngine {
54
- getMetrics(): {
55
- readonly core: EngineCoreMetrics;
56
- readonly adapters: ReadonlyArray<{
57
- readonly name: string;
58
- readonly metrics: { readonly connected: boolean };
59
- }>;
60
- };
61
- getAvailableAdapters(): ReadonlyArray<{
62
- readonly name: string;
63
- readonly enabled: boolean;
64
- }>;
65
- getState(): EngineStateSnapshot;
66
- execute(command: EngineCommand): Promise<CommandResult>;
67
- getInstanceDetail(id: string): InstanceDetail | undefined;
68
- getModelSchema(name: string): ModelSchema | undefined;
69
- getEventLog(query?: EventLogQuery): EventLogResult;
70
- getRunningTemplates(): readonly TemplateRun[];
71
- getManifest(): Manifest;
72
- }
73
-
74
- /** Network binding configuration. */
75
- interface AdminServerNetworkOptions {
76
- readonly host: string;
77
- readonly port: number;
78
- readonly tickIntervalMs: number;
79
- }
80
-
81
- /** Protocol adapter status configuration. */
82
- interface AdminServerProtocolOptions {
83
- readonly getLastTickProgressAt: () => number;
84
- }
85
-
86
- /** WebSocket streaming configuration. When provided, enables the /ws endpoint. */
87
- interface AdminServerWSOptions {
88
- readonly bus: import("@miravo/core").EventBus;
89
- }
90
-
91
- /** Authentication configuration for the admin server. */
92
- interface AdminServerAuth {
93
- /** Verify a plaintext password against the stored hash. */
94
- readonly verifyPassword: (password: string) => Promise<boolean>;
95
- /** True when the effective password is the literal default "admin". */
96
- readonly isDefaultPassword: boolean;
97
- /** Server-side session store. */
98
- readonly sessionStore: SessionStore;
99
- /** Change the admin password (verifies old, hashes + persists new). */
100
- readonly changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
101
- /** When true, all requests are treated as authenticated. No session required. */
102
- readonly anonymousMode?: boolean;
103
- }
104
-
105
- interface AdminServerOptions {
106
- readonly engine: AdminEngine;
107
- readonly network: AdminServerNetworkOptions;
108
- readonly protocols: AdminServerProtocolOptions;
109
- /** WebSocket streaming. When provided, enables /ws endpoint. */
110
- readonly ws?: AdminServerWSOptions | undefined;
111
- /** Authentication configuration. */
112
- readonly auth: AdminServerAuth;
113
- /** Event log for platform operation tracking. */
114
- readonly eventLog?: import("@miravo/core").EventLog | undefined;
115
- /** Path to console static assets directory. When set, serves the SPA at non-API routes. */
116
- readonly consoleDir?: string | undefined;
117
- /** Template catalog provider for GET /catalog/templates. */
118
- readonly getCatalog?: (() => readonly CatalogTemplate[]) | undefined;
119
- /** Model catalog provider for GET /catalog/models. */
120
- readonly getCatalogModels?: (() => Promise<readonly CatalogModel[]>) | undefined;
121
- /** Model schema from YAML for GET /catalog/models/:name. */
122
- readonly getCatalogModelSchema?: ((name: string) => Promise<ModelSchema | null>) | undefined;
123
- /** Raw YAML text for GET /catalog/models/:name/yaml. */
124
- readonly getModelYaml?: ((name: string) => Promise<string | null>) | undefined;
125
- /** LE Digital Twin JSON for GET /catalog/models/:name/le-dt.json. */
126
- readonly getLeDtJson?: ((name: string) => Promise<string | null>) | undefined;
127
- /** Template detail with topology for GET /catalog/templates/:id. */
128
- readonly getTemplateDetail?: ((id: string) => Promise<CatalogTemplateDetail | null>) | undefined;
129
- /** Full LE export (JSON + CSV) for GET /catalog/templates/:id/le-template.json and /le-tags.csv. */
130
- readonly getLeExport?:
131
- | ((
132
- id: string,
133
- opts: { host: string; protocol: import("@miravo/shared").ProtocolName | undefined },
134
- ) => Promise<{ templateJson: string; tagsCSV: string } | null>)
135
- | undefined;
136
- /** Raw YAML text for GET /catalog/templates/:id/yaml. */
137
- readonly getTemplateYaml?: ((id: string) => Promise<string | null>) | undefined;
138
- /** Simulation name for daemon identity verification via /readyz. */
139
- readonly simulationName?: string | undefined;
140
- /** Run ID for daemon identity verification via /readyz. */
141
- readonly runId?: string | undefined;
142
- /** Callback to request graceful shutdown. Used by POST /shutdown. */
143
- readonly requestShutdown?: ((reason: string) => void) | undefined;
144
- }
145
-
146
- interface AdminServerHandle {
147
- readonly endpoint: string;
148
- readonly wsHub: WSHub | null;
149
- stop(): void;
150
- }
151
-
152
- /**
153
- * Start the Miravo admin HTTP server.
154
- *
155
- * - `/healthz`: liveness (process/tick loop health)
156
- * - `/readyz`: readiness (engine running + adapters connected when enabled)
157
- * - `/metrics`: engine metrics snapshot
158
- * - `/state`: engine state snapshot
159
- * - `/commands`: command execution endpoint using EngineCommandSchema
160
- */
161
- function startAdminServer(options: AdminServerOptions): AdminServerHandle {
162
- const { engine } = options;
163
- const { host, port } = options.network;
164
- const { getLastTickProgressAt } = options.protocols;
165
-
166
- const { auth } = options;
167
-
168
- function getSession(request: Request) {
169
- const sessionId = extractSessionId(request);
170
- return sessionId ? auth.sessionStore.validate(sessionId) : null;
171
- }
172
-
173
- /** Validate session cookie. Returns error Response or null if OK. */
174
- function checkSession(request: Request): Response | null {
175
- if (options.auth.anonymousMode) return null;
176
- const session = getSession(request);
177
- if (!session) {
178
- const sessionId = extractSessionId(request);
179
- if (!sessionId) {
180
- return jsonResponse(
181
- { message: "Unauthorized.", error: "Valid session required. Log in at /auth/login." },
182
- 401,
183
- );
184
- }
185
- return jsonResponse(
186
- { message: "Session expired.", error: "Log in again at /auth/login." },
187
- 401,
188
- );
189
- }
190
- return null;
191
- }
192
-
193
- // Create WebSocket hub if ws config is provided
194
- const wsHub = options.ws
195
- ? new WSHub({
196
- bus: options.ws.bus,
197
- snapshotSource: {
198
- getState: () => engine.getState(),
199
- getMetrics: () => engine.getMetrics(),
200
- getRunningTemplates: () => engine.getRunningTemplates(),
201
- },
202
- })
203
- : null;
204
-
205
- // Pre-resolve console dir and index.html once at startup (avoids per-request exists() check).
206
- // Verify the file exists so we don't silently serve a missing build as a 200.
207
- const consoleDir = options.consoleDir ? resolve(options.consoleDir) : null;
208
- const consoleIndexPath = consoleDir ? `${consoleDir}/index.html` : null;
209
- const consoleIndexFile =
210
- consoleIndexPath && existsSync(consoleIndexPath) ? Bun.file(consoleIndexPath) : null;
211
-
212
- const server = Bun.serve({
213
- hostname: host,
214
- port,
215
- async fetch(request, server) {
216
- const url = new URL(request.url);
217
- const pathname = url.pathname;
218
-
219
- // WebSocket upgrade for /ws
220
- if (pathname === "/ws") {
221
- if (!wsHub) {
222
- return plainResponse("WebSocket not available", 503);
223
- }
224
- const sessionBlock = checkSession(request);
225
- if (sessionBlock) return sessionBlock;
226
- const upgraded = server.upgrade(request, {
227
- // WSClientData — Bun's untyped websocket generic requires the cast
228
- data: { channels: new Set(ALL_CHANNELS) } as unknown as undefined,
229
- });
230
- if (!upgraded) {
231
- return plainResponse("WebSocket upgrade failed", 400);
232
- }
233
- // Bun requires a Response return, but upgrade() consumes the request
234
- return undefined as unknown as Response;
235
- }
236
-
237
- // Health/metrics routes — compute expensive metrics only when needed
238
- if (pathname === "/healthz" || pathname === "/readyz" || pathname === "/metrics") {
239
- if (pathname === "/metrics" && request.method !== "GET") {
240
- return plainResponse("Method Not Allowed", 405, "GET");
241
- }
242
-
243
- const metrics = engine.getMetrics();
244
-
245
- if (pathname === "/metrics") {
246
- const metricsAuth = checkSession(request);
247
- if (metricsAuth) return metricsAuth;
248
- return jsonResponse(metrics, 200);
249
- }
250
-
251
- const now = performance.now();
252
- const stallThresholdMs = Math.max(100, options.network.tickIntervalMs * 3);
253
- const lastTickProgressAt = getLastTickProgressAt();
254
- const stalled =
255
- metrics.core.state === "running" &&
256
- Number.isFinite(lastTickProgressAt) &&
257
- now - lastTickProgressAt > stallThresholdMs;
258
-
259
- const availableAdapters = engine.getAvailableAdapters();
260
- const mqttEnabled = availableAdapters.some(
261
- (adapter) => adapter.name === "mqtt" && adapter.enabled,
262
- );
263
- const opcuaEnabled = availableAdapters.some(
264
- (adapter) => adapter.name === "opcua" && adapter.enabled,
265
- );
266
-
267
- const mqttConnected = mqttEnabled
268
- ? (metrics.adapters.find((a) => a.name === "mqtt")?.metrics.connected ?? false)
269
- : false;
270
- const opcuaConnected = opcuaEnabled
271
- ? (metrics.adapters.find((a) => a.name === "opcua")?.metrics.connected ?? false)
272
- : false;
273
- const healthBody = buildHealthBody({
274
- metrics,
275
- stalled,
276
- mqttEnabled,
277
- mqttConnected,
278
- opcuaEnabled,
279
- opcuaConnected,
280
- });
281
-
282
- if (pathname === "/healthz") {
283
- const status = livenessStatus({ core: metrics.core, stalled });
284
- return jsonResponse(
285
- {
286
- status: status.status,
287
- ...healthBody,
288
- ...(status.reason ? { reason: status.reason } : {}),
289
- } satisfies HealthResponseBody,
290
- status.status === "ok" ? 200 : 503,
291
- );
292
- }
293
-
294
- // /readyz
295
- const status = readinessStatus({
296
- core: metrics.core,
297
- stalled,
298
- mqttEnabled,
299
- mqttConnected,
300
- opcuaEnabled,
301
- opcuaConnected,
302
- });
303
- return jsonResponse(
304
- {
305
- status: status.status,
306
- ...healthBody,
307
- ...(status.reason ? { reason: status.reason } : {}),
308
- ...(options.simulationName ? { simulationName: options.simulationName } : {}),
309
- ...(options.runId ? { runId: options.runId } : {}),
310
- } satisfies HealthResponseBody,
311
- status.status === "ok" ? 200 : 503,
312
- );
313
- }
314
-
315
- if (pathname === "/state") {
316
- if (request.method !== "GET") {
317
- return plainResponse("Method Not Allowed", 405, "GET");
318
- }
319
- const stateAuth = checkSession(request);
320
- if (stateAuth) return stateAuth;
321
- return jsonResponse(engine.getState(), 200);
322
- }
323
-
324
- if (pathname === "/bootstrap") {
325
- if (request.method !== "GET") {
326
- return plainResponse("Method Not Allowed", 405, "GET");
327
- }
328
- if (options.auth.anonymousMode) {
329
- const runtime: RuntimeSnapshot = wsHub
330
- ? wsHub.buildRuntimeSnapshot()
331
- : (JSON.parse(
332
- JSON.stringify({
333
- state: engine.getState(),
334
- metrics: engine.getMetrics(),
335
- runningTemplates: engine.getRunningTemplates(),
336
- revision: 0,
337
- }),
338
- ) as RuntimeSnapshot);
339
- return jsonResponse(
340
- {
341
- authenticated: true,
342
- username: "anonymous",
343
- runtime,
344
- } satisfies ConsoleBootstrapResponse,
345
- 200,
346
- );
347
- }
348
- const session = getSession(request);
349
- if (!session) {
350
- return jsonResponse({ authenticated: false } satisfies ConsoleBootstrapResponse, 200);
351
- }
352
- const runtime: RuntimeSnapshot = wsHub
353
- ? wsHub.buildRuntimeSnapshot()
354
- : (JSON.parse(
355
- JSON.stringify({
356
- state: engine.getState(),
357
- metrics: engine.getMetrics(),
358
- runningTemplates: engine.getRunningTemplates(),
359
- revision: 0,
360
- }),
361
- ) as RuntimeSnapshot);
362
- const body: ConsoleBootstrapResponse = {
363
- authenticated: true,
364
- username: session.username,
365
- runtime,
366
- };
367
- return jsonResponse(body, 200);
368
- }
369
-
370
- if (pathname === "/commands") {
371
- if (request.method !== "POST") {
372
- return plainResponse("Method Not Allowed", 405, "POST");
373
- }
374
- const cmdAuth = checkSession(request);
375
- if (cmdAuth) return cmdAuth;
376
- const ctBlock = checkContentType(request);
377
- if (ctBlock) return ctBlock;
378
- const commandOrError = await parseCommandRequest(request);
379
- if (commandOrError instanceof Response) {
380
- return commandOrError;
381
- }
382
- return jsonResponse(await engine.execute(commandOrError), 200);
383
- }
384
-
385
- // POST /shutdown — graceful shutdown for daemon mode
386
- if (pathname === "/shutdown") {
387
- if (request.method !== "POST") {
388
- return plainResponse("Method Not Allowed", 405, "POST");
389
- }
390
- if (!options.requestShutdown) {
391
- return jsonResponse(
392
- {
393
- message: "Shutdown not available.",
394
- error: "This server does not support remote shutdown.",
395
- },
396
- 501,
397
- );
398
- }
399
- const shutdownAuth = checkSession(request);
400
- if (shutdownAuth) return shutdownAuth;
401
- const shutdownCtBlock = checkContentType(request);
402
- if (shutdownCtBlock) return shutdownCtBlock;
403
-
404
- let reason = "remote shutdown request";
405
- try {
406
- const body = (await request.json()) as unknown;
407
- if (typeof body === "object" && body !== null && "reason" in body) {
408
- const maybeReason = (body as Record<string, unknown>)["reason"];
409
- if (typeof maybeReason === "string" && maybeReason.length > 0) {
410
- reason = maybeReason;
411
- }
412
- }
413
- } catch {
414
- // No body or invalid JSON — use default reason
415
- }
416
-
417
- options.requestShutdown(reason);
418
- return jsonResponse({ status: "shutting_down" }, 200);
419
- }
420
-
421
- // --- New REST endpoints (Phase 1c) ---
422
-
423
- // GET /instances/:id
424
- if (pathname.startsWith("/instances/")) {
425
- if (request.method !== "GET") {
426
- return plainResponse("Method Not Allowed", 405, "GET");
427
- }
428
- const instanceAuth = checkSession(request);
429
- if (instanceAuth) return instanceAuth;
430
- const instanceId = pathname.slice("/instances/".length);
431
- if (!instanceId) {
432
- return jsonResponse(
433
- { message: "Missing instance ID.", error: "Use /instances/{id}" },
434
- 400,
435
- );
436
- }
437
- const detail = engine.getInstanceDetail(instanceId);
438
- if (!detail) {
439
- return jsonResponse(
440
- { message: `Instance "${instanceId}" not found.`, error: "Check the instance ID." },
441
- 404,
442
- );
443
- }
444
- return jsonResponse(detail, 200);
445
- }
446
-
447
- // GET /models/:name
448
- if (pathname.startsWith("/models/")) {
449
- if (request.method !== "GET") {
450
- return plainResponse("Method Not Allowed", 405, "GET");
451
- }
452
- const modelAuth = checkSession(request);
453
- if (modelAuth) return modelAuth;
454
- const modelName = pathname.slice("/models/".length);
455
- if (!modelName) {
456
- return jsonResponse({ message: "Missing model name.", error: "Use /models/{name}" }, 400);
457
- }
458
- const schema = engine.getModelSchema(modelName);
459
- if (!schema) {
460
- return jsonResponse(
461
- { message: `Model "${modelName}" not found.`, error: "Check the model name." },
462
- 404,
463
- );
464
- }
465
- return jsonResponse(schema, 200);
466
- }
467
-
468
- // GET /events
469
- if (pathname === "/events") {
470
- if (request.method !== "GET") {
471
- return plainResponse("Method Not Allowed", 405, "GET");
472
- }
473
- const eventsAuth = checkSession(request);
474
- if (eventsAuth) return eventsAuth;
475
- const typeParam = url.searchParams.get("type");
476
- if (typeParam && !VALID_EVENT_TYPES.has(typeParam)) {
477
- return jsonResponse(
478
- {
479
- message: `Invalid event type "${typeParam}".`,
480
- error: `Valid types: ${[...VALID_EVENT_TYPES].join(", ")}`,
481
- },
482
- 400,
483
- );
484
- }
485
- const instanceIdParam = url.searchParams.get("instanceId");
486
- const limitRaw = url.searchParams.get("limit");
487
- const cursorRaw = url.searchParams.get("cursor");
488
- const query: EventLogQuery = {};
489
- if (typeParam)
490
- (query as { type: EventLogQuery["type"] }).type = typeParam as EventLogQuery["type"];
491
- if (instanceIdParam) (query as { instanceId: string }).instanceId = instanceIdParam;
492
- const parsedLimit = limitRaw ? Number.parseInt(limitRaw, 10) : 0;
493
- if (parsedLimit > 0 && Number.isFinite(parsedLimit))
494
- (query as { limit: number }).limit = parsedLimit;
495
- const parsedCursor = cursorRaw ? Number.parseInt(cursorRaw, 10) : Number.NaN;
496
- if (Number.isFinite(parsedCursor)) (query as { cursor: number }).cursor = parsedCursor;
497
- return jsonResponse(engine.getEventLog(query), 200);
498
- }
499
-
500
- // GET /catalog/models
501
- if (pathname === "/catalog/models") {
502
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
503
- const auth = checkSession(request);
504
- if (auth) return auth;
505
- const models = options.getCatalogModels ? await options.getCatalogModels() : [];
506
- return jsonResponse(models, 200);
507
- }
508
-
509
- // GET /catalog/models/:name/le-dt.json
510
- if (pathname.startsWith("/catalog/models/") && pathname.endsWith("/le-dt.json")) {
511
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
512
- const auth = checkSession(request);
513
- if (auth) return auth;
514
- const name = decodeURIComponent(
515
- pathname.slice("/catalog/models/".length, -"/le-dt.json".length),
516
- );
517
- if (
518
- !name ||
519
- name.includes("/") ||
520
- name.includes("\\") ||
521
- name.includes("..") ||
522
- !options.getLeDtJson
523
- )
524
- return jsonResponse({ message: "Not found.", error: "Model not found." }, 404);
525
- const leDtJson = await options.getLeDtJson(name);
526
- if (!leDtJson)
527
- return jsonResponse(
528
- { message: `Model "${name}" not found.`, error: "Check the model name." },
529
- 404,
530
- );
531
- return jsonResponse(JSON.parse(leDtJson), 200);
532
- }
533
-
534
- // GET /catalog/models/:name/yaml
535
- if (pathname.startsWith("/catalog/models/") && pathname.endsWith("/yaml")) {
536
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
537
- const auth = checkSession(request);
538
- if (auth) return auth;
539
- const name = decodeURIComponent(pathname.slice("/catalog/models/".length, -"/yaml".length));
540
- if (
541
- !name ||
542
- name.includes("/") ||
543
- name.includes("\\") ||
544
- name.includes("..") ||
545
- !options.getModelYaml
546
- )
547
- return jsonResponse({ message: "Not found.", error: "Model not found." }, 404);
548
- const yaml = await options.getModelYaml(name);
549
- if (!yaml)
550
- return jsonResponse(
551
- { message: `Model "${name}" not found.`, error: "Check the model name." },
552
- 404,
553
- );
554
- return yamlResponse(yaml, 200);
555
- }
556
-
557
- // GET /catalog/models/:name (schema)
558
- if (pathname.startsWith("/catalog/models/")) {
559
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
560
- const auth = checkSession(request);
561
- if (auth) return auth;
562
- const name = decodeURIComponent(pathname.slice("/catalog/models/".length));
563
- if (
564
- !name ||
565
- name.includes("/") ||
566
- name.includes("\\") ||
567
- name.includes("..") ||
568
- !options.getCatalogModelSchema
569
- )
570
- return jsonResponse({ message: "Not found.", error: "Model not found." }, 404);
571
- const schema = await options.getCatalogModelSchema(name);
572
- if (!schema)
573
- return jsonResponse(
574
- { message: `Model "${name}" not found.`, error: "Check the model name." },
575
- 404,
576
- );
577
- return jsonResponse(schema, 200);
578
- }
579
-
580
- // GET /catalog/templates/:id/le-template.json and /le-tags.csv
581
- if (
582
- pathname.startsWith("/catalog/templates/") &&
583
- (pathname.endsWith("/le-template.json") || pathname.endsWith("/le-tags.csv"))
584
- ) {
585
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
586
- const auth = checkSession(request);
587
- if (auth) return auth;
588
- const isJson = pathname.endsWith("/le-template.json");
589
- const suffix = isJson ? "/le-template.json" : "/le-tags.csv";
590
- const id = decodeURIComponent(pathname.slice("/catalog/templates/".length, -suffix.length));
591
- const host = url.searchParams.get("host")?.trim() ?? "";
592
- const protocolParam = url.searchParams.get("protocol") ?? undefined;
593
- if (
594
- !id ||
595
- id.includes("/") ||
596
- id.includes("\\") ||
597
- id.includes("..") ||
598
- !options.getLeExport
599
- )
600
- return jsonResponse({ message: "Not found.", error: "Template not found." }, 404);
601
- if (!host) {
602
- return jsonResponse(
603
- {
604
- message: "Missing host query parameter.",
605
- error: "Provide ?host=<hostname-or-ip> reachable from Litmus Edge.",
606
- },
607
- 400,
608
- );
609
- }
610
- const result = await options.getLeExport(id, {
611
- host,
612
- protocol: protocolParam as import("@miravo/shared").ProtocolName | undefined,
613
- });
614
- if (!result)
615
- return jsonResponse(
616
- { message: `Template "${id}" not found.`, error: "Check the template ID." },
617
- 404,
618
- );
619
- return isJson
620
- ? new Response(result.templateJson, {
621
- status: 200,
622
- headers: {
623
- "content-type": "application/json",
624
- "content-disposition": `attachment; filename="${id}.le-template.json"`,
625
- },
626
- })
627
- : new Response(result.tagsCSV, {
628
- status: 200,
629
- headers: {
630
- "content-type": "text/csv",
631
- "content-disposition": `attachment; filename="${id}-tags.csv"`,
632
- },
633
- });
634
- }
635
-
636
- // GET /catalog/templates/:id/yaml
637
- if (pathname.startsWith("/catalog/templates/") && pathname.endsWith("/yaml")) {
638
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
639
- const auth = checkSession(request);
640
- if (auth) return auth;
641
- const id = decodeURIComponent(
642
- pathname.slice("/catalog/templates/".length, -"/yaml".length),
643
- );
644
- if (
645
- !id ||
646
- id.includes("/") ||
647
- id.includes("\\") ||
648
- id.includes("..") ||
649
- !options.getTemplateYaml
650
- )
651
- return jsonResponse({ message: "Not found.", error: "Template not found." }, 404);
652
- const yaml = await options.getTemplateYaml(id);
653
- if (!yaml)
654
- return jsonResponse(
655
- { message: `Template "${id}" not found.`, error: "Check the template ID." },
656
- 404,
657
- );
658
- return yamlResponse(yaml, 200);
659
- }
660
-
661
- // GET /catalog/templates/:id (detail with topology)
662
- if (pathname.startsWith("/catalog/templates/") && pathname !== "/catalog/templates") {
663
- if (request.method !== "GET") return plainResponse("Method Not Allowed", 405, "GET");
664
- const auth = checkSession(request);
665
- if (auth) return auth;
666
- const id = decodeURIComponent(pathname.slice("/catalog/templates/".length));
667
- if (
668
- !id ||
669
- id.includes("/") ||
670
- id.includes("\\") ||
671
- id.includes("..") ||
672
- !options.getTemplateDetail
673
- )
674
- return jsonResponse({ message: "Not found.", error: "Template not found." }, 404);
675
- const detail = await options.getTemplateDetail(id);
676
- if (!detail)
677
- return jsonResponse(
678
- { message: `Template "${id}" not found.`, error: "Check the template ID." },
679
- 404,
680
- );
681
- return jsonResponse(detail, 200);
682
- }
683
-
684
- // GET /catalog/templates
685
- if (pathname === "/catalog/templates") {
686
- if (request.method !== "GET") {
687
- return plainResponse("Method Not Allowed", 405, "GET");
688
- }
689
- const catalogAuth = checkSession(request);
690
- if (catalogAuth) return catalogAuth;
691
- const catalog = options.getCatalog ? options.getCatalog() : [];
692
- return jsonResponse(catalog, 200);
693
- }
694
-
695
- // GET /running-templates
696
- if (pathname === "/running-templates") {
697
- if (request.method !== "GET") {
698
- return plainResponse("Method Not Allowed", 405, "GET");
699
- }
700
- const rtAuth = checkSession(request);
701
- if (rtAuth) return rtAuth;
702
- return jsonResponse(engine.getRunningTemplates(), 200);
703
- }
704
-
705
- // GET /manifest
706
- if (pathname === "/manifest") {
707
- if (request.method !== "GET") {
708
- return plainResponse("Method Not Allowed", 405, "GET");
709
- }
710
- const manifestAuth = checkSession(request);
711
- if (manifestAuth) return manifestAuth;
712
- return jsonResponse(engine.getManifest(), 200);
713
- }
714
-
715
- // --- Auth routes ---
716
-
717
- if (pathname === "/auth/login" && request.method === "POST") {
718
- return handleLogin(request, auth);
719
- }
720
-
721
- if (pathname === "/auth/logout" && request.method === "POST") {
722
- return handleLogout(request, auth);
723
- }
724
-
725
- if (pathname === "/auth/password" && request.method === "PUT") {
726
- const pwAuth = checkSession(request);
727
- if (pwAuth) return pwAuth;
728
- return handlePasswordChange(request, auth);
729
- }
730
-
731
- if (pathname === "/auth/status") {
732
- if (request.method !== "GET") {
733
- return plainResponse("Method Not Allowed", 405, "GET");
734
- }
735
- return handleAuthStatus(request, auth);
736
- }
737
-
738
- // --- Platform routes (LE apply/cleanup) ---
739
- if (pathname.startsWith("/platforms/")) {
740
- const platformResponse = await handlePlatformRoute(
741
- pathname,
742
- request,
743
- checkSession,
744
- options.eventLog,
745
- );
746
- if (platformResponse) return platformResponse;
747
- }
748
-
749
- // --- Console static file serving ---
750
- if (consoleIndexFile && consoleDir) {
751
- if (pathname !== "/") {
752
- // Resolve and verify the path stays within consoleDir to prevent traversal.
753
- const resolvedFile = resolve(consoleDir, `.${pathname}`);
754
- if (resolvedFile.startsWith(`${consoleDir}/`)) {
755
- const file = Bun.file(resolvedFile);
756
- if (await file.exists()) {
757
- return new Response(file);
758
- }
759
- }
760
- }
761
- // SPA fallback: serve pre-verified index.html
762
- return new Response(consoleIndexFile);
763
- }
764
-
765
- return plainResponse("Not Found", 404);
766
- },
767
- websocket: wsHub
768
- ? {
769
- open(ws: import("bun").ServerWebSocket<unknown>) {
770
- wsHub.onOpen(ws as import("bun").ServerWebSocket<WSClientData>);
771
- },
772
- message(ws: import("bun").ServerWebSocket<unknown>, message: string | Buffer) {
773
- wsHub.onMessage(ws as import("bun").ServerWebSocket<WSClientData>, message);
774
- },
775
- close(ws: import("bun").ServerWebSocket<unknown>) {
776
- wsHub.onClose(ws as import("bun").ServerWebSocket<WSClientData>);
777
- },
778
- }
779
- : {
780
- open() {},
781
- message() {},
782
- close() {},
783
- },
784
- });
785
-
786
- return {
787
- endpoint: `http://${host}:${server.port}`,
788
- wsHub,
789
- stop() {
790
- auth.sessionStore.clear();
791
- wsHub?.close();
792
- server.stop(true);
793
- },
794
- };
795
- }
796
-
797
- export { startAdminServer };