miravo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/console-dist/assets/activity-DsWeRq4h.js +1 -0
- package/console-dist/assets/api-4AqKvsm9.js +1 -0
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
- package/console-dist/assets/box-C1WaoOMF.js +1 -0
- package/console-dist/assets/card-BjeP32PN.js +1 -0
- package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
- package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
- package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
- package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
- package/console-dist/assets/control-CPhvwx9o.js +1 -0
- package/console-dist/assets/control-Cg2up9NA.js +1 -0
- package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
- package/console-dist/assets/dist-BhbDML0a.js +1 -0
- package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
- package/console-dist/assets/download-DQ6NH16U.js +1 -0
- package/console-dist/assets/engine-store-CrLnz692.js +1 -0
- package/console-dist/assets/events-CXm29Ok4.js +1 -0
- package/console-dist/assets/events-D01IxhnP.js +1 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
- package/console-dist/assets/fleet-lotMGVqe.js +1 -0
- package/console-dist/assets/format-kXTonpDJ.js +1 -0
- package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
- package/console-dist/assets/index-CVPKSDvJ.js +12 -0
- package/console-dist/assets/index-DJkgDeun.css +2 -0
- package/console-dist/assets/input-BWEQFptx.js +1 -0
- package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
- package/console-dist/assets/label-uephX5ZG.js +1 -0
- package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
- package/console-dist/assets/login-C3RJli0m.js +126 -0
- package/console-dist/assets/models-B_UioWQ5.js +1 -0
- package/console-dist/assets/models-_IPlPiZ7.js +1 -0
- package/console-dist/assets/not-found-B8gAm08S.js +1 -0
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
- package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
- package/console-dist/assets/platforms-DStNbtVc.js +1 -0
- package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
- package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
- package/console-dist/assets/radio-CoHegOv5.js +1 -0
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
- package/console-dist/assets/select-CeO4vP0G.js +1 -0
- package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
- package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
- package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
- package/console-dist/assets/tags-CiXOIHtY.js +4 -0
- package/console-dist/assets/tags-FQCXIxrP.js +1 -0
- package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
- package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
- package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
- package/console-dist/assets/validation-Blvud9_O.js +1 -0
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
- package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
- package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
- package/console-dist/index.html +70 -2
- package/package.json +9 -8
- package/src/__tests__/integration/executable-methods-runner.ts +7 -6
- package/src/__tests__/integration/executable-methods.integration.ts +214 -122
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
- package/src/commands/adapter.ts +180 -7
- package/src/commands/apply.test.ts +62 -0
- package/src/commands/apply.ts +216 -0
- package/src/commands/cleanup.test.ts +34 -0
- package/src/commands/cleanup.ts +201 -0
- package/src/commands/export.test.ts +51 -0
- package/src/commands/export.ts +404 -0
- package/src/commands/fork.ts +2 -2
- package/src/commands/inject.ts +2 -2
- package/src/commands/load.ts +1 -1
- package/src/commands/logs.ts +13 -15
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +1 -1
- package/src/commands/pause.ts +3 -3
- package/src/commands/platforms.test.ts +186 -0
- package/src/commands/platforms.ts +371 -0
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +2 -2
- package/src/commands/spawn.ts +2 -2
- package/src/commands/speed.ts +2 -2
- package/src/commands/start/adapter-config.ts +157 -0
- package/src/commands/start/catalog-le-dt.test.ts +50 -0
- package/src/commands/start/catalog-le-dt.ts +187 -0
- package/src/commands/start/catalog-model-schema.ts +89 -0
- package/src/commands/start/catalog-template-detail.ts +54 -0
- package/src/commands/start/daemon.ts +255 -0
- package/src/commands/start/display.ts +150 -0
- package/src/commands/start/engine-setup.ts +247 -0
- package/src/commands/start/events.ts +126 -0
- package/src/commands/start/index.ts +897 -0
- package/src/commands/start/protocol-plans.ts +239 -0
- package/src/commands/start/template-loader.ts +197 -0
- package/src/commands/start/template-resolver.ts +185 -0
- package/src/commands/start/types.ts +73 -0
- package/src/commands/start.test.ts +137 -16
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +19 -8
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +1 -3
- package/src/index.ts +11 -3
- package/src/lib/admin-auth.ts +157 -0
- package/src/lib/admin-client.test.ts +31 -15
- package/src/lib/admin-client.ts +106 -6
- package/src/lib/admin-platform-routes.ts +1017 -0
- package/src/lib/admin-platform-stream.test.ts +434 -0
- package/src/lib/admin-platform-stream.ts +788 -0
- package/src/lib/admin-responses.ts +211 -0
- package/src/lib/admin-server.test.ts +274 -55
- package/src/lib/admin-server.ts +387 -248
- package/src/lib/auth-store.ts +87 -0
- package/src/lib/daemon-registry.ts +12 -14
- package/src/lib/le/le-apply.test.ts +591 -0
- package/src/lib/le/le-apply.ts +832 -0
- package/src/lib/le/le-auth.ts +89 -0
- package/src/lib/le/le-cleanup.test.ts +168 -0
- package/src/lib/le/le-cleanup.ts +399 -0
- package/src/lib/le/le-cli-helpers.ts +77 -0
- package/src/lib/le/le-client.ts +184 -0
- package/src/lib/le/le-concurrency.test.ts +157 -0
- package/src/lib/le/le-concurrency.ts +78 -0
- package/src/lib/le/le-connection-test.ts +42 -0
- package/src/lib/le/le-devicehub-properties.test.ts +53 -0
- package/src/lib/le/le-devicehub-properties.ts +33 -0
- package/src/lib/le/le-devicehub.ts +213 -0
- package/src/lib/le/le-digital-twins.ts +285 -0
- package/src/lib/le/le-drivers.ts +23 -0
- package/src/lib/le/le-endpoint.ts +40 -0
- package/src/lib/le/le-environments.test.ts +73 -0
- package/src/lib/le/le-environments.ts +156 -0
- package/src/lib/le/le-markers.ts +87 -0
- package/src/lib/le/le-plan.test.ts +26 -0
- package/src/lib/le/le-plan.ts +109 -0
- package/src/lib/le/le-platform-operations.test.ts +198 -0
- package/src/lib/le/le-platform-operations.ts +340 -0
- package/src/lib/le/le-static-model-schema.ts +78 -0
- package/src/lib/le/le-static-planner.test.ts +255 -0
- package/src/lib/le/le-static-planner.ts +442 -0
- package/src/lib/le/le-template-exporter.test.ts +527 -0
- package/src/lib/le/le-template-exporter.ts +459 -0
- package/src/lib/le/le-value-types.ts +23 -0
- package/src/lib/mqtt-client.ts +1 -2
- package/src/lib/resolve-simulation.ts +1 -6
- package/src/lib/session-store.ts +110 -0
- package/src/lib/shutdown.test.ts +184 -2
- package/src/lib/shutdown.ts +104 -34
- package/src/lib/template-category.ts +3 -3
- package/src/lib/template-locator.ts +8 -2
- package/src/lib/ws-hub.ts +72 -46
- package/src/logger.ts +39 -14
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/console-dist/assets/index-hUJSpKtB.js +0 -119
- package/src/commands/start.ts +0 -2023
package/src/lib/admin-server.ts
CHANGED
|
@@ -6,11 +6,35 @@ import type {
|
|
|
6
6
|
EventLogQuery,
|
|
7
7
|
EventLogResult,
|
|
8
8
|
InstanceDetail,
|
|
9
|
+
Manifest,
|
|
9
10
|
ModelSchema,
|
|
10
11
|
TemplateRun,
|
|
11
12
|
} from "@miravo/core";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
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";
|
|
14
38
|
import type { WSClientData } from "./ws-hub.ts";
|
|
15
39
|
import { ALL_CHANNELS, WSHub } from "./ws-hub.ts";
|
|
16
40
|
|
|
@@ -22,6 +46,8 @@ const VALID_EVENT_TYPES: ReadonlySet<string> = new Set<LoggedEvent["type"]>([
|
|
|
22
46
|
"instance:created",
|
|
23
47
|
"instance:removed",
|
|
24
48
|
"engine:state-changed",
|
|
49
|
+
"platform:resource",
|
|
50
|
+
"platform:operation",
|
|
25
51
|
]);
|
|
26
52
|
|
|
27
53
|
interface AdminEngine {
|
|
@@ -42,6 +68,7 @@ interface AdminEngine {
|
|
|
42
68
|
getModelSchema(name: string): ModelSchema | undefined;
|
|
43
69
|
getEventLog(query?: EventLogQuery): EventLogResult;
|
|
44
70
|
getRunningTemplates(): readonly TemplateRun[];
|
|
71
|
+
getManifest(): Manifest;
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
/** Network binding configuration. */
|
|
@@ -59,8 +86,20 @@ interface AdminServerProtocolOptions {
|
|
|
59
86
|
/** WebSocket streaming configuration. When provided, enables the /ws endpoint. */
|
|
60
87
|
interface AdminServerWSOptions {
|
|
61
88
|
readonly bus: import("@miravo/core").EventBus;
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
interface AdminServerOptions {
|
|
@@ -69,12 +108,33 @@ interface AdminServerOptions {
|
|
|
69
108
|
readonly protocols: AdminServerProtocolOptions;
|
|
70
109
|
/** WebSocket streaming. When provided, enables /ws endpoint. */
|
|
71
110
|
readonly ws?: AdminServerWSOptions | undefined;
|
|
72
|
-
/**
|
|
73
|
-
readonly
|
|
111
|
+
/** Authentication configuration. */
|
|
112
|
+
readonly auth: AdminServerAuth;
|
|
113
|
+
/** Event log for platform operation tracking. */
|
|
114
|
+
readonly eventLog?: import("@miravo/core").EventLog | undefined;
|
|
74
115
|
/** Path to console static assets directory. When set, serves the SPA at non-API routes. */
|
|
75
116
|
readonly consoleDir?: string | undefined;
|
|
76
117
|
/** Template catalog provider for GET /catalog/templates. */
|
|
77
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;
|
|
78
138
|
/** Simulation name for daemon identity verification via /readyz. */
|
|
79
139
|
readonly simulationName?: string | undefined;
|
|
80
140
|
/** Run ID for daemon identity verification via /readyz. */
|
|
@@ -83,175 +143,12 @@ interface AdminServerOptions {
|
|
|
83
143
|
readonly requestShutdown?: ((reason: string) => void) | undefined;
|
|
84
144
|
}
|
|
85
145
|
|
|
86
|
-
interface AdminServerStatus {
|
|
87
|
-
readonly status: "ok" | "fail";
|
|
88
|
-
readonly reason?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
146
|
interface AdminServerHandle {
|
|
92
147
|
readonly endpoint: string;
|
|
93
148
|
readonly wsHub: WSHub | null;
|
|
94
149
|
stop(): void;
|
|
95
150
|
}
|
|
96
151
|
|
|
97
|
-
interface HealthResponseBody {
|
|
98
|
-
readonly status: "ok" | "fail";
|
|
99
|
-
readonly simulationState: EngineCoreMetrics["state"];
|
|
100
|
-
readonly tickCount: number;
|
|
101
|
-
readonly mqttEnabled: boolean;
|
|
102
|
-
readonly mqttConnected: boolean | null;
|
|
103
|
-
readonly opcuaEnabled: boolean;
|
|
104
|
-
readonly opcuaConnected: boolean | null;
|
|
105
|
-
readonly stalled: boolean;
|
|
106
|
-
readonly reason?: string;
|
|
107
|
-
readonly simulationName?: string;
|
|
108
|
-
readonly runId?: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface AdminErrorResponse {
|
|
112
|
-
readonly message: string;
|
|
113
|
-
readonly error: string;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function jsonResponse(body: unknown, status: number): Response {
|
|
117
|
-
return new Response(JSON.stringify(body), {
|
|
118
|
-
status,
|
|
119
|
-
headers: {
|
|
120
|
-
"content-type": "application/json; charset=utf-8",
|
|
121
|
-
"cache-control": "no-store",
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function plainResponse(body: string, status: number, allow?: string): Response {
|
|
127
|
-
return new Response(body, {
|
|
128
|
-
status,
|
|
129
|
-
headers: {
|
|
130
|
-
"content-type": "text/plain; charset=utf-8",
|
|
131
|
-
"cache-control": "no-store",
|
|
132
|
-
...(allow ? { allow } : {}),
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function livenessStatus(input: {
|
|
138
|
-
readonly core: EngineCoreMetrics;
|
|
139
|
-
readonly stalled: boolean;
|
|
140
|
-
}): AdminServerStatus {
|
|
141
|
-
if (input.core.state === "stopped") {
|
|
142
|
-
return { status: "fail", reason: 'engine state is "stopped"' };
|
|
143
|
-
}
|
|
144
|
-
if (input.stalled) {
|
|
145
|
-
return { status: "fail", reason: "tick loop stalled" };
|
|
146
|
-
}
|
|
147
|
-
return { status: "ok" };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function readinessStatus(input: {
|
|
151
|
-
readonly core: EngineCoreMetrics;
|
|
152
|
-
readonly stalled: boolean;
|
|
153
|
-
readonly mqttEnabled: boolean;
|
|
154
|
-
readonly mqttConnected: boolean;
|
|
155
|
-
readonly opcuaEnabled: boolean;
|
|
156
|
-
readonly opcuaConnected: boolean;
|
|
157
|
-
}): AdminServerStatus {
|
|
158
|
-
if (input.core.state !== "running") {
|
|
159
|
-
return { status: "fail", reason: `engine state is "${input.core.state}"` };
|
|
160
|
-
}
|
|
161
|
-
if (input.stalled) {
|
|
162
|
-
return { status: "fail", reason: "tick loop stalled" };
|
|
163
|
-
}
|
|
164
|
-
if (input.mqttEnabled && !input.mqttConnected) {
|
|
165
|
-
return { status: "fail", reason: "mqtt disconnected" };
|
|
166
|
-
}
|
|
167
|
-
if (input.opcuaEnabled && !input.opcuaConnected) {
|
|
168
|
-
return { status: "fail", reason: "opcua disconnected" };
|
|
169
|
-
}
|
|
170
|
-
return { status: "ok" };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function commandValidationErrorResponse(
|
|
174
|
-
commandType: string,
|
|
175
|
-
correlationId: string | undefined,
|
|
176
|
-
issues: string,
|
|
177
|
-
): Response {
|
|
178
|
-
return jsonResponse(
|
|
179
|
-
{
|
|
180
|
-
success: false,
|
|
181
|
-
command: commandType,
|
|
182
|
-
correlationId,
|
|
183
|
-
message: `Command validation failed: ${issues}`,
|
|
184
|
-
error:
|
|
185
|
-
"Check the command shape. Required: { type, ...fields }. See documentation for valid command types.",
|
|
186
|
-
},
|
|
187
|
-
400,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function extractCorrelationId(payload: unknown): string | undefined {
|
|
192
|
-
if (typeof payload !== "object" || payload === null) {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
const maybeCorrelationId = (payload as Record<string, unknown>)["correlationId"];
|
|
196
|
-
return typeof maybeCorrelationId === "string" ? maybeCorrelationId : undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function extractCommandType(payload: unknown): string {
|
|
200
|
-
if (typeof payload !== "object" || payload === null) {
|
|
201
|
-
return "unknown";
|
|
202
|
-
}
|
|
203
|
-
const maybeType = (payload as Record<string, unknown>)["type"];
|
|
204
|
-
return typeof maybeType === "string" ? maybeType : "unknown";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function buildHealthBody(input: {
|
|
208
|
-
readonly metrics: { readonly core: EngineCoreMetrics };
|
|
209
|
-
readonly stalled: boolean;
|
|
210
|
-
readonly mqttEnabled: boolean;
|
|
211
|
-
readonly mqttConnected: boolean;
|
|
212
|
-
readonly opcuaEnabled: boolean;
|
|
213
|
-
readonly opcuaConnected: boolean;
|
|
214
|
-
}): Omit<HealthResponseBody, "status" | "reason"> {
|
|
215
|
-
return {
|
|
216
|
-
simulationState: input.metrics.core.state,
|
|
217
|
-
tickCount: input.metrics.core.tickCount,
|
|
218
|
-
mqttEnabled: input.mqttEnabled,
|
|
219
|
-
mqttConnected: input.mqttEnabled ? input.mqttConnected : null,
|
|
220
|
-
opcuaEnabled: input.opcuaEnabled,
|
|
221
|
-
opcuaConnected: input.opcuaEnabled ? input.opcuaConnected : null,
|
|
222
|
-
stalled: input.stalled,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function parseCommandRequest(request: Request): Promise<Response | EngineCommand> {
|
|
227
|
-
let payload: unknown;
|
|
228
|
-
try {
|
|
229
|
-
payload = await request.json();
|
|
230
|
-
} catch {
|
|
231
|
-
return jsonResponse(
|
|
232
|
-
{
|
|
233
|
-
message: "Invalid JSON in command request body.",
|
|
234
|
-
error: 'Send a valid JSON object. Example: {"type":"pause"}',
|
|
235
|
-
} satisfies AdminErrorResponse,
|
|
236
|
-
400,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const validation = EngineCommandSchema.safeParse(payload);
|
|
241
|
-
if (!validation.success) {
|
|
242
|
-
const issues = validation.error.issues
|
|
243
|
-
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
244
|
-
.join("; ");
|
|
245
|
-
return commandValidationErrorResponse(
|
|
246
|
-
extractCommandType(payload),
|
|
247
|
-
extractCorrelationId(payload),
|
|
248
|
-
issues,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return validation.data;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
152
|
/**
|
|
256
153
|
* Start the Miravo admin HTTP server.
|
|
257
154
|
*
|
|
@@ -266,73 +163,42 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
266
163
|
const { host, port } = options.network;
|
|
267
164
|
const { getLastTickProgressAt } = options.protocols;
|
|
268
165
|
|
|
269
|
-
const
|
|
270
|
-
const isLoopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
271
|
-
const isWildcard = host === "0.0.0.0" || host === "::";
|
|
272
|
-
|
|
273
|
-
/** Check bearer token on mutation endpoints. Returns error Response or null if OK. */
|
|
274
|
-
function checkAuth(request: Request): Response | null {
|
|
275
|
-
if (!adminToken) return null;
|
|
276
|
-
const authHeader = request.headers.get("authorization");
|
|
277
|
-
if (authHeader === `Bearer ${adminToken}`) return null;
|
|
278
|
-
return jsonResponse(
|
|
279
|
-
{ message: "Unauthorized.", error: "Provide a valid Authorization: Bearer <token> header." },
|
|
280
|
-
401,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
166
|
+
const { auth } = options;
|
|
283
167
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return jsonResponse(
|
|
288
|
-
{
|
|
289
|
-
message: "Mutation endpoints require authentication.",
|
|
290
|
-
error: "Set MIRAVO_ADMIN_TOKEN when binding to a non-loopback address.",
|
|
291
|
-
},
|
|
292
|
-
403,
|
|
293
|
-
);
|
|
168
|
+
function getSession(request: Request) {
|
|
169
|
+
const sessionId = extractSessionId(request);
|
|
170
|
+
return sessionId ? auth.sessionStore.validate(sessionId) : null;
|
|
294
171
|
}
|
|
295
172
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
/** Check Origin header on mutation endpoints for non-loopback binds. Returns error Response or null if OK. */
|
|
314
|
-
function checkOrigin(request: Request): Response | null {
|
|
315
|
-
// Loopback and wildcard binds skip Origin checking — wildcard (0.0.0.0)
|
|
316
|
-
// accepts connections from any interface, so we can't match a specific host.
|
|
317
|
-
if (isLoopback || isWildcard) return null;
|
|
318
|
-
const origin = request.headers.get("origin");
|
|
319
|
-
if (!origin) return null; // No origin = non-browser client, allow
|
|
320
|
-
try {
|
|
321
|
-
const originHost = new URL(origin).hostname;
|
|
322
|
-
if (originHost === host || originHost === "localhost" || originHost === "127.0.0.1")
|
|
323
|
-
return null;
|
|
324
|
-
} catch {
|
|
325
|
-
// Malformed origin
|
|
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
|
+
);
|
|
326
189
|
}
|
|
327
|
-
return
|
|
190
|
+
return null;
|
|
328
191
|
}
|
|
329
192
|
|
|
330
193
|
// Create WebSocket hub if ws config is provided
|
|
331
194
|
const wsHub = options.ws
|
|
332
195
|
? new WSHub({
|
|
333
196
|
bus: options.ws.bus,
|
|
334
|
-
|
|
335
|
-
|
|
197
|
+
snapshotSource: {
|
|
198
|
+
getState: () => engine.getState(),
|
|
199
|
+
getMetrics: () => engine.getMetrics(),
|
|
200
|
+
getRunningTemplates: () => engine.getRunningTemplates(),
|
|
201
|
+
},
|
|
336
202
|
})
|
|
337
203
|
: null;
|
|
338
204
|
|
|
@@ -355,8 +221,8 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
355
221
|
if (!wsHub) {
|
|
356
222
|
return plainResponse("WebSocket not available", 503);
|
|
357
223
|
}
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
224
|
+
const sessionBlock = checkSession(request);
|
|
225
|
+
if (sessionBlock) return sessionBlock;
|
|
360
226
|
const upgraded = server.upgrade(request, {
|
|
361
227
|
// WSClientData — Bun's untyped websocket generic requires the cast
|
|
362
228
|
data: { channels: new Set(ALL_CHANNELS) } as unknown as undefined,
|
|
@@ -377,6 +243,8 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
377
243
|
const metrics = engine.getMetrics();
|
|
378
244
|
|
|
379
245
|
if (pathname === "/metrics") {
|
|
246
|
+
const metricsAuth = checkSession(request);
|
|
247
|
+
if (metricsAuth) return metricsAuth;
|
|
380
248
|
return jsonResponse(metrics, 200);
|
|
381
249
|
}
|
|
382
250
|
|
|
@@ -448,21 +316,65 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
448
316
|
if (request.method !== "GET") {
|
|
449
317
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
450
318
|
}
|
|
319
|
+
const stateAuth = checkSession(request);
|
|
320
|
+
if (stateAuth) return stateAuth;
|
|
451
321
|
return jsonResponse(engine.getState(), 200);
|
|
452
322
|
}
|
|
453
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
|
+
|
|
454
370
|
if (pathname === "/commands") {
|
|
455
371
|
if (request.method !== "POST") {
|
|
456
372
|
return plainResponse("Method Not Allowed", 405, "POST");
|
|
457
373
|
}
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
374
|
+
const cmdAuth = checkSession(request);
|
|
375
|
+
if (cmdAuth) return cmdAuth;
|
|
460
376
|
const ctBlock = checkContentType(request);
|
|
461
377
|
if (ctBlock) return ctBlock;
|
|
462
|
-
const originBlock = checkOrigin(request);
|
|
463
|
-
if (originBlock) return originBlock;
|
|
464
|
-
const authBlock = checkAuth(request);
|
|
465
|
-
if (authBlock) return authBlock;
|
|
466
378
|
const commandOrError = await parseCommandRequest(request);
|
|
467
379
|
if (commandOrError instanceof Response) {
|
|
468
380
|
return commandOrError;
|
|
@@ -484,14 +396,10 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
484
396
|
501,
|
|
485
397
|
);
|
|
486
398
|
}
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
399
|
+
const shutdownAuth = checkSession(request);
|
|
400
|
+
if (shutdownAuth) return shutdownAuth;
|
|
489
401
|
const shutdownCtBlock = checkContentType(request);
|
|
490
402
|
if (shutdownCtBlock) return shutdownCtBlock;
|
|
491
|
-
const originBlock = checkOrigin(request);
|
|
492
|
-
if (originBlock) return originBlock;
|
|
493
|
-
const authBlock = checkAuth(request);
|
|
494
|
-
if (authBlock) return authBlock;
|
|
495
403
|
|
|
496
404
|
let reason = "remote shutdown request";
|
|
497
405
|
try {
|
|
@@ -517,6 +425,8 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
517
425
|
if (request.method !== "GET") {
|
|
518
426
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
519
427
|
}
|
|
428
|
+
const instanceAuth = checkSession(request);
|
|
429
|
+
if (instanceAuth) return instanceAuth;
|
|
520
430
|
const instanceId = pathname.slice("/instances/".length);
|
|
521
431
|
if (!instanceId) {
|
|
522
432
|
return jsonResponse(
|
|
@@ -539,6 +449,8 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
539
449
|
if (request.method !== "GET") {
|
|
540
450
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
541
451
|
}
|
|
452
|
+
const modelAuth = checkSession(request);
|
|
453
|
+
if (modelAuth) return modelAuth;
|
|
542
454
|
const modelName = pathname.slice("/models/".length);
|
|
543
455
|
if (!modelName) {
|
|
544
456
|
return jsonResponse({ message: "Missing model name.", error: "Use /models/{name}" }, 400);
|
|
@@ -558,6 +470,8 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
558
470
|
if (request.method !== "GET") {
|
|
559
471
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
560
472
|
}
|
|
473
|
+
const eventsAuth = checkSession(request);
|
|
474
|
+
if (eventsAuth) return eventsAuth;
|
|
561
475
|
const typeParam = url.searchParams.get("type");
|
|
562
476
|
if (typeParam && !VALID_EVENT_TYPES.has(typeParam)) {
|
|
563
477
|
return jsonResponse(
|
|
@@ -583,11 +497,197 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
583
497
|
return jsonResponse(engine.getEventLog(query), 200);
|
|
584
498
|
}
|
|
585
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
|
+
|
|
586
684
|
// GET /catalog/templates
|
|
587
685
|
if (pathname === "/catalog/templates") {
|
|
588
686
|
if (request.method !== "GET") {
|
|
589
687
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
590
688
|
}
|
|
689
|
+
const catalogAuth = checkSession(request);
|
|
690
|
+
if (catalogAuth) return catalogAuth;
|
|
591
691
|
const catalog = options.getCatalog ? options.getCatalog() : [];
|
|
592
692
|
return jsonResponse(catalog, 200);
|
|
593
693
|
}
|
|
@@ -597,9 +697,55 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
597
697
|
if (request.method !== "GET") {
|
|
598
698
|
return plainResponse("Method Not Allowed", 405, "GET");
|
|
599
699
|
}
|
|
700
|
+
const rtAuth = checkSession(request);
|
|
701
|
+
if (rtAuth) return rtAuth;
|
|
600
702
|
return jsonResponse(engine.getRunningTemplates(), 200);
|
|
601
703
|
}
|
|
602
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
|
+
|
|
603
749
|
// --- Console static file serving ---
|
|
604
750
|
if (consoleIndexFile && consoleDir) {
|
|
605
751
|
if (pathname !== "/") {
|
|
@@ -641,18 +787,11 @@ function startAdminServer(options: AdminServerOptions): AdminServerHandle {
|
|
|
641
787
|
endpoint: `http://${host}:${server.port}`,
|
|
642
788
|
wsHub,
|
|
643
789
|
stop() {
|
|
790
|
+
auth.sessionStore.clear();
|
|
644
791
|
wsHub?.close();
|
|
645
792
|
server.stop(true);
|
|
646
793
|
},
|
|
647
794
|
};
|
|
648
795
|
}
|
|
649
796
|
|
|
650
|
-
export type {
|
|
651
|
-
AdminEngine,
|
|
652
|
-
AdminServerHandle,
|
|
653
|
-
AdminServerNetworkOptions,
|
|
654
|
-
AdminServerOptions,
|
|
655
|
-
AdminServerProtocolOptions,
|
|
656
|
-
AdminServerWSOptions,
|
|
657
|
-
};
|
|
658
797
|
export { startAdminServer };
|