@immahq/aegis 0.0.1

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.
@@ -0,0 +1,112 @@
1
+ import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
2
+ import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
3
+ import { blake3 } from "@noble/hashes/blake3.js";
4
+ import { bytesToHex, concatBytes } from "@noble/hashes/utils.js";
5
+ import { Logger } from "./logger.js";
6
+ import { PreKeyManager } from "./prekey-manager.js";
7
+ import { ERRORS } from "./constants.js";
8
+ export class IdentityManager {
9
+ constructor(storage) {
10
+ Object.defineProperty(this, "storage", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "preKeyManager", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ this.storage = storage;
23
+ this.preKeyManager = new PreKeyManager();
24
+ }
25
+ async createIdentity() {
26
+ try {
27
+ Logger.log("Identity", "Creating new identity");
28
+ const kemKeyPair = ml_kem768.keygen();
29
+ const dsaKeyPair = ml_dsa65.keygen();
30
+ const userId = bytesToHex(blake3(concatBytes(kemKeyPair.publicKey, dsaKeyPair.publicKey), {
31
+ dkLen: 32,
32
+ }));
33
+ const preKeyPair = ml_kem768.keygen();
34
+ const preKeySignature = ml_dsa65.sign(preKeyPair.publicKey, dsaKeyPair.secretKey);
35
+ const preKey = {
36
+ id: 1,
37
+ keyPair: preKeyPair,
38
+ signature: preKeySignature,
39
+ used: false,
40
+ createdAt: Date.now(),
41
+ };
42
+ await this.preKeyManager.savePreKey(preKey);
43
+ const identity = {
44
+ kemKeyPair,
45
+ dsaKeyPair,
46
+ userId,
47
+ createdAt: Date.now(),
48
+ preKeySecret: preKeyPair.secretKey,
49
+ };
50
+ const publicBundle = {
51
+ userId,
52
+ kemPublicKey: kemKeyPair.publicKey,
53
+ dsaPublicKey: dsaKeyPair.publicKey,
54
+ preKey: {
55
+ id: preKey.id,
56
+ key: preKeyPair.publicKey,
57
+ signature: preKeySignature,
58
+ },
59
+ createdAt: Date.now(),
60
+ };
61
+ await this.storage.saveIdentity(identity);
62
+ Logger.log("Identity", "Identity created successfully", {
63
+ userId: userId.substring(0, 16) + "...",
64
+ });
65
+ return { identity, publicBundle };
66
+ }
67
+ catch (error) {
68
+ Logger.error("Identity", "Failed to create identity", error);
69
+ throw error;
70
+ }
71
+ }
72
+ async getIdentity() {
73
+ const identity = await this.storage.getIdentity();
74
+ if (!identity)
75
+ throw new Error(ERRORS.IDENTITY_NOT_FOUND);
76
+ return identity;
77
+ }
78
+ async getPublicBundle() {
79
+ const identity = await this.getIdentity();
80
+ const preKey = await this.preKeyManager.getUnusedPreKey();
81
+ if (!preKey) {
82
+ const newPreKey = await this.preKeyManager.generatePreKey(identity);
83
+ return {
84
+ userId: identity.userId,
85
+ kemPublicKey: identity.kemKeyPair.publicKey,
86
+ dsaPublicKey: identity.dsaKeyPair.publicKey,
87
+ preKey: {
88
+ id: newPreKey.id,
89
+ key: newPreKey.keyPair.publicKey,
90
+ signature: newPreKey.signature,
91
+ },
92
+ createdAt: identity.createdAt,
93
+ };
94
+ }
95
+ return {
96
+ userId: identity.userId,
97
+ kemPublicKey: identity.kemKeyPair.publicKey,
98
+ dsaPublicKey: identity.dsaKeyPair.publicKey,
99
+ preKey: {
100
+ id: preKey.id,
101
+ key: preKey.keyPair.publicKey,
102
+ signature: preKey.signature,
103
+ },
104
+ createdAt: identity.createdAt,
105
+ };
106
+ }
107
+ async rotateIdentity() {
108
+ const result = await this.createIdentity();
109
+ await this.preKeyManager.clear();
110
+ return result;
111
+ }
112
+ }
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { E2EE as Aegis } from "./e2ee.js";
2
+ export { MemoryStorage } from "./storage.js";
3
+ export { Logger } from "./logger.js";
4
+ export { KemRatchet } from "./ratchet.js";
5
+ export { SessionKeyExchange } from "./session.js";
6
+ export { IdentityManager } from "./identity-manager.js";
7
+ export { SessionManager } from "./session-manager.js";
8
+ export { CryptoManager } from "./crypto-manager.js";
9
+ export { RatchetManager } from "./ratchet-manager.js";
10
+ export { ReplayProtection } from "./replay-protection.js";
11
+ export { GroupManager } from "./group-manager.js";
package/dist/logger.js ADDED
@@ -0,0 +1,23 @@
1
+ export class Logger {
2
+ static log(component, message, data) {
3
+ const safeData = data ? { ...data } : undefined;
4
+ // Remove any sensitive data from logs
5
+ if (safeData) {
6
+ const sensitiveKeys = [
7
+ "secretKey",
8
+ "privateKey",
9
+ "rootKey",
10
+ "chainKey",
11
+ "ciphertext",
12
+ ];
13
+ sensitiveKeys.forEach((key) => delete safeData[key]);
14
+ }
15
+ console.log(`[Aegis:${component}] ${message}`, safeData || "");
16
+ }
17
+ static error(component, message, error) {
18
+ console.error(`[Aegis:${component}] ERROR: ${message}`, error || "");
19
+ }
20
+ static warn(component, message, data) {
21
+ console.warn(`[Aegis:${component}] WARN: ${message}`, data || "");
22
+ }
23
+ }
@@ -0,0 +1,63 @@
1
+ import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
2
+ import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
3
+ export class PreKeyManager {
4
+ constructor() {
5
+ Object.defineProperty(this, "preKeys", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: new Map()
10
+ });
11
+ Object.defineProperty(this, "nextPreKeyId", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: 1
16
+ });
17
+ }
18
+ async generatePreKey(identity) {
19
+ const preKeyPair = ml_kem768.keygen();
20
+ const preKeySignature = ml_dsa65.sign(preKeyPair.publicKey, identity.dsaKeyPair.secretKey);
21
+ const preKey = {
22
+ id: this.nextPreKeyId++,
23
+ keyPair: preKeyPair,
24
+ signature: preKeySignature,
25
+ used: false,
26
+ createdAt: Date.now(),
27
+ };
28
+ this.preKeys.set(preKey.id, preKey);
29
+ return preKey;
30
+ }
31
+ async savePreKey(preKey) {
32
+ this.preKeys.set(preKey.id, preKey);
33
+ }
34
+ getPreKey(id) {
35
+ return this.preKeys.get(id) || null;
36
+ }
37
+ getUnusedPreKey() {
38
+ for (const preKey of this.preKeys.values()) {
39
+ if (!preKey.used) {
40
+ return preKey;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ markPreKeyAsUsed(id) {
46
+ const preKey = this.preKeys.get(id);
47
+ if (preKey) {
48
+ preKey.used = true;
49
+ }
50
+ }
51
+ removeOldPreKeys(maxAge = 7 * 24 * 60 * 60 * 1000) {
52
+ const now = Date.now();
53
+ for (const [id, preKey] of this.preKeys.entries()) {
54
+ if (now - preKey.createdAt > maxAge) {
55
+ this.preKeys.delete(id);
56
+ }
57
+ }
58
+ }
59
+ clear() {
60
+ this.preKeys.clear();
61
+ this.nextPreKeyId = 1;
62
+ }
63
+ }
@@ -0,0 +1,130 @@
1
+ import { bytesToHex } from "@noble/hashes/utils.js";
2
+ import { Logger } from "./logger.js";
3
+ import { RATCHET_AFTER_MESSAGES } from "./constants.js";
4
+ import { KemRatchet } from "./ratchet.js";
5
+ export class RatchetManager {
6
+ shouldPerformSendingRatchet(session) {
7
+ const messageCount = session.sendingChain?.messageNumber || 0;
8
+ const isFirstMessageAsInitiator = session.isInitiator && messageCount === 0;
9
+ return (session.peerRatchetPublicKey !== null &&
10
+ !isFirstMessageAsInitiator &&
11
+ messageCount >= RATCHET_AFTER_MESSAGES);
12
+ }
13
+ needsReceivingRatchet(session, header) {
14
+ return (header.isRatchetMessage === true ||
15
+ (session.peerRatchetPublicKey !== null &&
16
+ bytesToHex(header.ratchetPublicKey) !==
17
+ bytesToHex(session.peerRatchetPublicKey)));
18
+ }
19
+ performSendingRatchet(session) {
20
+ if (!session.peerRatchetPublicKey) {
21
+ throw new Error("No peer ratchet public key available for ratchet");
22
+ }
23
+ const result = KemRatchet.performKemRatchetEncapsulate(session.rootKey, session.peerRatchetPublicKey);
24
+ const newSession = { ...session };
25
+ const pendingRatchetState = {
26
+ newRootKey: result.newRootKey,
27
+ newRatchetKeyPair: result.newRatchetKeyPair,
28
+ sendingChain: result.sendingChain,
29
+ receivingChain: result.receivingChain,
30
+ kemCiphertext: result.kemCiphertext,
31
+ previousReceivingChain: session.receivingChain,
32
+ previousSendingChain: session.sendingChain,
33
+ };
34
+ newSession.pendingRatchetState = pendingRatchetState;
35
+ newSession.previousSendingChainLength =
36
+ session.sendingChain?.messageNumber ?? 0;
37
+ newSession.ratchetCount++;
38
+ newSession.state = "RATCHET_PENDING";
39
+ Logger.log("Ratchet", "Prepared sending KEM ratchet", {
40
+ ratchetCount: newSession.ratchetCount,
41
+ messageNumber: session.sendingChain?.messageNumber || 0,
42
+ });
43
+ return {
44
+ session: newSession,
45
+ kemCiphertext: result.kemCiphertext,
46
+ };
47
+ }
48
+ performReceivingRatchet(session, kemCiphertext) {
49
+ if (!session.currentRatchetKeyPair?.secretKey) {
50
+ throw new Error("No current ratchet secret key available");
51
+ }
52
+ const result = KemRatchet.performKemRatchetDecapsulate(session.rootKey, kemCiphertext, session.currentRatchetKeyPair.secretKey);
53
+ const previousReceivingChain = session.receivingChain;
54
+ const updatedSession = { ...session };
55
+ updatedSession.rootKey = result.newRootKey;
56
+ updatedSession.currentRatchetKeyPair = result.newRatchetKeyPair;
57
+ updatedSession.sendingChain = result.sendingChain;
58
+ updatedSession.receivingChain = result.receivingChain;
59
+ updatedSession.ratchetCount++;
60
+ updatedSession.state = "ACTIVE";
61
+ updatedSession.pendingRatchetState = {
62
+ newRootKey: result.newRootKey,
63
+ newRatchetKeyPair: result.newRatchetKeyPair,
64
+ sendingChain: result.sendingChain,
65
+ receivingChain: result.receivingChain,
66
+ kemCiphertext,
67
+ previousReceivingChain,
68
+ previousSendingChain: session.sendingChain,
69
+ };
70
+ Logger.log("Ratchet", "Performed receiving KEM ratchet", {
71
+ ratchetCount: updatedSession.ratchetCount,
72
+ hasOldChain: previousReceivingChain !== null,
73
+ });
74
+ return updatedSession;
75
+ }
76
+ applyPendingRatchet(session) {
77
+ if (!session.pendingRatchetState) {
78
+ return session;
79
+ }
80
+ const { pendingRatchetState } = session;
81
+ const updatedSession = { ...session };
82
+ updatedSession.rootKey = pendingRatchetState.newRootKey;
83
+ updatedSession.currentRatchetKeyPair =
84
+ pendingRatchetState.newRatchetKeyPair;
85
+ updatedSession.sendingChain = pendingRatchetState.sendingChain;
86
+ updatedSession.receivingChain = pendingRatchetState.receivingChain;
87
+ updatedSession.pendingRatchetState = undefined;
88
+ updatedSession.state = "ACTIVE";
89
+ Logger.log("Ratchet", "Applied pending ratchet state", {
90
+ ratchetCount: updatedSession.ratchetCount,
91
+ });
92
+ return updatedSession;
93
+ }
94
+ async triggerRatchet(sessionId, session) {
95
+ if (!session.peerRatchetPublicKey) {
96
+ throw new Error("No peer ratchet public key available");
97
+ }
98
+ const result = KemRatchet.performKemRatchetEncapsulate(session.rootKey, session.peerRatchetPublicKey);
99
+ const newSession = {
100
+ ...session,
101
+ rootKey: result.newRootKey,
102
+ currentRatchetKeyPair: result.newRatchetKeyPair,
103
+ sendingChain: result.sendingChain,
104
+ receivingChain: result.receivingChain,
105
+ previousSendingChainLength: session.sendingChain?.messageNumber ?? 0,
106
+ ratchetCount: session.ratchetCount + 1,
107
+ state: "ACTIVE",
108
+ pendingRatchetState: {
109
+ newRootKey: result.newRootKey,
110
+ newRatchetKeyPair: result.newRatchetKeyPair,
111
+ sendingChain: result.sendingChain,
112
+ receivingChain: result.receivingChain,
113
+ kemCiphertext: result.kemCiphertext,
114
+ previousReceivingChain: session.receivingChain,
115
+ previousSendingChain: session.sendingChain,
116
+ },
117
+ };
118
+ Logger.log("Ratchet", "Manually triggered ratchet", {
119
+ sessionId: sessionId.substring(0, 16) + "...",
120
+ newRatchetCount: newSession.ratchetCount,
121
+ });
122
+ return newSession;
123
+ }
124
+ getDecryptionChainForRatchetMessage(session) {
125
+ if (!session.pendingRatchetState?.previousReceivingChain) {
126
+ return session.receivingChain;
127
+ }
128
+ return session.pendingRatchetState.previousReceivingChain;
129
+ }
130
+ }
@@ -0,0 +1,168 @@
1
+ import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
2
+ import { blake3 } from "@noble/hashes/blake3.js";
3
+ import { concatBytes } from "@noble/hashes/utils.js";
4
+ import { ML_KEM768_PUBLIC_KEY_LENGTH } from "./constants";
5
+ export class KemRatchet {
6
+ // Perform KEM ratchet as encapsulator (when we send a new ratchet key)
7
+ static performKemRatchetEncapsulate(rootKey, peerRatchetPublicKey) {
8
+ if (!peerRatchetPublicKey) {
9
+ throw new Error("peerRatchetPublicKey is required for KEM ratchet encapsulation");
10
+ }
11
+ // Validate peer's ratchet public key
12
+ if (!this.validateRatchetPublicKey(peerRatchetPublicKey)) {
13
+ throw new Error("Invalid peer ratchet public key");
14
+ }
15
+ // Generate new ratchet keypair for NEXT round
16
+ const newRatchetKeyPair = ml_kem768.keygen();
17
+ // Perform KEM encapsulation to peer's CURRENT ratchet public key
18
+ const { sharedSecret, cipherText } = ml_kem768.encapsulate(peerRatchetPublicKey);
19
+ // Derive new root key
20
+ const newRootKey = this.deriveKey(rootKey, sharedSecret, this.CONTEXT_ROOT_KEY);
21
+ // Derive sending and receiving chain keys
22
+ const sendingChainKey = this.deriveKey(newRootKey, cipherText, this.CONTEXT_CHAIN_KEY);
23
+ const receivingChainKey = this.deriveKey(newRootKey, sharedSecret, this.CONTEXT_CHAIN_KEY);
24
+ return {
25
+ newRootKey,
26
+ newRatchetKeyPair,
27
+ sendingChain: {
28
+ chainKey: sendingChainKey,
29
+ messageNumber: 0,
30
+ },
31
+ receivingChain: {
32
+ chainKey: receivingChainKey,
33
+ messageNumber: 0,
34
+ },
35
+ kemCiphertext: cipherText,
36
+ };
37
+ }
38
+ // Perform KEM ratchet as decapsulator (when we receive a new ratchet key)
39
+ static performKemRatchetDecapsulate(rootKey, kemCiphertext, currentRatchetSecretKey) {
40
+ if (!kemCiphertext || kemCiphertext.length === 0) {
41
+ throw new Error("KEM ciphertext is required for decapsulation");
42
+ }
43
+ // Decapsulate the KEM cipherText using our current secret key
44
+ const sharedSecret = ml_kem768.decapsulate(kemCiphertext, currentRatchetSecretKey);
45
+ // Generate new ratchet keypair for next round
46
+ const newRatchetKeyPair = ml_kem768.keygen();
47
+ // Derive new root key (must match encapsulator's derivation)
48
+ const newRootKey = this.deriveKey(rootKey, sharedSecret, this.CONTEXT_ROOT_KEY);
49
+ // Derive chain keys (roles are swapped compared to encapsulator)
50
+ const sendingChainKey = this.deriveKey(newRootKey, sharedSecret, this.CONTEXT_CHAIN_KEY);
51
+ const receivingChainKey = this.deriveKey(newRootKey, kemCiphertext, this.CONTEXT_CHAIN_KEY);
52
+ return {
53
+ newRootKey,
54
+ newRatchetKeyPair,
55
+ sendingChain: {
56
+ chainKey: sendingChainKey,
57
+ messageNumber: 0,
58
+ },
59
+ receivingChain: {
60
+ chainKey: receivingChainKey,
61
+ messageNumber: 0,
62
+ },
63
+ kemCiphertext: new Uint8Array(0),
64
+ };
65
+ }
66
+ // Generate confirmation MAC for session keys
67
+ static generateConfirmationMac(sessionId, rootKey, chainKey, isResponse = false) {
68
+ const context = isResponse
69
+ ? new Uint8Array([0x06]) // Different context for response
70
+ : this.CONTEXT_CONFIRMATION;
71
+ const data = concatBytes(new TextEncoder().encode(sessionId), rootKey, chainKey, new Uint8Array([isResponse ? 1 : 0]));
72
+ return this.deriveKey(rootKey, data, context);
73
+ }
74
+ // Verify confirmation MAC
75
+ static verifyConfirmationMac(sessionId, rootKey, chainKey, receivedMac, isResponse = false) {
76
+ const expectedMac = this.generateConfirmationMac(sessionId, rootKey, chainKey, isResponse);
77
+ // Constant-time comparison
78
+ if (expectedMac.length !== receivedMac.length)
79
+ return false;
80
+ let result = 0;
81
+ for (let i = 0; i < expectedMac.length; i++) {
82
+ result |= expectedMac[i] ^ receivedMac[i];
83
+ }
84
+ return result === 0;
85
+ }
86
+ // Symmetric ratchet for deriving message keys within a chain
87
+ static symmetricRatchet(chain) {
88
+ // Derive message key from current chain key
89
+ const messageKey = this.deriveKey(chain.chainKey, new Uint8Array([0x00]), this.CONTEXT_MESSAGE_KEY);
90
+ // Derive new chain key
91
+ const newChainKey = this.deriveKey(chain.chainKey, new Uint8Array([0x01]), this.CONTEXT_CHAIN_KEY);
92
+ const newChain = {
93
+ chainKey: newChainKey,
94
+ messageNumber: chain.messageNumber + 1,
95
+ };
96
+ return { messageKey, newChain };
97
+ }
98
+ // Skip message keys for out-of-order messages
99
+ static skipMessageKeys(chain, targetMessageNumber, maxSkip) {
100
+ const skippedKeys = new Map();
101
+ if (targetMessageNumber <= chain.messageNumber) {
102
+ throw new Error("Cannot skip to earlier message number");
103
+ }
104
+ const skipCount = targetMessageNumber - chain.messageNumber;
105
+ if (skipCount > maxSkip) {
106
+ throw new Error(`Skip count ${skipCount} exceeds maximum ${maxSkip}`);
107
+ }
108
+ let currentChain = { ...chain };
109
+ // Generate keys for skipped messages
110
+ for (let i = 0; i < skipCount; i++) {
111
+ const { messageKey, newChain } = this.symmetricRatchet(currentChain);
112
+ skippedKeys.set(currentChain.messageNumber, messageKey);
113
+ currentChain = newChain;
114
+ }
115
+ return { skippedKeys, newChain: currentChain };
116
+ }
117
+ // Helper method for key derivation
118
+ static deriveKey(key, data, context) {
119
+ return blake3(concatBytes(key, data, context), { dkLen: 32 });
120
+ }
121
+ // Generate a fresh ratchet keypair
122
+ static generateRatchetKeyPair() {
123
+ return ml_kem768.keygen();
124
+ }
125
+ // Check if we should perform a KEM ratchet
126
+ static shouldPerformRatchet(messageCount, lastRatchetTime, maxMessages = 100, maxTime = 7 * 24 * 60 * 60 * 1000 // 7 days
127
+ ) {
128
+ const now = Date.now();
129
+ return messageCount >= maxMessages || now - lastRatchetTime >= maxTime;
130
+ }
131
+ // Validate a ratchet public key
132
+ static validateRatchetPublicKey(publicKey) {
133
+ return (publicKey instanceof Uint8Array &&
134
+ publicKey.length === ML_KEM768_PUBLIC_KEY_LENGTH);
135
+ }
136
+ // Derive a unique ratchet identifier
137
+ static deriveRatchetId(publicKey, chainKey) {
138
+ const hash = blake3(concatBytes(publicKey, chainKey), { dkLen: 16 });
139
+ return Array.from(hash)
140
+ .map((b) => b.toString(16).padStart(2, "0"))
141
+ .join("");
142
+ }
143
+ }
144
+ // Constants for key derivation contexts
145
+ Object.defineProperty(KemRatchet, "CONTEXT_ROOT_KEY", {
146
+ enumerable: true,
147
+ configurable: true,
148
+ writable: true,
149
+ value: new Uint8Array([0x01])
150
+ });
151
+ Object.defineProperty(KemRatchet, "CONTEXT_CHAIN_KEY", {
152
+ enumerable: true,
153
+ configurable: true,
154
+ writable: true,
155
+ value: new Uint8Array([0x02])
156
+ });
157
+ Object.defineProperty(KemRatchet, "CONTEXT_MESSAGE_KEY", {
158
+ enumerable: true,
159
+ configurable: true,
160
+ writable: true,
161
+ value: new Uint8Array([0x03])
162
+ });
163
+ Object.defineProperty(KemRatchet, "CONTEXT_CONFIRMATION", {
164
+ enumerable: true,
165
+ configurable: true,
166
+ writable: true,
167
+ value: new Uint8Array([0x05])
168
+ });
@@ -0,0 +1,37 @@
1
+ import { MAX_STORED_MESSAGE_IDS } from "./constants.js";
2
+ export class ReplayProtection {
3
+ getSkippedKeyId(ratchetPublicKey, messageNumber) {
4
+ return `${this.bytesToHex(ratchetPublicKey)}:${messageNumber}`;
5
+ }
6
+ bytesToHex(bytes) {
7
+ return Array.from(bytes)
8
+ .map((b) => b.toString(16).padStart(2, "0"))
9
+ .join("");
10
+ }
11
+ cleanupSkippedKeys(session) {
12
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
13
+ const now = Date.now();
14
+ for (const [keyId, key] of session.skippedMessageKeys.entries()) {
15
+ if (now - key.timestamp > maxAge) {
16
+ session.skippedMessageKeys.delete(keyId);
17
+ }
18
+ }
19
+ }
20
+ // Simple replay protection: Store received message IDs
21
+ storeReceivedMessageId(session, messageId) {
22
+ session.receivedMessageIds.add(messageId);
23
+ // Keep the set size manageable
24
+ if (session.receivedMessageIds.size > MAX_STORED_MESSAGE_IDS) {
25
+ // Remove oldest entries (Set doesn't have order, so we recreate)
26
+ const ids = Array.from(session.receivedMessageIds);
27
+ session.receivedMessageIds = new Set(ids.slice(-MAX_STORED_MESSAGE_IDS));
28
+ }
29
+ }
30
+ async getReplayProtectionStatus(_sessionId, session) {
31
+ return {
32
+ storedMessageIds: session.receivedMessageIds.size,
33
+ lastProcessedTimestamp: session.lastProcessedTimestamp,
34
+ replayWindowSize: session.replayWindowSize,
35
+ };
36
+ }
37
+ }