@easyoref/shared 1.21.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.
package/src/store.ts ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Session-based alert state store — Redis operations.
3
+ *
4
+ * A "session" spans the lifecycle of one attack event:
5
+ * early_warning → (optional red_alert) → resolved → +10 min tail
6
+ *
7
+ * Keys:
8
+ * session:active — ActiveSession JSON TTL 45min
9
+ * session:posts — LPUSH list of ChannelPost TTL 45min
10
+ * session:ext_cache — HASH {post_hash → extraction JSON} TTL 45min
11
+ * alert:{alertId}:meta — AlertMeta JSON TTL 20min
12
+ *
13
+ * Only the LATEST alert's Telegram message gets enrichment edits.
14
+ * Posts accumulate across the entire session (shared context).
15
+ */
16
+
17
+ import { getRedis } from "./redis.js";
18
+ import type {
19
+ ActiveSession,
20
+ AlertMeta,
21
+ AlertType,
22
+ ChannelPost,
23
+ EnrichmentData,
24
+ TelegramMessage,
25
+ } from "./schemas.js";
26
+ import { createEmptyEnrichmentData } from "./schemas.js";
27
+
28
+ // Re-export types for backward compatibility
29
+ export type { ActiveSession, AlertMeta, ChannelPost, TelegramMessage };
30
+
31
+ // Schema version for migration handling
32
+ export const SCHEMA_VERSION = "2.0.0";
33
+ const SCHEMA_VERSION_KEY = "schema:version";
34
+
35
+ let schemaVersionChecked = false;
36
+
37
+ export async function ensureSchemaVersion(): Promise<void> {
38
+ if (schemaVersionChecked) return;
39
+ schemaVersionChecked = true;
40
+
41
+ const redis = getRedis();
42
+ const stored = await redis.get(SCHEMA_VERSION_KEY);
43
+
44
+ if (stored !== SCHEMA_VERSION) {
45
+ await redis.flushall();
46
+ await redis.set(SCHEMA_VERSION_KEY, SCHEMA_VERSION);
47
+ }
48
+ }
49
+
50
+ const META_TTL_S = 20 * 60; // 20 minutes
51
+ const SESSION_TTL_S = 45 * 60; // 45 min worst case
52
+
53
+ // ── Session phase timeouts ─────────────────────────────
54
+
55
+ /** Max duration (ms) for each phase before auto-expire */
56
+ export const PHASE_TIMEOUT_MS: Record<AlertType, number> = {
57
+ early_warning: 30 * 60 * 1000, // 30 min
58
+ red_alert: 15 * 60 * 1000, // 15 min
59
+ resolved: 10 * 60 * 1000, // 10 min tail
60
+ };
61
+
62
+ /** Enrichment interval (ms) per phase */
63
+ export const PHASE_ENRICH_DELAY_MS: Record<AlertType, number> = {
64
+ early_warning: 60_000, // 60s — channels need time to post; saves tokens
65
+ red_alert: 45_000, // 45s
66
+ resolved: 150_000, // 150s (2.5 min) — per user requirement: 10 min window, update every 2.5 min
67
+ };
68
+
69
+ /** Initial enrichment delay — first job after alert (channels need time to post) */
70
+ export const PHASE_INITIAL_DELAY_MS: Record<AlertType, number> = {
71
+ early_warning: 120_000, // 2 min — wait for launch reports
72
+ red_alert: 15_000, // 15s
73
+ resolved: 90_000, // 90s — wait for first wave of post-incident reports
74
+ };
75
+
76
+ // ──Alert Meta (per-alert) ─────────────────────────────
77
+
78
+ export async function saveAlertMeta(meta: AlertMeta): Promise<void> {
79
+ const redis = getRedis();
80
+ await redis.setex(
81
+ `alert:${meta.alertId}:meta`,
82
+ META_TTL_S,
83
+ JSON.stringify(meta),
84
+ );
85
+ }
86
+
87
+ export async function getAlertMeta(
88
+ alertId: string,
89
+ ): Promise<AlertMeta | undefined> {
90
+ const redis = getRedis();
91
+ const raw = await redis.get(`alert:${alertId}:meta`);
92
+ return raw ? (JSON.parse(raw) as AlertMeta) : undefined;
93
+ }
94
+
95
+ // ── Session posts (shared across entire session) ───────
96
+
97
+ export async function pushSessionPost(post: ChannelPost): Promise<void> {
98
+ const redis = getRedis();
99
+ await redis.lpush("session:posts", JSON.stringify(post));
100
+ await redis.expire("session:posts", SESSION_TTL_S);
101
+ }
102
+
103
+ export async function getSessionPosts(): Promise<ChannelPost[]> {
104
+ const redis = getRedis();
105
+ const items = await redis.lrange("session:posts", 0, -1);
106
+ return items.map((i: string) => JSON.parse(i) as ChannelPost);
107
+ }
108
+
109
+ // ── Active session ─────────────────────────────────────
110
+
111
+ export async function setActiveSession(session: ActiveSession): Promise<void> {
112
+ const redis = getRedis();
113
+ await redis.setex("session:active", SESSION_TTL_S, JSON.stringify(session));
114
+ }
115
+
116
+ export async function getActiveSession(): Promise<ActiveSession | undefined> {
117
+ const redis = getRedis();
118
+ const raw = await redis.get("session:active");
119
+ return raw ? (JSON.parse(raw) as ActiveSession) : undefined;
120
+ }
121
+
122
+ export async function clearSession(): Promise<void> {
123
+ const redis = getRedis();
124
+ await redis.del(
125
+ "session:active",
126
+ "session:posts",
127
+ "session:enrichment",
128
+ EXT_CACHE_KEY,
129
+ LAST_UPDATE_KEY,
130
+ );
131
+ }
132
+
133
+ export function isPhaseExpired(session: ActiveSession): boolean {
134
+ const elapsed = Date.now() - session.phaseStartTs;
135
+ return elapsed >= PHASE_TIMEOUT_MS[session.phase];
136
+ }
137
+
138
+ // ── Compat shims (used by gramjs-monitor, graph) ───────
139
+
140
+ export async function getActiveAlert(): Promise<
141
+ | {
142
+ alertId: string;
143
+ alertTs: number;
144
+ alertType: AlertType;
145
+ }
146
+ | undefined
147
+ > {
148
+ const s = await getActiveSession();
149
+ if (!s) return undefined;
150
+ return {
151
+ alertId: s.latestAlertId,
152
+ alertTs: s.latestAlertTs,
153
+ alertType: s.phase,
154
+ };
155
+ }
156
+
157
+ export async function pushChannelPost(
158
+ _alertId: string,
159
+ post: ChannelPost,
160
+ ): Promise<void> {
161
+ await pushSessionPost(post);
162
+ }
163
+
164
+ export async function getChannelPosts(
165
+ _alertId: string,
166
+ ): Promise<ChannelPost[]> {
167
+ return getSessionPosts();
168
+ }
169
+
170
+ // ── Enrichment data (cross-phase persistence) ──────────
171
+
172
+ export async function saveEnrichmentData(data: EnrichmentData): Promise<void> {
173
+ const redis = getRedis();
174
+ await redis.setex("session:enrichment", SESSION_TTL_S, JSON.stringify(data));
175
+ }
176
+
177
+ export async function getEnrichmentData(): Promise<EnrichmentData> {
178
+ const redis = getRedis();
179
+ const raw = await redis.get("session:enrichment");
180
+ return raw
181
+ ? (JSON.parse(raw) as EnrichmentData)
182
+ : createEmptyEnrichmentData();
183
+ }
184
+
185
+ // ── Last update timestamp (tracks when last enrichment job ran) ──
186
+
187
+ const LAST_UPDATE_KEY = "session:last_update_ts";
188
+
189
+ export async function getLastUpdateTs(): Promise<number> {
190
+ const redis = getRedis();
191
+ const raw = await redis.get(LAST_UPDATE_KEY);
192
+ return raw ? Number(raw) : 0;
193
+ }
194
+
195
+ export async function setLastUpdateTs(ts: number): Promise<void> {
196
+ const redis = getRedis();
197
+ await redis.setex(LAST_UPDATE_KEY, SESSION_TTL_S, String(ts));
198
+ }
199
+
200
+ // ── Extraction cache (post-level dedup between jobs) ───
201
+
202
+ const EXT_CACHE_KEY = "session:ext_cache";
203
+
204
+ /**
205
+ * Get cached extraction results for a batch of post hashes.
206
+ * Returns a map: postHash → serialized ValidatedExtraction JSON.
207
+ */
208
+ export async function getCachedExtractions(
209
+ postHashes: string[],
210
+ ): Promise<Map<string, string>> {
211
+ if (postHashes.length === 0) return new Map();
212
+ const redis = getRedis();
213
+ const results = await redis.hmget(EXT_CACHE_KEY, ...postHashes);
214
+ const map = new Map<string, string>();
215
+ postHashes.forEach((hash, i) => {
216
+ if (results[i]) map.set(hash, results[i]!);
217
+ });
218
+ return map;
219
+ }
220
+
221
+ /**
222
+ * Save new extraction results to cache.
223
+ * @param entries - Record of postHash → serialized ValidatedExtraction JSON
224
+ */
225
+ export async function saveCachedExtractions(
226
+ entries: Record<string, string>,
227
+ ): Promise<void> {
228
+ if (Object.keys(entries).length === 0) return;
229
+ const redis = getRedis();
230
+ await redis.hset(EXT_CACHE_KEY, entries);
231
+ await redis.expire(EXT_CACHE_KEY, SESSION_TTL_S);
232
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["dist"]
9
+ }