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.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
package/src/lib/admin-server.ts
DELETED
|
@@ -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 };
|