@a-company/sentinel 0.2.0 → 3.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/dist/adapters/express.d.ts +3 -1
- package/dist/adapters/fastify.d.ts +3 -1
- package/dist/adapters/hono.d.ts +3 -1
- package/dist/chunk-NTX74ZPM.js +2624 -0
- package/dist/chunk-VQ3SIN7S.js +422 -0
- package/dist/cli.js +6 -6
- package/dist/{commands-KIMGFR2I.js → commands-E7ACLOE7.js} +2367 -258
- package/dist/{dist-2F7NO4H4.js → dist-AG5JNIZU.js} +27 -2
- package/dist/{dist-BPWLYV4U.js → dist-TYG2XME3.js} +27 -2
- package/dist/index.d.ts +57 -5
- package/dist/index.js +288 -186
- package/dist/mcp.js +1538 -124
- package/dist/sdk-CzFDYYST.d.ts +180 -0
- package/dist/server/index.d.ts +20 -3
- package/dist/server/index.js +805 -9
- package/dist/storage-BKyt7aPJ.d.ts +319 -0
- package/dist/transport-DqamniUy.d.ts +185 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +10 -0
- package/dist/{sdk-B27_vK1g.d.ts → types-BmVoO1iF.d.ts} +196 -259
- package/package.json +15 -1
- package/ui/dist/assets/index-BuK-X6af.js +62 -0
- package/ui/dist/assets/index-BuK-X6af.js.map +1 -0
- package/ui/dist/assets/{index-DPxatSdT.css → index-DCtwmE2_.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/dist/chunk-KPMG4XED.js +0 -1249
- package/ui/dist/assets/index-BNgsn_C8.js +0 -62
- package/ui/dist/assets/index-BNgsn_C8.js.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
SentinelStorage
|
|
3
|
-
|
|
2
|
+
SentinelStorage,
|
|
3
|
+
loadServerConfig
|
|
4
|
+
} from "../chunk-NTX74ZPM.js";
|
|
4
5
|
|
|
5
6
|
// src/server/index.ts
|
|
6
7
|
import express from "express";
|
|
8
|
+
import * as http from "http";
|
|
7
9
|
import * as path3 from "path";
|
|
8
10
|
import * as fs2 from "fs";
|
|
9
11
|
import { fileURLToPath } from "url";
|
|
10
12
|
import chalk3 from "chalk";
|
|
13
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
11
14
|
|
|
12
15
|
// src/server/routes/symbols.ts
|
|
13
16
|
import { Router } from "express";
|
|
@@ -104,7 +107,7 @@ async function loadParadigmConfig(projectDir) {
|
|
|
104
107
|
}
|
|
105
108
|
async function loadWithPremiseCore(projectDir) {
|
|
106
109
|
try {
|
|
107
|
-
const { aggregateFromDirectory } = await import("../dist-
|
|
110
|
+
const { aggregateFromDirectory } = await import("../dist-AG5JNIZU.js");
|
|
108
111
|
log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
|
|
109
112
|
const result = await aggregateFromDirectory(projectDir);
|
|
110
113
|
const counts = {};
|
|
@@ -762,6 +765,655 @@ function createPatternsRouter(_projectDir) {
|
|
|
762
765
|
return router;
|
|
763
766
|
}
|
|
764
767
|
|
|
768
|
+
// src/server/routes/logs.ts
|
|
769
|
+
import { Router as Router6 } from "express";
|
|
770
|
+
import { v4 as uuidv4 } from "uuid";
|
|
771
|
+
function inferSymbolType(symbol) {
|
|
772
|
+
if (symbol.startsWith("#")) return "component";
|
|
773
|
+
if (symbol.startsWith("^")) return "gate";
|
|
774
|
+
if (symbol.startsWith("!")) return "signal";
|
|
775
|
+
if (symbol.startsWith("$")) return "flow";
|
|
776
|
+
if (symbol.startsWith("~")) return "aspect";
|
|
777
|
+
return "raw";
|
|
778
|
+
}
|
|
779
|
+
function validateSymbol(symbol, index) {
|
|
780
|
+
const entry = index.find((e) => e.symbol === symbol);
|
|
781
|
+
if (entry) return { known: true };
|
|
782
|
+
const symbolName = symbol.replace(/^[#^!$~]/, "");
|
|
783
|
+
let bestMatch;
|
|
784
|
+
let bestScore = 0;
|
|
785
|
+
for (const e of index) {
|
|
786
|
+
const eName = e.symbol.replace(/^[#^!$~]/, "");
|
|
787
|
+
let shared = 0;
|
|
788
|
+
for (let i = 0; i < Math.min(symbolName.length, eName.length); i++) {
|
|
789
|
+
if (symbolName[i] === eName[i]) shared++;
|
|
790
|
+
else break;
|
|
791
|
+
}
|
|
792
|
+
const score = shared / Math.max(symbolName.length, eName.length);
|
|
793
|
+
if (score > bestScore && score > 0.5) {
|
|
794
|
+
bestScore = score;
|
|
795
|
+
bestMatch = e.symbol;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return { known: false, suggestion: bestMatch };
|
|
799
|
+
}
|
|
800
|
+
function autoPromoteToIncident(entry, storage) {
|
|
801
|
+
try {
|
|
802
|
+
const symbolType = inferSymbolType(entry.symbol);
|
|
803
|
+
const symbols = {};
|
|
804
|
+
if (symbolType === "component") symbols.component = entry.symbol;
|
|
805
|
+
else if (symbolType === "gate") symbols.gate = entry.symbol;
|
|
806
|
+
else if (symbolType === "signal") symbols.signal = entry.symbol;
|
|
807
|
+
else if (symbolType === "flow") symbols.flow = entry.symbol;
|
|
808
|
+
else symbols.component = entry.symbol;
|
|
809
|
+
storage.recordIncident({
|
|
810
|
+
error: {
|
|
811
|
+
message: entry.message,
|
|
812
|
+
type: "LogError"
|
|
813
|
+
},
|
|
814
|
+
symbols,
|
|
815
|
+
environment: entry.environment || "unknown",
|
|
816
|
+
service: entry.service
|
|
817
|
+
});
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
var insertsSincePrune = 0;
|
|
822
|
+
function createLogsRouter(options) {
|
|
823
|
+
const router = Router6();
|
|
824
|
+
const { storage, serverConfig, onLogReceived, symbolIndex } = options;
|
|
825
|
+
router.post("/", async (req, res) => {
|
|
826
|
+
try {
|
|
827
|
+
const body = req.body;
|
|
828
|
+
let entries;
|
|
829
|
+
if (Array.isArray(body.entries)) {
|
|
830
|
+
entries = body.entries;
|
|
831
|
+
} else if (body.level && body.symbol && body.message && body.service) {
|
|
832
|
+
entries = [body];
|
|
833
|
+
} else {
|
|
834
|
+
res.status(400).json({ error: "Expected {entries: [...]} or a single log entry with level, symbol, message, service" });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (entries.length > serverConfig.maxBatchSize) {
|
|
838
|
+
res.status(413).json({
|
|
839
|
+
error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
for (let i = 0; i < entries.length; i++) {
|
|
844
|
+
const e = entries[i];
|
|
845
|
+
if (!e.level || !e.symbol || !e.message || !e.service) {
|
|
846
|
+
res.status(400).json({
|
|
847
|
+
error: `Entry ${i}: missing required fields (level, symbol, message, service)`
|
|
848
|
+
});
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!["debug", "info", "warn", "error"].includes(e.level)) {
|
|
852
|
+
res.status(400).json({
|
|
853
|
+
error: `Entry ${i}: invalid level "${e.level}", must be debug|info|warn|error`
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const result = storage.insertLogBatch(entries);
|
|
859
|
+
for (const input of entries) {
|
|
860
|
+
const entry = {
|
|
861
|
+
id: input.id || uuidv4(),
|
|
862
|
+
timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
863
|
+
level: input.level,
|
|
864
|
+
symbol: input.symbol,
|
|
865
|
+
symbolType: input.symbolType || inferSymbolType(input.symbol),
|
|
866
|
+
message: input.message,
|
|
867
|
+
data: input.data,
|
|
868
|
+
service: input.service,
|
|
869
|
+
sessionId: input.sessionId,
|
|
870
|
+
correlationId: input.correlationId,
|
|
871
|
+
durationMs: input.durationMs,
|
|
872
|
+
environment: input.environment
|
|
873
|
+
};
|
|
874
|
+
let validation;
|
|
875
|
+
if (symbolIndex) {
|
|
876
|
+
validation = validateSymbol(entry.symbol, symbolIndex);
|
|
877
|
+
}
|
|
878
|
+
if (entry.level === "error") {
|
|
879
|
+
autoPromoteToIncident(entry, storage);
|
|
880
|
+
}
|
|
881
|
+
if (onLogReceived) {
|
|
882
|
+
onLogReceived(entry, validation);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
insertsSincePrune += result.accepted;
|
|
886
|
+
if (serverConfig.maxLogs > 0 && insertsSincePrune >= serverConfig.pruneIntervalInserts) {
|
|
887
|
+
insertsSincePrune = 0;
|
|
888
|
+
storage.pruneLogs(serverConfig.maxLogs);
|
|
889
|
+
}
|
|
890
|
+
res.json({ accepted: result.accepted, errors: result.errors.length > 0 ? result.errors : void 0 });
|
|
891
|
+
} catch (error) {
|
|
892
|
+
res.status(500).json({ error: "Failed to insert logs" });
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
router.get("/", async (req, res) => {
|
|
896
|
+
try {
|
|
897
|
+
const options2 = {
|
|
898
|
+
level: req.query.level,
|
|
899
|
+
symbol: req.query.symbol,
|
|
900
|
+
service: req.query.service,
|
|
901
|
+
sessionId: req.query.sessionId,
|
|
902
|
+
correlationId: req.query.correlationId,
|
|
903
|
+
search: req.query.search,
|
|
904
|
+
since: req.query.since,
|
|
905
|
+
until: req.query.until,
|
|
906
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
907
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
908
|
+
};
|
|
909
|
+
const logs = storage.queryLogs(options2);
|
|
910
|
+
const total = storage.getLogCount(options2);
|
|
911
|
+
res.json({ count: logs.length, total, logs });
|
|
912
|
+
} catch (error) {
|
|
913
|
+
res.status(500).json({ error: "Failed to query logs" });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
return router;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/server/routes/services.ts
|
|
920
|
+
import { Router as Router7 } from "express";
|
|
921
|
+
function createServicesRouter(options) {
|
|
922
|
+
const router = Router7();
|
|
923
|
+
const { storage } = options;
|
|
924
|
+
router.post("/", async (req, res) => {
|
|
925
|
+
try {
|
|
926
|
+
const { name, version, pid, environment, metadata } = req.body;
|
|
927
|
+
if (!name) {
|
|
928
|
+
res.status(400).json({ error: "Missing required field: name" });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
storage.registerService({ name, version, pid, environment, metadata });
|
|
932
|
+
res.json({ success: true, service: name });
|
|
933
|
+
} catch (error) {
|
|
934
|
+
res.status(500).json({ error: "Failed to register service" });
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
router.get("/", async (_req, res) => {
|
|
938
|
+
try {
|
|
939
|
+
const services = storage.getServices();
|
|
940
|
+
res.json({ count: services.length, services });
|
|
941
|
+
} catch (error) {
|
|
942
|
+
res.status(500).json({ error: "Failed to list services" });
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
return router;
|
|
946
|
+
}
|
|
947
|
+
function createStateRouter(options) {
|
|
948
|
+
const router = Router7();
|
|
949
|
+
const { storage } = options;
|
|
950
|
+
router.post("/", async (req, res) => {
|
|
951
|
+
try {
|
|
952
|
+
const { service, sessionId, state, activeFlows, activeGates } = req.body;
|
|
953
|
+
if (!service || !sessionId || !state) {
|
|
954
|
+
res.status(400).json({ error: "Missing required fields: service, sessionId, state" });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
storage.upsertAppState({
|
|
958
|
+
service,
|
|
959
|
+
sessionId,
|
|
960
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
961
|
+
state,
|
|
962
|
+
activeFlows,
|
|
963
|
+
activeGates
|
|
964
|
+
});
|
|
965
|
+
storage.updateServiceLastSeen(service);
|
|
966
|
+
res.json({ success: true });
|
|
967
|
+
} catch (error) {
|
|
968
|
+
res.status(500).json({ error: "Failed to update state" });
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
router.get("/", async (_req, res) => {
|
|
972
|
+
try {
|
|
973
|
+
const states = storage.getAllAppStates();
|
|
974
|
+
res.json({ count: states.length, states });
|
|
975
|
+
} catch (error) {
|
|
976
|
+
res.status(500).json({ error: "Failed to get states" });
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
router.get("/:service", async (req, res) => {
|
|
980
|
+
try {
|
|
981
|
+
const states = storage.getAppState(req.params.service);
|
|
982
|
+
res.json({ count: states.length, states });
|
|
983
|
+
} catch (error) {
|
|
984
|
+
res.status(500).json({ error: "Failed to get service state" });
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
return router;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/server/routes/metrics.ts
|
|
991
|
+
import { Router as Router8 } from "express";
|
|
992
|
+
var VALID_METRIC_TYPES = ["counter", "gauge", "histogram"];
|
|
993
|
+
function createMetricsRouter(options) {
|
|
994
|
+
const router = Router8();
|
|
995
|
+
const { storage, serverConfig } = options;
|
|
996
|
+
router.post("/", (req, res) => {
|
|
997
|
+
try {
|
|
998
|
+
const body = req.body;
|
|
999
|
+
let entries;
|
|
1000
|
+
if (Array.isArray(body.entries)) {
|
|
1001
|
+
entries = body.entries;
|
|
1002
|
+
} else if (body.name && body.type && body.value !== void 0 && body.service) {
|
|
1003
|
+
entries = [body];
|
|
1004
|
+
} else {
|
|
1005
|
+
res.status(400).json({
|
|
1006
|
+
error: "Expected {entries: [...]} or a single metric with name, type, value, service"
|
|
1007
|
+
});
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (entries.length > serverConfig.maxBatchSize) {
|
|
1011
|
+
res.status(413).json({
|
|
1012
|
+
error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
|
|
1013
|
+
});
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1017
|
+
const e = entries[i];
|
|
1018
|
+
if (!e.name || !e.type || e.value === void 0 || !e.service) {
|
|
1019
|
+
res.status(400).json({
|
|
1020
|
+
error: `Entry ${i}: missing required fields (name, type, value, service)`
|
|
1021
|
+
});
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (!VALID_METRIC_TYPES.includes(e.type)) {
|
|
1025
|
+
res.status(400).json({
|
|
1026
|
+
error: `Entry ${i}: invalid type "${e.type}", must be counter|gauge|histogram`
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const result = storage.insertMetricBatch(entries);
|
|
1032
|
+
res.json({
|
|
1033
|
+
accepted: result.accepted,
|
|
1034
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
1035
|
+
});
|
|
1036
|
+
} catch {
|
|
1037
|
+
res.status(500).json({ error: "Failed to insert metrics" });
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
router.get("/", (req, res) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const options2 = {
|
|
1043
|
+
name: req.query.name,
|
|
1044
|
+
type: req.query.type,
|
|
1045
|
+
service: req.query.service,
|
|
1046
|
+
tag: req.query.tag,
|
|
1047
|
+
since: req.query.since,
|
|
1048
|
+
until: req.query.until,
|
|
1049
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
1050
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
1051
|
+
};
|
|
1052
|
+
const metrics = storage.queryMetrics(options2);
|
|
1053
|
+
const total = storage.getMetricCount(options2);
|
|
1054
|
+
res.json({ count: metrics.length, total, metrics });
|
|
1055
|
+
} catch {
|
|
1056
|
+
res.status(500).json({ error: "Failed to query metrics" });
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
router.get("/aggregate/:name", (req, res) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const aggregation = storage.aggregateMetric(req.params.name, {
|
|
1062
|
+
service: req.query.service,
|
|
1063
|
+
since: req.query.since,
|
|
1064
|
+
until: req.query.until
|
|
1065
|
+
});
|
|
1066
|
+
res.json(aggregation);
|
|
1067
|
+
} catch {
|
|
1068
|
+
res.status(500).json({ error: "Failed to aggregate metric" });
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
return router;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/server/routes/traces.ts
|
|
1075
|
+
import { Router as Router9 } from "express";
|
|
1076
|
+
function createTracesRouter(options) {
|
|
1077
|
+
const router = Router9();
|
|
1078
|
+
const { storage } = options;
|
|
1079
|
+
router.post("/", (req, res) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const body = req.body;
|
|
1082
|
+
if (!body.traceId || !body.service || !body.symbol || !body.operation) {
|
|
1083
|
+
res.status(400).json({
|
|
1084
|
+
error: "Missing required fields: traceId, service, symbol, operation"
|
|
1085
|
+
});
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const spanId = storage.insertSpan(body);
|
|
1089
|
+
res.json({ spanId, traceId: body.traceId });
|
|
1090
|
+
} catch {
|
|
1091
|
+
res.status(500).json({ error: "Failed to insert trace span" });
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
router.get("/", (req, res) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const traces = storage.queryTraces({
|
|
1097
|
+
service: req.query.service,
|
|
1098
|
+
symbol: req.query.symbol,
|
|
1099
|
+
since: req.query.since,
|
|
1100
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 20
|
|
1101
|
+
});
|
|
1102
|
+
res.json({ count: traces.length, traces });
|
|
1103
|
+
} catch {
|
|
1104
|
+
res.status(500).json({ error: "Failed to query traces" });
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
router.get("/:traceId", (req, res) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const trace = storage.getTrace(req.params.traceId);
|
|
1110
|
+
if (!trace) {
|
|
1111
|
+
res.status(404).json({ error: "Trace not found" });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
res.json(trace);
|
|
1115
|
+
} catch {
|
|
1116
|
+
res.status(500).json({ error: "Failed to get trace" });
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
return router;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/server/routes/schemas.ts
|
|
1123
|
+
import { Router as Router10 } from "express";
|
|
1124
|
+
function createSchemasRouter(options) {
|
|
1125
|
+
const router = Router10();
|
|
1126
|
+
const { storage } = options;
|
|
1127
|
+
router.post("/", (req, res) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const body = req.body;
|
|
1130
|
+
if (!body.id || !body.version || !body.name || !body.scope || !body.eventTypes) {
|
|
1131
|
+
res.status(400).json({
|
|
1132
|
+
error: "Missing required fields: id, version, name, scope, eventTypes"
|
|
1133
|
+
});
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (!body.scope.field || !body.scope.type || !body.scope.label || !body.scope.ordering) {
|
|
1137
|
+
res.status(400).json({
|
|
1138
|
+
error: "Invalid scope: requires field, type, label, ordering"
|
|
1139
|
+
});
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (!Array.isArray(body.eventTypes) || body.eventTypes.length === 0) {
|
|
1143
|
+
res.status(400).json({
|
|
1144
|
+
error: "eventTypes must be a non-empty array"
|
|
1145
|
+
});
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
for (let i = 0; i < body.eventTypes.length; i++) {
|
|
1149
|
+
const et = body.eventTypes[i];
|
|
1150
|
+
if (!et.type || !et.category) {
|
|
1151
|
+
res.status(400).json({
|
|
1152
|
+
error: `eventTypes[${i}]: missing required fields (type, category)`
|
|
1153
|
+
});
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const schema = storage.registerSchema(body);
|
|
1158
|
+
res.status(201).json(schema);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
res.status(500).json({ error: "Failed to register schema" });
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
router.get("/", (_req, res) => {
|
|
1164
|
+
try {
|
|
1165
|
+
const schemas = storage.listSchemas();
|
|
1166
|
+
res.json({ count: schemas.length, schemas });
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
res.status(500).json({ error: "Failed to list schemas" });
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
router.get("/:id", (req, res) => {
|
|
1172
|
+
try {
|
|
1173
|
+
const schema = storage.getSchema(req.params.id);
|
|
1174
|
+
if (!schema) {
|
|
1175
|
+
res.status(404).json({ error: "Schema not found" });
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
res.json(schema);
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
res.status(500).json({ error: "Failed to get schema" });
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
return router;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/server/routes/events.ts
|
|
1187
|
+
import { Router as Router11 } from "express";
|
|
1188
|
+
function createEventsRouter(options) {
|
|
1189
|
+
const router = Router11();
|
|
1190
|
+
const { storage, serverConfig, onEventReceived } = options;
|
|
1191
|
+
let insertsSincePrune2 = 0;
|
|
1192
|
+
router.post("/", (req, res) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const body = req.body;
|
|
1195
|
+
if (!body.schemaId || !body.service || !Array.isArray(body.events)) {
|
|
1196
|
+
res.status(400).json({
|
|
1197
|
+
error: "Expected { schemaId, service, events: [...] }"
|
|
1198
|
+
});
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const { schemaId, service, events } = body;
|
|
1202
|
+
const schema = storage.getSchema(schemaId);
|
|
1203
|
+
if (!schema) {
|
|
1204
|
+
res.status(404).json({
|
|
1205
|
+
error: `Schema "${schemaId}" not found. Register it first via POST /api/schemas.`
|
|
1206
|
+
});
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (events.length > serverConfig.maxBatchSize) {
|
|
1210
|
+
res.status(413).json({
|
|
1211
|
+
error: `Batch too large: ${events.length} events, max ${serverConfig.maxBatchSize}`
|
|
1212
|
+
});
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
for (let i = 0; i < events.length; i++) {
|
|
1216
|
+
const e = events[i];
|
|
1217
|
+
if (!e.type) {
|
|
1218
|
+
res.status(400).json({
|
|
1219
|
+
error: `events[${i}]: missing required field "type"`
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const result = storage.insertEventBatch(schemaId, service, events);
|
|
1225
|
+
if (onEventReceived) {
|
|
1226
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
1227
|
+
for (const et of schema.eventTypes) {
|
|
1228
|
+
typeMap.set(et.type, et.category);
|
|
1229
|
+
}
|
|
1230
|
+
for (const input of events) {
|
|
1231
|
+
const evt = {
|
|
1232
|
+
id: input.id || "",
|
|
1233
|
+
schemaId,
|
|
1234
|
+
eventType: input.type,
|
|
1235
|
+
category: typeMap.get(input.type) || "unknown",
|
|
1236
|
+
timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1237
|
+
scopeValue: input.scopeValue != null ? String(input.scopeValue) : void 0,
|
|
1238
|
+
sessionId: input.sessionId,
|
|
1239
|
+
service,
|
|
1240
|
+
data: input.data,
|
|
1241
|
+
severity: input.severity || "info",
|
|
1242
|
+
parentEventId: input.parentEventId,
|
|
1243
|
+
depth: input.depth
|
|
1244
|
+
};
|
|
1245
|
+
onEventReceived(evt);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
insertsSincePrune2 += result.accepted;
|
|
1249
|
+
if (serverConfig.maxLogs > 0 && insertsSincePrune2 >= serverConfig.pruneIntervalInserts) {
|
|
1250
|
+
insertsSincePrune2 = 0;
|
|
1251
|
+
storage.pruneEvents(serverConfig.maxLogs);
|
|
1252
|
+
}
|
|
1253
|
+
res.json({
|
|
1254
|
+
accepted: result.accepted,
|
|
1255
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
1256
|
+
});
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
res.status(500).json({ error: "Failed to ingest events" });
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
router.get("/", (req, res) => {
|
|
1262
|
+
try {
|
|
1263
|
+
const query = {
|
|
1264
|
+
schemaId: req.query.schemaId,
|
|
1265
|
+
eventType: req.query.eventType,
|
|
1266
|
+
category: req.query.category,
|
|
1267
|
+
service: req.query.service,
|
|
1268
|
+
sessionId: req.query.sessionId,
|
|
1269
|
+
scopeValue: req.query.scopeValue,
|
|
1270
|
+
scopeFrom: req.query.scopeFrom,
|
|
1271
|
+
scopeTo: req.query.scopeTo,
|
|
1272
|
+
severity: req.query.severity,
|
|
1273
|
+
since: req.query.since,
|
|
1274
|
+
until: req.query.until,
|
|
1275
|
+
search: req.query.search,
|
|
1276
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
1277
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
1278
|
+
};
|
|
1279
|
+
const events = storage.queryEvents(query);
|
|
1280
|
+
const total = storage.getEventCount(query);
|
|
1281
|
+
res.json({ count: events.length, total, events });
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
res.status(500).json({ error: "Failed to query events" });
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
router.get("/scopes", (req, res) => {
|
|
1287
|
+
try {
|
|
1288
|
+
const schemaId = req.query.schemaId;
|
|
1289
|
+
if (!schemaId) {
|
|
1290
|
+
res.status(400).json({ error: "schemaId query parameter is required" });
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const scopes = storage.getEventScopes(schemaId, {
|
|
1294
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
1295
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0,
|
|
1296
|
+
sessionId: req.query.sessionId
|
|
1297
|
+
});
|
|
1298
|
+
res.json({ count: scopes.length, scopes });
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
res.status(500).json({ error: "Failed to query scopes" });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
router.get("/scope/:value", (req, res) => {
|
|
1304
|
+
try {
|
|
1305
|
+
const schemaId = req.query.schemaId;
|
|
1306
|
+
if (!schemaId) {
|
|
1307
|
+
res.status(400).json({ error: "schemaId query parameter is required" });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const events = storage.queryEventsByScope(schemaId, req.params.value);
|
|
1311
|
+
res.json({ count: events.length, events });
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
res.status(500).json({ error: "Failed to query scope events" });
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
return router;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/server/middleware/auth.ts
|
|
1320
|
+
function createAuthMiddleware(config) {
|
|
1321
|
+
return function authMiddleware(requiredPermission) {
|
|
1322
|
+
return (req, res, next) => {
|
|
1323
|
+
if (!config.enabled) {
|
|
1324
|
+
next();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const authHeader = req.headers.authorization;
|
|
1328
|
+
if (!authHeader) {
|
|
1329
|
+
res.status(401).json({ error: "Authentication required. Provide Authorization: Bearer <token>" });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
1333
|
+
if (!match) {
|
|
1334
|
+
res.status(401).json({ error: "Invalid authorization format. Use: Bearer <token>" });
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const tokenValue = match[1];
|
|
1338
|
+
const tokenEntry = config.tokens.find((t) => t.token === tokenValue);
|
|
1339
|
+
if (!tokenEntry) {
|
|
1340
|
+
res.status(401).json({ error: "Invalid token" });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (tokenEntry.expiresAt && new Date(tokenEntry.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
1344
|
+
res.status(401).json({ error: "Token expired" });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const permissionLevel = { read: 1, write: 2, admin: 3 };
|
|
1348
|
+
const hasPermission = tokenEntry.permissions.some(
|
|
1349
|
+
(p) => permissionLevel[p] >= permissionLevel[requiredPermission]
|
|
1350
|
+
);
|
|
1351
|
+
if (!hasPermission) {
|
|
1352
|
+
res.status(403).json({ error: `Insufficient permissions. Required: ${requiredPermission}` });
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
req.authToken = tokenEntry;
|
|
1356
|
+
next();
|
|
1357
|
+
};
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/server/middleware/rate-limit.ts
|
|
1362
|
+
var serviceWindows = /* @__PURE__ */ new Map();
|
|
1363
|
+
var globalWindow = { count: 0, windowStart: Date.now() };
|
|
1364
|
+
var WINDOW_MS = 6e4;
|
|
1365
|
+
function getOrResetWindow(entry) {
|
|
1366
|
+
const now = Date.now();
|
|
1367
|
+
if (now - entry.windowStart > WINDOW_MS) {
|
|
1368
|
+
entry.count = 0;
|
|
1369
|
+
entry.windowStart = now;
|
|
1370
|
+
}
|
|
1371
|
+
return entry;
|
|
1372
|
+
}
|
|
1373
|
+
function createRateLimiter(config) {
|
|
1374
|
+
return (req, res, next) => {
|
|
1375
|
+
if (!config.enabled) {
|
|
1376
|
+
next();
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
const service = req.body?.service || req.body?.entries?.[0]?.service || req.query.service || "_unknown";
|
|
1380
|
+
const rule = config.perService[service] || config.global;
|
|
1381
|
+
if (rule.samplingRate < 1 && Math.random() > rule.samplingRate) {
|
|
1382
|
+
res.status(200).json({ accepted: 0, sampled: true, message: "Request dropped by sampling" });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const gw = getOrResetWindow(globalWindow);
|
|
1386
|
+
if (gw.count >= config.global.maxRequestsPerMinute) {
|
|
1387
|
+
res.status(429).json({
|
|
1388
|
+
error: "Global rate limit exceeded",
|
|
1389
|
+
retryAfterMs: WINDOW_MS - (Date.now() - gw.windowStart)
|
|
1390
|
+
});
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (!serviceWindows.has(service)) {
|
|
1394
|
+
serviceWindows.set(service, { count: 0, windowStart: Date.now() });
|
|
1395
|
+
}
|
|
1396
|
+
const sw = getOrResetWindow(serviceWindows.get(service));
|
|
1397
|
+
if (sw.count >= rule.maxRequestsPerMinute) {
|
|
1398
|
+
res.status(429).json({
|
|
1399
|
+
error: `Rate limit exceeded for service: ${service}`,
|
|
1400
|
+
retryAfterMs: WINDOW_MS - (Date.now() - sw.windowStart)
|
|
1401
|
+
});
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const batchSize = req.body?.entries?.length || 1;
|
|
1405
|
+
if (batchSize > rule.maxEntriesPerBatch) {
|
|
1406
|
+
res.status(413).json({
|
|
1407
|
+
error: `Batch too large: ${batchSize} entries, max ${rule.maxEntriesPerBatch} for service ${service}`
|
|
1408
|
+
});
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
gw.count++;
|
|
1412
|
+
sw.count++;
|
|
1413
|
+
next();
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
765
1417
|
// src/server/index.ts
|
|
766
1418
|
var __filename = fileURLToPath(import.meta.url);
|
|
767
1419
|
var __dirname = path3.dirname(__filename);
|
|
@@ -790,11 +1442,20 @@ var log3 = {
|
|
|
790
1442
|
};
|
|
791
1443
|
function createApp(options) {
|
|
792
1444
|
const app = express();
|
|
793
|
-
app.use(express.json());
|
|
1445
|
+
app.use(express.json({ limit: "5mb" }));
|
|
794
1446
|
app.use((_req, res, next) => {
|
|
795
|
-
|
|
1447
|
+
const corsOrigin = options.serverConfig?.cors?.origin;
|
|
1448
|
+
const origin = Array.isArray(corsOrigin) ? corsOrigin.join(", ") : corsOrigin ?? "*";
|
|
1449
|
+
res.header("Access-Control-Allow-Origin", origin);
|
|
796
1450
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
797
|
-
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
1451
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1452
|
+
if (options.serverConfig?.cors?.credentials) {
|
|
1453
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
1454
|
+
}
|
|
1455
|
+
if (_req.method === "OPTIONS") {
|
|
1456
|
+
res.sendStatus(204);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
798
1459
|
next();
|
|
799
1460
|
});
|
|
800
1461
|
app.use("/api/symbols", createSymbolsRouter(options.projectDir));
|
|
@@ -802,6 +1463,34 @@ function createApp(options) {
|
|
|
802
1463
|
app.use("/api/commits", createCommitsRouter(options.projectDir));
|
|
803
1464
|
app.use("/api/incidents", createIncidentsRouter(options.projectDir));
|
|
804
1465
|
app.use("/api/patterns", createPatternsRouter(options.projectDir));
|
|
1466
|
+
if (options.storage && options.serverConfig) {
|
|
1467
|
+
const config = options.serverConfig;
|
|
1468
|
+
const auth = createAuthMiddleware(config.auth);
|
|
1469
|
+
const rateLimiter = createRateLimiter(config.rateLimit);
|
|
1470
|
+
app.use("/api/logs", rateLimiter, auth("write"), createLogsRouter({
|
|
1471
|
+
storage: options.storage,
|
|
1472
|
+
serverConfig: config,
|
|
1473
|
+
onLogReceived: options.onLogReceived,
|
|
1474
|
+
symbolIndex: options.symbolIndex
|
|
1475
|
+
}));
|
|
1476
|
+
app.use("/api/services", rateLimiter, auth("write"), createServicesRouter({ storage: options.storage }));
|
|
1477
|
+
app.use("/api/state", rateLimiter, auth("write"), createStateRouter({ storage: options.storage }));
|
|
1478
|
+
app.use("/api/metrics", rateLimiter, auth("write"), createMetricsRouter({
|
|
1479
|
+
storage: options.storage,
|
|
1480
|
+
serverConfig: config
|
|
1481
|
+
}));
|
|
1482
|
+
app.use("/api/traces", rateLimiter, auth("write"), createTracesRouter({
|
|
1483
|
+
storage: options.storage
|
|
1484
|
+
}));
|
|
1485
|
+
app.use("/api/schemas", rateLimiter, auth("write"), createSchemasRouter({
|
|
1486
|
+
storage: options.storage
|
|
1487
|
+
}));
|
|
1488
|
+
app.use("/api/events", rateLimiter, auth("write"), createEventsRouter({
|
|
1489
|
+
storage: options.storage,
|
|
1490
|
+
serverConfig: config,
|
|
1491
|
+
onEventReceived: options.onEventReceived
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
805
1494
|
app.get("/api/health", (_req, res) => {
|
|
806
1495
|
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
807
1496
|
});
|
|
@@ -817,12 +1506,119 @@ function createApp(options) {
|
|
|
817
1506
|
return app;
|
|
818
1507
|
}
|
|
819
1508
|
async function startServer(options) {
|
|
820
|
-
const
|
|
1509
|
+
const serverConfig = loadServerConfig(options.projectDir);
|
|
1510
|
+
if (options.logPruneLimit !== void 0) {
|
|
1511
|
+
serverConfig.maxLogs = options.logPruneLimit;
|
|
1512
|
+
}
|
|
1513
|
+
const storage = new SentinelStorage(options.dbPath);
|
|
1514
|
+
await storage.ensureReady();
|
|
1515
|
+
let symbolIndex = [];
|
|
1516
|
+
try {
|
|
1517
|
+
symbolIndex = await loadSymbolIndex(options.projectDir);
|
|
1518
|
+
} catch {
|
|
1519
|
+
log3.component("sentinel-server").warn("Could not load symbol index for validation");
|
|
1520
|
+
}
|
|
1521
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
1522
|
+
function broadcast(message) {
|
|
1523
|
+
const data = JSON.stringify(message);
|
|
1524
|
+
for (const client of wsClients) {
|
|
1525
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1526
|
+
client.send(data);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
function onLogReceived(entry, validation) {
|
|
1531
|
+
const message = { type: "log", entry };
|
|
1532
|
+
if (validation && !validation.known) {
|
|
1533
|
+
message.validation = validation;
|
|
1534
|
+
}
|
|
1535
|
+
broadcast(message);
|
|
1536
|
+
if (entry.symbolType === "signal" || entry.symbolType === "gate" || entry.symbolType === "flow") {
|
|
1537
|
+
broadcast({
|
|
1538
|
+
type: "flow_event",
|
|
1539
|
+
flowId: entry.symbolType === "flow" ? entry.symbol : void 0,
|
|
1540
|
+
nodeSymbol: entry.symbol,
|
|
1541
|
+
event: entry.symbolType,
|
|
1542
|
+
timestamp: entry.timestamp,
|
|
1543
|
+
service: entry.service
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function onEventReceived(event) {
|
|
1548
|
+
broadcast({ type: "event", event });
|
|
1549
|
+
}
|
|
1550
|
+
const app = createApp({
|
|
1551
|
+
...options,
|
|
1552
|
+
storage,
|
|
1553
|
+
serverConfig,
|
|
1554
|
+
symbolIndex,
|
|
1555
|
+
onLogReceived,
|
|
1556
|
+
onEventReceived
|
|
1557
|
+
});
|
|
821
1558
|
log3.component("sentinel-server").info("Starting server", { port: options.port });
|
|
822
1559
|
log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
|
|
823
1560
|
return new Promise((resolve, reject) => {
|
|
824
|
-
const
|
|
1561
|
+
const httpServer = http.createServer(app);
|
|
1562
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
1563
|
+
wss.on("connection", (ws) => {
|
|
1564
|
+
if (wsClients.size >= serverConfig.wsMaxSubscribers) {
|
|
1565
|
+
ws.close(1013, "Max subscribers reached");
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
wsClients.add(ws);
|
|
1569
|
+
log3.component("sentinel-ws").info("Client connected", { total: wsClients.size });
|
|
1570
|
+
ws.on("message", (raw) => {
|
|
1571
|
+
try {
|
|
1572
|
+
const msg = JSON.parse(raw.toString());
|
|
1573
|
+
if (msg.method === "ping") {
|
|
1574
|
+
ws.send(JSON.stringify({
|
|
1575
|
+
jsonrpc: "2.0",
|
|
1576
|
+
result: { pong: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1577
|
+
id: msg.id
|
|
1578
|
+
}));
|
|
1579
|
+
} else if (msg.method === "subscribe") {
|
|
1580
|
+
ws.send(JSON.stringify({
|
|
1581
|
+
jsonrpc: "2.0",
|
|
1582
|
+
result: { subscribed: true },
|
|
1583
|
+
id: msg.id
|
|
1584
|
+
}));
|
|
1585
|
+
} else if (msg.method === "query_logs") {
|
|
1586
|
+
const logs = storage.queryLogs(msg.params || {});
|
|
1587
|
+
ws.send(JSON.stringify({
|
|
1588
|
+
jsonrpc: "2.0",
|
|
1589
|
+
result: { logs },
|
|
1590
|
+
id: msg.id
|
|
1591
|
+
}));
|
|
1592
|
+
} else if (msg.method === "query_events") {
|
|
1593
|
+
const events = storage.queryEvents(msg.params || {});
|
|
1594
|
+
ws.send(JSON.stringify({
|
|
1595
|
+
jsonrpc: "2.0",
|
|
1596
|
+
result: { events },
|
|
1597
|
+
id: msg.id
|
|
1598
|
+
}));
|
|
1599
|
+
} else if (msg.method === "query_scopes") {
|
|
1600
|
+
const { schemaId, ...rest } = msg.params || {};
|
|
1601
|
+
const scopes = schemaId ? storage.getEventScopes(schemaId, rest) : [];
|
|
1602
|
+
ws.send(JSON.stringify({
|
|
1603
|
+
jsonrpc: "2.0",
|
|
1604
|
+
result: { scopes },
|
|
1605
|
+
id: msg.id
|
|
1606
|
+
}));
|
|
1607
|
+
}
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
ws.on("close", () => {
|
|
1612
|
+
wsClients.delete(ws);
|
|
1613
|
+
log3.component("sentinel-ws").info("Client disconnected", { total: wsClients.size });
|
|
1614
|
+
});
|
|
1615
|
+
ws.on("error", () => {
|
|
1616
|
+
wsClients.delete(ws);
|
|
1617
|
+
});
|
|
1618
|
+
});
|
|
1619
|
+
httpServer.listen(options.port, () => {
|
|
825
1620
|
log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
|
|
1621
|
+
log3.component("sentinel-ws").success("WebSocket ready", { url: `ws://localhost:${options.port}` });
|
|
826
1622
|
if (options.open) {
|
|
827
1623
|
import("open").then((openModule) => {
|
|
828
1624
|
openModule.default(`http://localhost:${options.port}`);
|
|
@@ -833,7 +1629,7 @@ async function startServer(options) {
|
|
|
833
1629
|
}
|
|
834
1630
|
resolve();
|
|
835
1631
|
});
|
|
836
|
-
|
|
1632
|
+
httpServer.on("error", (err) => {
|
|
837
1633
|
if (err.code === "EADDRINUSE") {
|
|
838
1634
|
log3.component("sentinel-server").error("Port already in use", { port: options.port });
|
|
839
1635
|
} else {
|