@aegis-fluxion/core 0.9.0 → 0.10.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Low-level encrypted WebSocket primitives for the `aegis-fluxion` ecosystem.
4
4
 
5
- Version: **0.9.0**
5
+ Version: **0.10.0**
6
6
 
7
7
  ---
8
8
 
@@ -10,6 +10,7 @@ Version: **0.9.0**
10
10
 
11
11
  - Ephemeral ECDH handshake (`prime256v1`)
12
12
  - AES-256-GCM encrypted envelopes
13
+ - Built-in telemetry via `getMetrics()` and `getMetricsPrometheus()`
13
14
  - ACK request/response (Promise + callback styles)
14
15
  - Encrypted chunked streaming for large `Buffer`/`Readable` payloads
15
16
  - Secure room routing (`join`, `leave`, `leaveAll`, `to(room).emit(...)`)
@@ -28,6 +29,122 @@ npm install @aegis-fluxion/core ws
28
29
 
29
30
  ---
30
31
 
32
+ ## Observability & telemetry (new in 0.10.0)
33
+
34
+ `SecureServer` exposes real-time metrics for operational visibility:
35
+
36
+ - active secure connections
37
+ - successful/failed handshakes (including resume attempts)
38
+ - encrypted message and byte throughput (ingress/egress)
39
+ - DDoS/rate-limit counters (blocked, throttled, disconnected)
40
+
41
+ ### JSON metrics snapshot
42
+
43
+ ```ts
44
+ import { SecureServer } from "@aegis-fluxion/core";
45
+
46
+ const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
47
+
48
+ const snapshot = server.getMetrics();
49
+ console.log(snapshot.activeConnections);
50
+ console.log(snapshot.encryptedMessagesReceivedTotal);
51
+ ```
52
+
53
+ ### Prometheus endpoint integration
54
+
55
+ ```ts
56
+ import { createServer } from "node:http";
57
+ import { SecureServer } from "@aegis-fluxion/core";
58
+
59
+ const secureServer = new SecureServer({ host: "127.0.0.1", port: 8080 });
60
+
61
+ createServer((request, response) => {
62
+ if (request.url === "/metrics") {
63
+ response.setHeader("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
64
+ response.end(secureServer.getMetricsPrometheus());
65
+ return;
66
+ }
67
+
68
+ response.statusCode = 404;
69
+ response.end("Not Found");
70
+ }).listen(9100, "127.0.0.1");
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Frontend integration (React)
76
+
77
+ `@aegis-fluxion/core` is server/runtime focused. For browser clients, pair it with
78
+ `@aegis-fluxion/browser-client`.
79
+
80
+ ### Node backend (`SecureServer`)
81
+
82
+ ```ts
83
+ import { SecureServer } from "@aegis-fluxion/core";
84
+
85
+ const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
86
+
87
+ server.on("feed:publish", async (payload) => {
88
+ server.emit("feed:message", payload);
89
+ return { ok: true };
90
+ });
91
+ ```
92
+
93
+ ### React frontend (`BrowserSecureClient`)
94
+
95
+ ```tsx
96
+ import { useEffect, useMemo, useState } from "react";
97
+ import { BrowserSecureClient } from "@aegis-fluxion/browser-client";
98
+
99
+ export function SecureFeedPanel() {
100
+ const [status, setStatus] = useState("connecting");
101
+ const [messages, setMessages] = useState<string[]>([]);
102
+
103
+ const client = useMemo(() => {
104
+ return new BrowserSecureClient("ws://127.0.0.1:8080", {
105
+ autoConnect: false,
106
+ reconnect: true
107
+ });
108
+ }, []);
109
+
110
+ useEffect(() => {
111
+ const onReady = () => setStatus("ready");
112
+ const onDisconnect = () => setStatus("disconnected");
113
+ const onFeedMessage = (payload: unknown) => {
114
+ const data = payload as { text?: string };
115
+ if (typeof data.text === "string") {
116
+ setMessages((prev) => [data.text, ...prev]);
117
+ }
118
+ };
119
+
120
+ client.on("ready", onReady);
121
+ client.on("disconnect", onDisconnect);
122
+ client.on("feed:message", onFeedMessage);
123
+ client.connect();
124
+
125
+ return () => {
126
+ client.off("ready", onReady);
127
+ client.off("disconnect", onDisconnect);
128
+ client.off("feed:message", onFeedMessage);
129
+ client.disconnect();
130
+ };
131
+ }, [client]);
132
+
133
+ return (
134
+ <section>
135
+ <p>Status: {status}</p>
136
+ <ul>
137
+ {messages.map((message) => (
138
+ <li key={message}>{message}</li>
139
+ ))}
140
+ </ul>
141
+ </section>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ---
147
+
31
148
  ## Chunked streaming (new in 0.9.0)
32
149
 
33
150
  `@aegis-fluxion/core@0.9.0` adds secure chunked stream transport for large payloads.
package/dist/index.cjs CHANGED
@@ -259,6 +259,9 @@ function parseEnvelopeFromText(decodedPayload) {
259
259
  function decodeCloseReason(reason) {
260
260
  return reason.toString("utf8");
261
261
  }
262
+ function escapePrometheusLabelValue(value) {
263
+ return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
264
+ }
262
265
  function isReservedEmitEvent(event) {
263
266
  return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === INTERNAL_STREAM_FRAME_EVENT || event === READY_EVENT;
264
267
  }
@@ -793,6 +796,7 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
793
796
  }
794
797
  var SecureServer = class {
795
798
  instanceId = crypto.randomUUID();
799
+ startedAtMs = Date.now();
796
800
  socketServer;
797
801
  adapter = null;
798
802
  heartbeatConfig;
@@ -822,6 +826,20 @@ var SecureServer = class {
822
826
  rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
823
827
  rateLimitBucketsByIp = /* @__PURE__ */ new Map();
824
828
  sessionTicketStore = /* @__PURE__ */ new Map();
829
+ telemetryCounters = {
830
+ totalConnections: 0,
831
+ handshakeSuccessTotal: 0,
832
+ handshakeFailureTotal: 0,
833
+ resumeHandshakeSuccessTotal: 0,
834
+ resumeHandshakeFailureTotal: 0,
835
+ encryptedMessagesSentTotal: 0,
836
+ encryptedMessagesReceivedTotal: 0,
837
+ encryptedBytesSentTotal: 0,
838
+ encryptedBytesReceivedTotal: 0,
839
+ ddosBlockedTotal: 0,
840
+ ddosThrottledTotal: 0,
841
+ ddosDisconnectedTotal: 0
842
+ };
825
843
  constructor(options) {
826
844
  const { heartbeat, rateLimit, sessionResumption, adapter, ...socketServerOptions } = options;
827
845
  this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
@@ -845,6 +863,79 @@ var SecureServer = class {
845
863
  get clients() {
846
864
  return this.clientsById;
847
865
  }
866
+ getMetrics() {
867
+ const now = Date.now();
868
+ const uptimeSeconds = Math.max(0, (now - this.startedAtMs) / 1e3);
869
+ return {
870
+ serverId: this.instanceId,
871
+ timestampMs: now,
872
+ uptimeSeconds,
873
+ activeConnections: this.clientCount,
874
+ totalConnections: this.telemetryCounters.totalConnections,
875
+ handshakeSuccessTotal: this.telemetryCounters.handshakeSuccessTotal,
876
+ handshakeFailureTotal: this.telemetryCounters.handshakeFailureTotal,
877
+ resumeHandshakeSuccessTotal: this.telemetryCounters.resumeHandshakeSuccessTotal,
878
+ resumeHandshakeFailureTotal: this.telemetryCounters.resumeHandshakeFailureTotal,
879
+ encryptedMessagesSentTotal: this.telemetryCounters.encryptedMessagesSentTotal,
880
+ encryptedMessagesReceivedTotal: this.telemetryCounters.encryptedMessagesReceivedTotal,
881
+ encryptedBytesSentTotal: this.telemetryCounters.encryptedBytesSentTotal,
882
+ encryptedBytesReceivedTotal: this.telemetryCounters.encryptedBytesReceivedTotal,
883
+ ddosBlockedTotal: this.telemetryCounters.ddosBlockedTotal,
884
+ ddosThrottledTotal: this.telemetryCounters.ddosThrottledTotal,
885
+ ddosDisconnectedTotal: this.telemetryCounters.ddosDisconnectedTotal
886
+ };
887
+ }
888
+ getMetricsPrometheus() {
889
+ const metrics = this.getMetrics();
890
+ const labelValue = escapePrometheusLabelValue(metrics.serverId);
891
+ const labels = `{server_id="${labelValue}"}`;
892
+ const lines = [
893
+ "# HELP aegis_fluxion_server_active_connections Number of currently active secure connections.",
894
+ "# TYPE aegis_fluxion_server_active_connections gauge",
895
+ `aegis_fluxion_server_active_connections${labels} ${metrics.activeConnections}`,
896
+ "# HELP aegis_fluxion_server_total_connections_total Total number of accepted secure connections since process start.",
897
+ "# TYPE aegis_fluxion_server_total_connections_total counter",
898
+ `aegis_fluxion_server_total_connections_total${labels} ${metrics.totalConnections}`,
899
+ "# HELP aegis_fluxion_server_uptime_seconds Process uptime in seconds.",
900
+ "# TYPE aegis_fluxion_server_uptime_seconds gauge",
901
+ `aegis_fluxion_server_uptime_seconds${labels} ${metrics.uptimeSeconds}`,
902
+ "# HELP aegis_fluxion_server_handshake_success_total Total successful secure handshakes.",
903
+ "# TYPE aegis_fluxion_server_handshake_success_total counter",
904
+ `aegis_fluxion_server_handshake_success_total${labels} ${metrics.handshakeSuccessTotal}`,
905
+ "# HELP aegis_fluxion_server_handshake_failure_total Total failed secure handshake attempts.",
906
+ "# TYPE aegis_fluxion_server_handshake_failure_total counter",
907
+ `aegis_fluxion_server_handshake_failure_total${labels} ${metrics.handshakeFailureTotal}`,
908
+ "# HELP aegis_fluxion_server_resume_handshake_success_total Total successful session-resume handshakes.",
909
+ "# TYPE aegis_fluxion_server_resume_handshake_success_total counter",
910
+ `aegis_fluxion_server_resume_handshake_success_total${labels} ${metrics.resumeHandshakeSuccessTotal}`,
911
+ "# HELP aegis_fluxion_server_resume_handshake_failure_total Total failed session-resume handshakes.",
912
+ "# TYPE aegis_fluxion_server_resume_handshake_failure_total counter",
913
+ `aegis_fluxion_server_resume_handshake_failure_total${labels} ${metrics.resumeHandshakeFailureTotal}`,
914
+ "# HELP aegis_fluxion_server_encrypted_messages_sent_total Total encrypted messages sent by the server.",
915
+ "# TYPE aegis_fluxion_server_encrypted_messages_sent_total counter",
916
+ `aegis_fluxion_server_encrypted_messages_sent_total${labels} ${metrics.encryptedMessagesSentTotal}`,
917
+ "# HELP aegis_fluxion_server_encrypted_messages_received_total Total encrypted messages received by the server.",
918
+ "# TYPE aegis_fluxion_server_encrypted_messages_received_total counter",
919
+ `aegis_fluxion_server_encrypted_messages_received_total${labels} ${metrics.encryptedMessagesReceivedTotal}`,
920
+ "# HELP aegis_fluxion_server_encrypted_bytes_sent_total Total encrypted bytes sent by the server.",
921
+ "# TYPE aegis_fluxion_server_encrypted_bytes_sent_total counter",
922
+ `aegis_fluxion_server_encrypted_bytes_sent_total${labels} ${metrics.encryptedBytesSentTotal}`,
923
+ "# HELP aegis_fluxion_server_encrypted_bytes_received_total Total encrypted bytes received by the server.",
924
+ "# TYPE aegis_fluxion_server_encrypted_bytes_received_total counter",
925
+ `aegis_fluxion_server_encrypted_bytes_received_total${labels} ${metrics.encryptedBytesReceivedTotal}`,
926
+ "# HELP aegis_fluxion_server_ddos_blocked_total Total DDoS/flood attempts blocked by rate limiting.",
927
+ "# TYPE aegis_fluxion_server_ddos_blocked_total counter",
928
+ `aegis_fluxion_server_ddos_blocked_total${labels} ${metrics.ddosBlockedTotal}`,
929
+ "# HELP aegis_fluxion_server_ddos_throttled_total Total requests slowed down by adaptive throttling.",
930
+ "# TYPE aegis_fluxion_server_ddos_throttled_total counter",
931
+ `aegis_fluxion_server_ddos_throttled_total${labels} ${metrics.ddosThrottledTotal}`,
932
+ "# HELP aegis_fluxion_server_ddos_disconnected_total Total sockets disconnected due to severe rate limit violations.",
933
+ "# TYPE aegis_fluxion_server_ddos_disconnected_total counter",
934
+ `aegis_fluxion_server_ddos_disconnected_total${labels} ${metrics.ddosDisconnectedTotal}`
935
+ ];
936
+ return `${lines.join("\n")}
937
+ `;
938
+ }
848
939
  async setAdapter(adapter) {
849
940
  const previousAdapter = this.adapter;
850
941
  if (previousAdapter === adapter) {
@@ -1153,6 +1244,35 @@ var SecureServer = class {
1153
1244
  this.notifyError(normalizeToError(error, "Failed to close server."));
1154
1245
  }
1155
1246
  }
1247
+ recordHandshakeSuccess(resumed) {
1248
+ this.telemetryCounters.handshakeSuccessTotal += 1;
1249
+ if (resumed) {
1250
+ this.telemetryCounters.resumeHandshakeSuccessTotal += 1;
1251
+ }
1252
+ }
1253
+ recordHandshakeFailure(resumed) {
1254
+ this.telemetryCounters.handshakeFailureTotal += 1;
1255
+ if (resumed) {
1256
+ this.telemetryCounters.resumeHandshakeFailureTotal += 1;
1257
+ }
1258
+ }
1259
+ recordEncryptedMessageSent(byteLength) {
1260
+ this.telemetryCounters.encryptedMessagesSentTotal += 1;
1261
+ this.telemetryCounters.encryptedBytesSentTotal += Math.max(0, byteLength);
1262
+ }
1263
+ recordEncryptedMessageReceived(byteLength) {
1264
+ this.telemetryCounters.encryptedMessagesReceivedTotal += 1;
1265
+ this.telemetryCounters.encryptedBytesReceivedTotal += Math.max(0, byteLength);
1266
+ }
1267
+ recordDdosBlocked(disconnected) {
1268
+ this.telemetryCounters.ddosBlockedTotal += 1;
1269
+ if (disconnected) {
1270
+ this.telemetryCounters.ddosDisconnectedTotal += 1;
1271
+ }
1272
+ }
1273
+ recordDdosThrottled() {
1274
+ this.telemetryCounters.ddosThrottledTotal += 1;
1275
+ }
1156
1276
  resolveHeartbeatConfig(heartbeatOptions) {
1157
1277
  const intervalMs = heartbeatOptions?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1158
1278
  const timeoutMs = heartbeatOptions?.timeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
@@ -1582,6 +1702,7 @@ var SecureServer = class {
1582
1702
  lastPingAt: 0
1583
1703
  });
1584
1704
  this.roomNamesByClientId.set(clientId, /* @__PURE__ */ new Set());
1705
+ this.telemetryCounters.totalConnections += 1;
1585
1706
  socket.on("message", (rawData) => {
1586
1707
  void this.handleIncomingMessage(client, rawData);
1587
1708
  });
@@ -1610,6 +1731,7 @@ var SecureServer = class {
1610
1731
  try {
1611
1732
  const rateLimitDecision = this.evaluateIncomingRateLimit(client);
1612
1733
  if (rateLimitDecision.shouldDisconnect) {
1734
+ this.recordDdosBlocked(true);
1613
1735
  this.notifyError(
1614
1736
  new Error(
1615
1737
  `Rate limit disconnect triggered for client ${client.id}.`
@@ -1624,9 +1746,11 @@ var SecureServer = class {
1624
1746
  return;
1625
1747
  }
1626
1748
  if (rateLimitDecision.shouldDrop) {
1749
+ this.recordDdosBlocked(false);
1627
1750
  return;
1628
1751
  }
1629
1752
  if (rateLimitDecision.throttleDelayMs > 0) {
1753
+ this.recordDdosThrottled();
1630
1754
  this.notifyError(
1631
1755
  new Error(
1632
1756
  `Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
@@ -1667,6 +1791,7 @@ var SecureServer = class {
1667
1791
  return;
1668
1792
  }
1669
1793
  let decryptedPayload;
1794
+ const encryptedPayloadByteLength = rawDataToBuffer(rawData).length;
1670
1795
  try {
1671
1796
  decryptedPayload = decryptSerializedEnvelope(rawData, encryptionKey);
1672
1797
  } catch {
@@ -1674,6 +1799,7 @@ var SecureServer = class {
1674
1799
  return;
1675
1800
  }
1676
1801
  const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
1802
+ this.recordEncryptedMessageReceived(encryptedPayloadByteLength);
1677
1803
  if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
1678
1804
  this.handleRpcResponse(client.socket, decryptedEnvelope.data);
1679
1805
  return;
@@ -2032,6 +2158,7 @@ var SecureServer = class {
2032
2158
  try {
2033
2159
  const serializedEnvelope = await serializeEnvelope(envelope.event, envelope.data);
2034
2160
  const encryptedPayload = encryptSerializedEnvelope(serializedEnvelope, encryptionKey);
2161
+ this.recordEncryptedMessageSent(encryptedPayload.length);
2035
2162
  socket.send(encryptedPayload);
2036
2163
  } catch (error) {
2037
2164
  const normalizedError = normalizeToError(error, "Failed to send encrypted server payload.");
@@ -2230,6 +2357,7 @@ var SecureServer = class {
2230
2357
  }
2231
2358
  handleResumeHandshake(client, payload) {
2232
2359
  if (!this.sessionResumptionConfig.enabled) {
2360
+ this.recordHandshakeFailure(true);
2233
2361
  this.sendResumeAck(client.socket, {
2234
2362
  ok: false,
2235
2363
  reason: "Session resumption is disabled."
@@ -2238,6 +2366,7 @@ var SecureServer = class {
2238
2366
  }
2239
2367
  const ticketRecord = this.getSessionTicket(payload.sessionId);
2240
2368
  if (!ticketRecord) {
2369
+ this.recordHandshakeFailure(true);
2241
2370
  this.sendResumeAck(client.socket, {
2242
2371
  ok: false,
2243
2372
  reason: "Session ticket is unknown or expired."
@@ -2264,6 +2393,7 @@ var SecureServer = class {
2264
2393
  clientNonce
2265
2394
  );
2266
2395
  if (!equalsConstantTime(receivedProof, expectedProof)) {
2396
+ this.recordHandshakeFailure(true);
2267
2397
  this.sendResumeAck(client.socket, {
2268
2398
  ok: false,
2269
2399
  reason: "Session resumption proof validation failed."
@@ -2284,6 +2414,7 @@ var SecureServer = class {
2284
2414
  this.sharedSecretBySocket.set(client.socket, resumedKey);
2285
2415
  this.encryptionKeyBySocket.set(client.socket, resumedKey);
2286
2416
  handshakeState.isReady = true;
2417
+ this.recordHandshakeSuccess(true);
2287
2418
  this.sendResumeAck(client.socket, {
2288
2419
  ok: true,
2289
2420
  sessionId: ticketRecord.sessionId,
@@ -2293,6 +2424,7 @@ var SecureServer = class {
2293
2424
  this.notifyReady(client);
2294
2425
  this.issueSessionTicket(client.socket, resumedKey);
2295
2426
  } catch (error) {
2427
+ this.recordHandshakeFailure(true);
2296
2428
  this.sendResumeAck(client.socket, {
2297
2429
  ok: false,
2298
2430
  reason: "Session resumption payload was invalid."
@@ -2323,10 +2455,12 @@ var SecureServer = class {
2323
2455
  this.sharedSecretBySocket.set(client.socket, sharedSecret);
2324
2456
  this.encryptionKeyBySocket.set(client.socket, encryptionKey);
2325
2457
  handshakeState.isReady = true;
2458
+ this.recordHandshakeSuccess(false);
2326
2459
  void this.flushQueuedPayloads(client.socket);
2327
2460
  this.notifyReady(client);
2328
2461
  this.issueSessionTicket(client.socket, encryptionKey);
2329
2462
  } catch (error) {
2463
+ this.recordHandshakeFailure(false);
2330
2464
  this.notifyError(normalizeToError(error, "Failed to complete server handshake."));
2331
2465
  }
2332
2466
  }