instar 0.26.3 → 0.26.4
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/dashboard/index.html +55 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +45 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/slack-cli.d.ts.map +1 -1
- package/dist/commands/slack-cli.js +6 -0
- package/dist/commands/slack-cli.js.map +1 -1
- package/dist/core/CapabilityMapper.d.ts.map +1 -1
- package/dist/core/CapabilityMapper.js +2 -0
- package/dist/core/CapabilityMapper.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +17 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +64 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +37 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +9 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/messaging/imessage/IMessageAdapter.d.ts +112 -0
- package/dist/messaging/imessage/IMessageAdapter.d.ts.map +1 -0
- package/dist/messaging/imessage/IMessageAdapter.js +483 -0
- package/dist/messaging/imessage/IMessageAdapter.js.map +1 -0
- package/dist/messaging/imessage/NativeBackend.d.ts +82 -0
- package/dist/messaging/imessage/NativeBackend.d.ts.map +1 -0
- package/dist/messaging/imessage/NativeBackend.js +353 -0
- package/dist/messaging/imessage/NativeBackend.js.map +1 -0
- package/dist/messaging/imessage/OutboundAuditLog.d.ts +49 -0
- package/dist/messaging/imessage/OutboundAuditLog.d.ts.map +1 -0
- package/dist/messaging/imessage/OutboundAuditLog.js +58 -0
- package/dist/messaging/imessage/OutboundAuditLog.js.map +1 -0
- package/dist/messaging/imessage/OutboundRateLimiter.d.ts +54 -0
- package/dist/messaging/imessage/OutboundRateLimiter.d.ts.map +1 -0
- package/dist/messaging/imessage/OutboundRateLimiter.js +111 -0
- package/dist/messaging/imessage/OutboundRateLimiter.js.map +1 -0
- package/dist/messaging/imessage/index.d.ts +10 -0
- package/dist/messaging/imessage/index.d.ts.map +1 -0
- package/dist/messaging/imessage/index.js +13 -0
- package/dist/messaging/imessage/index.js.map +1 -0
- package/dist/messaging/imessage/normalize-phone.d.ts +26 -0
- package/dist/messaging/imessage/normalize-phone.d.ts.map +1 -0
- package/dist/messaging/imessage/normalize-phone.js +55 -0
- package/dist/messaging/imessage/normalize-phone.js.map +1 -0
- package/dist/messaging/imessage/types.d.ts +84 -0
- package/dist/messaging/imessage/types.d.ts.map +1 -0
- package/dist/messaging/imessage/types.js +5 -0
- package/dist/messaging/imessage/types.js.map +1 -0
- package/dist/messaging/shared/MessagingEventBus.d.ts +5 -0
- package/dist/messaging/shared/MessagingEventBus.d.ts.map +1 -1
- package/dist/messaging/shared/MessagingEventBus.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +31 -1
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/SocketModeClient.d.ts.map +1 -1
- package/dist/messaging/slack/SocketModeClient.js +6 -0
- package/dist/messaging/slack/SocketModeClient.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts +6 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +46 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/QuotaExhaustionDetector.d.ts +15 -0
- package/dist/monitoring/QuotaExhaustionDetector.d.ts.map +1 -1
- package/dist/monitoring/QuotaExhaustionDetector.js +26 -3
- package/dist/monitoring/QuotaExhaustionDetector.js.map +1 -1
- package/dist/monitoring/SessionMonitor.d.ts.map +1 -1
- package/dist/monitoring/SessionMonitor.js +5 -2
- package/dist/monitoring/SessionMonitor.js.map +1 -1
- package/dist/monitoring/SessionRecovery.d.ts +16 -1
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -1
- package/dist/monitoring/SessionRecovery.js +71 -8
- package/dist/monitoring/SessionRecovery.js.map +1 -1
- package/dist/scheduler/JobScheduler.js +1 -1
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +14 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/templates/hooks/settings-template.json +11 -0
- package/upgrades/0.26.4.md +17 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeBackend — Read-only macOS Messages database integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - SQLite reads from ~/Library/Messages/chat.db (via better-sqlite3)
|
|
6
|
+
* - Polling for new messages (watches max ROWID)
|
|
7
|
+
* - Conversation context formatting for session bootstrap
|
|
8
|
+
*
|
|
9
|
+
* Does NOT send messages. Sending happens from Claude Code sessions
|
|
10
|
+
* via imessage-reply.sh → imsg send CLI, because AppleScript Automation
|
|
11
|
+
* permission doesn't propagate through LaunchAgent process trees.
|
|
12
|
+
*
|
|
13
|
+
* Requires:
|
|
14
|
+
* - Full Disk Access for the process reading chat.db
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
// Apple Cocoa epoch: 2001-01-01T00:00:00Z in Unix epoch seconds
|
|
21
|
+
const APPLE_EPOCH_OFFSET = 978307200;
|
|
22
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
23
|
+
const DEFAULT_DB_PATH = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
24
|
+
export class NativeBackend extends EventEmitter {
|
|
25
|
+
db = null;
|
|
26
|
+
pollTimer = null;
|
|
27
|
+
lastRowId = 0;
|
|
28
|
+
_state = 'disconnected';
|
|
29
|
+
dbPath;
|
|
30
|
+
pollIntervalMs;
|
|
31
|
+
includeAttachments;
|
|
32
|
+
offsetPath;
|
|
33
|
+
authorizedContacts;
|
|
34
|
+
// Prepared statements (cached for performance)
|
|
35
|
+
stmtNewMessages = null;
|
|
36
|
+
stmtChats = null;
|
|
37
|
+
stmtHistory = null;
|
|
38
|
+
stmtMaxRowId = null;
|
|
39
|
+
stmtContextHistory = null;
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
super();
|
|
42
|
+
this.dbPath = options.dbPath || DEFAULT_DB_PATH;
|
|
43
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
44
|
+
this.includeAttachments = options.includeAttachments ?? true;
|
|
45
|
+
this.offsetPath = options.offsetPath ?? null;
|
|
46
|
+
this.authorizedContacts = options.authorizedContacts ?? [];
|
|
47
|
+
}
|
|
48
|
+
get state() {
|
|
49
|
+
return this._state;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Open the Messages database and start polling for new messages.
|
|
53
|
+
*/
|
|
54
|
+
async connect() {
|
|
55
|
+
if (this._state === 'connected')
|
|
56
|
+
return;
|
|
57
|
+
this._setState('connecting');
|
|
58
|
+
try {
|
|
59
|
+
const Database = (await import('better-sqlite3')).default;
|
|
60
|
+
// Open without readonly flag — readonly mode cannot read the WAL (write-ahead log).
|
|
61
|
+
// Messages.app writes to WAL continuously; new messages only appear in WAL until
|
|
62
|
+
// a checkpoint flushes them to the main db file. query_only pragma prevents writes
|
|
63
|
+
// while still allowing WAL reads.
|
|
64
|
+
this.db = new Database(this.dbPath, { fileMustExist: true });
|
|
65
|
+
this.db.pragma('query_only = ON');
|
|
66
|
+
// Scope poll query to authorized contacts for defense-in-depth.
|
|
67
|
+
// Even if the adapter's auth check is bypassed, the SQL itself won't return
|
|
68
|
+
// unauthorized messages. Falls back to unscoped if no contacts configured.
|
|
69
|
+
if (this.authorizedContacts.length > 0) {
|
|
70
|
+
const placeholders = this.authorizedContacts.map(() => '?').join(', ');
|
|
71
|
+
this.stmtNewMessages = this.db.prepare(`
|
|
72
|
+
SELECT m.ROWID, m.guid, m.text, m.date, m.is_from_me, m.service,
|
|
73
|
+
m.associated_message_type,
|
|
74
|
+
h.id AS sender,
|
|
75
|
+
c.chat_identifier AS chat_id, c.display_name AS chat_name
|
|
76
|
+
FROM message m
|
|
77
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
78
|
+
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
79
|
+
LEFT JOIN chat c ON cmj.chat_id = c.ROWID
|
|
80
|
+
WHERE m.ROWID > ?
|
|
81
|
+
AND (m.is_from_me = 1 OR h.id IN (${placeholders}))
|
|
82
|
+
ORDER BY m.ROWID ASC
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.stmtNewMessages = this.db.prepare(`
|
|
87
|
+
SELECT m.ROWID, m.guid, m.text, m.date, m.is_from_me, m.service,
|
|
88
|
+
m.associated_message_type,
|
|
89
|
+
h.id AS sender,
|
|
90
|
+
c.chat_identifier AS chat_id, c.display_name AS chat_name
|
|
91
|
+
FROM message m
|
|
92
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
93
|
+
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
94
|
+
LEFT JOIN chat c ON cmj.chat_id = c.ROWID
|
|
95
|
+
WHERE m.ROWID > ?
|
|
96
|
+
ORDER BY m.ROWID ASC
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
// Scope chat listing to authorized contacts
|
|
100
|
+
if (this.authorizedContacts.length > 0) {
|
|
101
|
+
const placeholders = this.authorizedContacts.map(() => '?').join(', ');
|
|
102
|
+
this.stmtChats = this.db.prepare(`
|
|
103
|
+
SELECT c.ROWID AS id, c.chat_identifier, c.display_name, c.service_name,
|
|
104
|
+
c.guid, c.is_archived,
|
|
105
|
+
(SELECT MAX(m.date) FROM message m
|
|
106
|
+
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
107
|
+
WHERE cmj.chat_id = c.ROWID) AS last_message_date
|
|
108
|
+
FROM chat c
|
|
109
|
+
WHERE c.chat_identifier IN (${placeholders})
|
|
110
|
+
ORDER BY last_message_date DESC
|
|
111
|
+
LIMIT ?
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.stmtChats = this.db.prepare(`
|
|
116
|
+
SELECT c.ROWID AS id, c.chat_identifier, c.display_name, c.service_name,
|
|
117
|
+
c.guid, c.is_archived,
|
|
118
|
+
(SELECT MAX(m.date) FROM message m
|
|
119
|
+
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
120
|
+
WHERE cmj.chat_id = c.ROWID) AS last_message_date
|
|
121
|
+
FROM chat c
|
|
122
|
+
ORDER BY last_message_date DESC
|
|
123
|
+
LIMIT ?
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
this.stmtHistory = this.db.prepare(`
|
|
127
|
+
SELECT m.ROWID, m.guid, m.text, m.date, m.is_from_me, m.service,
|
|
128
|
+
h.id AS sender
|
|
129
|
+
FROM message m
|
|
130
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
131
|
+
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
132
|
+
LEFT JOIN chat c ON cmj.chat_id = c.ROWID
|
|
133
|
+
WHERE c.chat_identifier = ?
|
|
134
|
+
ORDER BY m.date DESC
|
|
135
|
+
LIMIT ?
|
|
136
|
+
`);
|
|
137
|
+
// Context history query — filters by sender handle (phone/email)
|
|
138
|
+
this.stmtContextHistory = this.db.prepare(`
|
|
139
|
+
SELECT m.ROWID, m.text, m.date, m.is_from_me, h.id AS sender
|
|
140
|
+
FROM message m
|
|
141
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
142
|
+
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
143
|
+
LEFT JOIN chat c ON cmj.chat_id = c.ROWID
|
|
144
|
+
WHERE (h.id = ? OR c.chat_identifier = ?)
|
|
145
|
+
AND m.text IS NOT NULL
|
|
146
|
+
AND m.associated_message_type = 0
|
|
147
|
+
ORDER BY m.date DESC
|
|
148
|
+
LIMIT ?
|
|
149
|
+
`);
|
|
150
|
+
this.stmtMaxRowId = this.db.prepare('SELECT MAX(ROWID) AS max_id FROM message');
|
|
151
|
+
// Restore persisted poll offset if available; otherwise use a 50-message
|
|
152
|
+
// lookback so messages received while the server was down are processed.
|
|
153
|
+
const persistedOffset = this._loadOffset();
|
|
154
|
+
if (persistedOffset !== null) {
|
|
155
|
+
this.lastRowId = persistedOffset;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const maxRow = this.stmtMaxRowId.get();
|
|
159
|
+
const maxId = maxRow?.max_id ?? 0;
|
|
160
|
+
this.lastRowId = Math.max(0, maxId - 50);
|
|
161
|
+
}
|
|
162
|
+
this._setState('connected');
|
|
163
|
+
this._startPolling();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
this._setState('error');
|
|
167
|
+
throw new Error(`Failed to open Messages database: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Stop polling and close the database.
|
|
172
|
+
*/
|
|
173
|
+
async disconnect() {
|
|
174
|
+
this._stopPolling();
|
|
175
|
+
if (this.db) {
|
|
176
|
+
try {
|
|
177
|
+
this.db.close();
|
|
178
|
+
}
|
|
179
|
+
catch { /* already closed */ }
|
|
180
|
+
this.db = null;
|
|
181
|
+
}
|
|
182
|
+
this.stmtNewMessages = null;
|
|
183
|
+
this.stmtChats = null;
|
|
184
|
+
this.stmtHistory = null;
|
|
185
|
+
this.stmtContextHistory = null;
|
|
186
|
+
this.stmtMaxRowId = null;
|
|
187
|
+
this._setState('disconnected');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* List recent chats.
|
|
191
|
+
*/
|
|
192
|
+
listChats(limit = 20) {
|
|
193
|
+
if (!this.db || !this.stmtChats) {
|
|
194
|
+
throw new Error('Database not connected');
|
|
195
|
+
}
|
|
196
|
+
const chatBindParams = this.authorizedContacts.length > 0
|
|
197
|
+
? [...this.authorizedContacts, limit]
|
|
198
|
+
: [limit];
|
|
199
|
+
const rows = this.stmtChats.all(...chatBindParams);
|
|
200
|
+
return rows.map((row) => ({
|
|
201
|
+
chatId: row.chat_identifier,
|
|
202
|
+
displayName: row.display_name || undefined,
|
|
203
|
+
participants: [row.chat_identifier],
|
|
204
|
+
lastMessageDate: row.last_message_date
|
|
205
|
+
? this._cocoaToIso(row.last_message_date)
|
|
206
|
+
: undefined,
|
|
207
|
+
service: row.service_name,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get message history for a chat.
|
|
212
|
+
*/
|
|
213
|
+
getChatHistory(chatId, limit = 50) {
|
|
214
|
+
if (!this.db || !this.stmtHistory) {
|
|
215
|
+
throw new Error('Database not connected');
|
|
216
|
+
}
|
|
217
|
+
// Defense-in-depth: only return history for authorized contacts
|
|
218
|
+
if (this.authorizedContacts.length > 0 && !this.authorizedContacts.includes(chatId)) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const rows = this.stmtHistory.all(chatId, limit);
|
|
222
|
+
return rows.map((row) => ({
|
|
223
|
+
chatId,
|
|
224
|
+
messageId: row.guid,
|
|
225
|
+
sender: row.sender || chatId,
|
|
226
|
+
text: row.text || '',
|
|
227
|
+
timestamp: this._cocoaToUnix(row.date),
|
|
228
|
+
isFromMe: row.is_from_me === 1,
|
|
229
|
+
service: row.service,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Format conversation context for session bootstrap.
|
|
234
|
+
* Returns a formatted string of recent messages suitable for injection
|
|
235
|
+
* into a Claude Code session as conversation history.
|
|
236
|
+
*/
|
|
237
|
+
getConversationContext(sender, limit = 20) {
|
|
238
|
+
if (!this.db || !this.stmtContextHistory) {
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const rows = this.stmtContextHistory.all(sender, sender, limit);
|
|
243
|
+
if (rows.length === 0)
|
|
244
|
+
return '';
|
|
245
|
+
// Reverse to chronological order (query returns newest first)
|
|
246
|
+
rows.reverse();
|
|
247
|
+
const lines = rows.map((row) => {
|
|
248
|
+
const time = new Date(this._cocoaToUnix(row.date) * 1000);
|
|
249
|
+
const hh = time.getHours().toString().padStart(2, '0');
|
|
250
|
+
const mm = time.getMinutes().toString().padStart(2, '0');
|
|
251
|
+
const who = row.is_from_me ? 'Agent' : (row.sender || sender);
|
|
252
|
+
const text = row.text || '(attachment)';
|
|
253
|
+
return `[${hh}:${mm}] ${who}: ${text}`;
|
|
254
|
+
});
|
|
255
|
+
return `--- Conversation History (last ${rows.length} messages) ---\n${lines.join('\n')}\n--- End History ---`;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Internal ──
|
|
262
|
+
_startPolling() {
|
|
263
|
+
if (this.pollTimer)
|
|
264
|
+
return;
|
|
265
|
+
this.pollTimer = setInterval(() => this._poll(), this.pollIntervalMs);
|
|
266
|
+
}
|
|
267
|
+
_stopPolling() {
|
|
268
|
+
if (this.pollTimer) {
|
|
269
|
+
clearInterval(this.pollTimer);
|
|
270
|
+
this.pollTimer = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
_poll() {
|
|
274
|
+
if (!this.db || !this.stmtNewMessages)
|
|
275
|
+
return;
|
|
276
|
+
try {
|
|
277
|
+
// Pass authorized contacts as bind params when SQL is scoped
|
|
278
|
+
const bindParams = this.authorizedContacts.length > 0
|
|
279
|
+
? [this.lastRowId, ...this.authorizedContacts]
|
|
280
|
+
: [this.lastRowId];
|
|
281
|
+
const rows = this.stmtNewMessages.all(...bindParams);
|
|
282
|
+
const startRowId = this.lastRowId;
|
|
283
|
+
for (const row of rows) {
|
|
284
|
+
this.lastRowId = row.ROWID;
|
|
285
|
+
// Skip non-text messages (reactions, edits, etc.)
|
|
286
|
+
if (row.associated_message_type !== 0)
|
|
287
|
+
continue;
|
|
288
|
+
// Skip empty messages (typing indicators, read receipts)
|
|
289
|
+
if (!row.text && !this.includeAttachments)
|
|
290
|
+
continue;
|
|
291
|
+
const msg = {
|
|
292
|
+
chatId: row.chat_id || row.sender || 'unknown',
|
|
293
|
+
messageId: row.guid,
|
|
294
|
+
sender: row.sender || 'unknown',
|
|
295
|
+
senderName: row.chat_name || undefined,
|
|
296
|
+
text: row.text || '',
|
|
297
|
+
timestamp: this._cocoaToUnix(row.date),
|
|
298
|
+
isFromMe: row.is_from_me === 1,
|
|
299
|
+
service: row.service,
|
|
300
|
+
};
|
|
301
|
+
this.emit('message', msg);
|
|
302
|
+
}
|
|
303
|
+
// Persist offset if it advanced
|
|
304
|
+
if (this.lastRowId > startRowId) {
|
|
305
|
+
this._saveOffset(this.lastRowId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
const msg = err.message;
|
|
310
|
+
if (!msg.includes('SQLITE_BUSY') && !msg.includes('database is locked')) {
|
|
311
|
+
console.error(`[imessage-native] Poll error: ${msg}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
_setState(state) {
|
|
316
|
+
const prev = this._state;
|
|
317
|
+
this._state = state;
|
|
318
|
+
if (prev !== state) {
|
|
319
|
+
this.emit('stateChange', state, prev);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Convert Apple Cocoa nanosecond timestamp to Unix epoch seconds. */
|
|
323
|
+
_cocoaToUnix(cocoaNanos) {
|
|
324
|
+
return Math.floor(cocoaNanos / 1e9) + APPLE_EPOCH_OFFSET;
|
|
325
|
+
}
|
|
326
|
+
/** Load persisted poll offset from disk. */
|
|
327
|
+
_loadOffset() {
|
|
328
|
+
if (!this.offsetPath)
|
|
329
|
+
return null;
|
|
330
|
+
try {
|
|
331
|
+
const data = JSON.parse(fs.readFileSync(this.offsetPath, 'utf-8'));
|
|
332
|
+
if (typeof data.lastRowId === 'number' && data.lastRowId > 0) {
|
|
333
|
+
return data.lastRowId;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch { /* first run or corrupted — use lookback */ }
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
/** Save poll offset to disk. */
|
|
340
|
+
_saveOffset(rowId) {
|
|
341
|
+
if (!this.offsetPath)
|
|
342
|
+
return;
|
|
343
|
+
try {
|
|
344
|
+
fs.writeFileSync(this.offsetPath, JSON.stringify({ lastRowId: rowId, savedAt: new Date().toISOString() }) + '\n');
|
|
345
|
+
}
|
|
346
|
+
catch { /* non-critical */ }
|
|
347
|
+
}
|
|
348
|
+
/** Convert Apple Cocoa nanosecond timestamp to ISO string. */
|
|
349
|
+
_cocoaToIso(cocoaNanos) {
|
|
350
|
+
return new Date(this._cocoaToUnix(cocoaNanos) * 1000).toISOString();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=NativeBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeBackend.js","sourceRoot":"","sources":["../../../src/messaging/imessage/NativeBackend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutboundAuditLog — Append-only JSONL logger for all outbound message attempts.
|
|
3
|
+
*
|
|
4
|
+
* Logs every outbound attempt (allowed or blocked) with:
|
|
5
|
+
* - Masked recipient (PII protection)
|
|
6
|
+
* - SHA-256 hashes for correlation (recipient + content)
|
|
7
|
+
* - Which enforcement layer blocked (if blocked)
|
|
8
|
+
* - Rate limit status at time of attempt
|
|
9
|
+
* - Send mode (reactive vs proactive)
|
|
10
|
+
*
|
|
11
|
+
* No plaintext message content or phone numbers are stored.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuditEntry {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
recipient: string;
|
|
16
|
+
recipientHash: string;
|
|
17
|
+
textLength: number;
|
|
18
|
+
textHash: string;
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
blockedBy: string | null;
|
|
21
|
+
sendMode: 'reactive' | 'proactive';
|
|
22
|
+
sessionName: string | null;
|
|
23
|
+
sendToken: string | null;
|
|
24
|
+
rateStatus: {
|
|
25
|
+
contactHour: number;
|
|
26
|
+
globalDay: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare class OutboundAuditLog {
|
|
30
|
+
private readonly logPath;
|
|
31
|
+
constructor(logPath: string);
|
|
32
|
+
/**
|
|
33
|
+
* Record an outbound attempt.
|
|
34
|
+
*/
|
|
35
|
+
record(entry: {
|
|
36
|
+
recipient: string;
|
|
37
|
+
text: string;
|
|
38
|
+
allowed: boolean;
|
|
39
|
+
blockedBy?: string;
|
|
40
|
+
sendMode?: 'reactive' | 'proactive';
|
|
41
|
+
sessionName?: string;
|
|
42
|
+
sendToken?: string;
|
|
43
|
+
rateStatus?: {
|
|
44
|
+
contactHour: number;
|
|
45
|
+
globalDay: number;
|
|
46
|
+
};
|
|
47
|
+
}): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=OutboundAuditLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutboundAuditLog.d.ts","sourceRoot":"","sources":["../../../src/messaging/imessage/OutboundAuditLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,UAAU,GAAG,WAAW,CAAC;IACnC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,MAAM;IAI3B;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;QACpC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;KACzD,GAAG,IAAI;CAqBT"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutboundAuditLog — Append-only JSONL logger for all outbound message attempts.
|
|
3
|
+
*
|
|
4
|
+
* Logs every outbound attempt (allowed or blocked) with:
|
|
5
|
+
* - Masked recipient (PII protection)
|
|
6
|
+
* - SHA-256 hashes for correlation (recipient + content)
|
|
7
|
+
* - Which enforcement layer blocked (if blocked)
|
|
8
|
+
* - Rate limit status at time of attempt
|
|
9
|
+
* - Send mode (reactive vs proactive)
|
|
10
|
+
*
|
|
11
|
+
* No plaintext message content or phone numbers are stored.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
export class OutboundAuditLog {
|
|
16
|
+
logPath;
|
|
17
|
+
constructor(logPath) {
|
|
18
|
+
this.logPath = logPath;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Record an outbound attempt.
|
|
22
|
+
*/
|
|
23
|
+
record(entry) {
|
|
24
|
+
const auditEntry = {
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
recipient: maskIdentifier(entry.recipient),
|
|
27
|
+
recipientHash: sha256Prefix(entry.recipient),
|
|
28
|
+
textLength: entry.text.length,
|
|
29
|
+
textHash: sha256Prefix(entry.text),
|
|
30
|
+
allowed: entry.allowed,
|
|
31
|
+
blockedBy: entry.blockedBy || null,
|
|
32
|
+
sendMode: entry.sendMode || 'reactive',
|
|
33
|
+
sessionName: entry.sessionName || null,
|
|
34
|
+
sendToken: entry.sendToken ? entry.sendToken.slice(0, 8) : null,
|
|
35
|
+
rateStatus: entry.rateStatus || { contactHour: 0, globalDay: 0 },
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
fs.appendFileSync(this.logPath, JSON.stringify(auditEntry) + '\n');
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`[imessage-audit] Failed to write audit log: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function sha256Prefix(input) {
|
|
46
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
47
|
+
}
|
|
48
|
+
function maskIdentifier(id) {
|
|
49
|
+
if (id.startsWith('+') && id.length > 6) {
|
|
50
|
+
return id.slice(0, 4) + '***' + id.slice(-4);
|
|
51
|
+
}
|
|
52
|
+
if (id.includes('@')) {
|
|
53
|
+
const [local, domain] = id.split('@');
|
|
54
|
+
return local.slice(0, 2) + '***@' + domain;
|
|
55
|
+
}
|
|
56
|
+
return '***';
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=OutboundAuditLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutboundAuditLog.js","sourceRoot":"","sources":["../../../src/messaging/imessage/OutboundAuditLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,MAAM,MAAM,aAAa,CAAC;AAgBjC,MAAM,OAAO,gBAAgB;IACV,OAAO,CAAS;IAEjC,YAAY,OAAe;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KASN;QACC,MAAM,UAAU,GAAe;YAC7B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC;YAC1C,aAAa,EAAE,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC;YAC5C,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;YAC7B,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC;YAClC,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,UAAU;YACtC,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI;YACtC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;YAC/D,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;SACjE,CAAC;QAEF,IAAI,CAAC;YACH,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;QACrE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,+CAAgD,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;CACF;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,cAAc,CAAC,EAAU;IAChC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACtC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutboundRateLimiter — In-memory sliding-window rate limiter for outbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Two independent limits:
|
|
5
|
+
* 1. Per-contact: max messages per hour (sliding window)
|
|
6
|
+
* 2. Global: max messages per day (rolling 24h window)
|
|
7
|
+
*
|
|
8
|
+
* Resets on server restart (in-memory only). This is acceptable because
|
|
9
|
+
* limits are a safety net, not a billing mechanism.
|
|
10
|
+
*/
|
|
11
|
+
export interface RateLimiterConfig {
|
|
12
|
+
/** Max outbound messages per contact per hour (default: 20) */
|
|
13
|
+
maxPerHour: number;
|
|
14
|
+
/** Max outbound messages globally per day (default: 100) */
|
|
15
|
+
maxPerDay: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RateLimitStatus {
|
|
18
|
+
/** Per-contact counts for the current hour window */
|
|
19
|
+
perContact: Map<string, number>;
|
|
20
|
+
/** Global count for the current day window */
|
|
21
|
+
globalToday: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class OutboundRateLimiter {
|
|
24
|
+
private readonly maxPerHour;
|
|
25
|
+
private readonly maxPerDay;
|
|
26
|
+
private contactTimestamps;
|
|
27
|
+
private globalTimestamps;
|
|
28
|
+
constructor(config?: Partial<RateLimiterConfig>);
|
|
29
|
+
/**
|
|
30
|
+
* Check if a send to this recipient is allowed under rate limits.
|
|
31
|
+
* Does NOT record the send — call record() after successful delivery.
|
|
32
|
+
*/
|
|
33
|
+
check(recipient: string): {
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Record a sent message for rate tracking.
|
|
39
|
+
*/
|
|
40
|
+
record(recipient: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get current rate limit status.
|
|
43
|
+
*/
|
|
44
|
+
status(): RateLimitStatus;
|
|
45
|
+
/**
|
|
46
|
+
* Get counts for a specific contact (for audit log inclusion).
|
|
47
|
+
*/
|
|
48
|
+
countsFor(recipient: string): {
|
|
49
|
+
contactHour: number;
|
|
50
|
+
globalDay: number;
|
|
51
|
+
};
|
|
52
|
+
private _cleanup;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=OutboundRateLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutboundRateLimiter.d.ts","sourceRoot":"","sources":["../../../src/messaging/imessage/OutboundRateLimiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAGnC,OAAO,CAAC,iBAAiB,CAA+B;IAExD,OAAO,CAAC,gBAAgB,CAAgB;gBAE5B,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAKnD;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IA2B/D;;OAEG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAgB/B;;OAEG;IACH,MAAM,IAAI,eAAe;IAiBzB;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAYxE,OAAO,CAAC,QAAQ;CAgBjB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutboundRateLimiter — In-memory sliding-window rate limiter for outbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Two independent limits:
|
|
5
|
+
* 1. Per-contact: max messages per hour (sliding window)
|
|
6
|
+
* 2. Global: max messages per day (rolling 24h window)
|
|
7
|
+
*
|
|
8
|
+
* Resets on server restart (in-memory only). This is acceptable because
|
|
9
|
+
* limits are a safety net, not a billing mechanism.
|
|
10
|
+
*/
|
|
11
|
+
export class OutboundRateLimiter {
|
|
12
|
+
maxPerHour;
|
|
13
|
+
maxPerDay;
|
|
14
|
+
// Timestamps of sent messages per contact (for sliding window)
|
|
15
|
+
contactTimestamps = new Map();
|
|
16
|
+
// Global timestamps
|
|
17
|
+
globalTimestamps = [];
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
this.maxPerHour = config.maxPerHour ?? 20;
|
|
20
|
+
this.maxPerDay = config.maxPerDay ?? 100;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a send to this recipient is allowed under rate limits.
|
|
24
|
+
* Does NOT record the send — call record() after successful delivery.
|
|
25
|
+
*/
|
|
26
|
+
check(recipient) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const hourAgo = now - 3_600_000;
|
|
29
|
+
const dayAgo = now - 86_400_000;
|
|
30
|
+
// Per-contact hourly check
|
|
31
|
+
const contactTs = this.contactTimestamps.get(recipient) || [];
|
|
32
|
+
const recentContact = contactTs.filter(t => t > hourAgo);
|
|
33
|
+
if (recentContact.length >= this.maxPerHour) {
|
|
34
|
+
return {
|
|
35
|
+
allowed: false,
|
|
36
|
+
reason: `per-contact hourly limit reached (${this.maxPerHour}/hr)`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Global daily check
|
|
40
|
+
const recentGlobal = this.globalTimestamps.filter(t => t > dayAgo);
|
|
41
|
+
if (recentGlobal.length >= this.maxPerDay) {
|
|
42
|
+
return {
|
|
43
|
+
allowed: false,
|
|
44
|
+
reason: `global daily limit reached (${this.maxPerDay}/day)`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { allowed: true };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record a sent message for rate tracking.
|
|
51
|
+
*/
|
|
52
|
+
record(recipient) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
// Contact timestamps
|
|
55
|
+
if (!this.contactTimestamps.has(recipient)) {
|
|
56
|
+
this.contactTimestamps.set(recipient, []);
|
|
57
|
+
}
|
|
58
|
+
this.contactTimestamps.get(recipient).push(now);
|
|
59
|
+
// Global timestamps
|
|
60
|
+
this.globalTimestamps.push(now);
|
|
61
|
+
// Periodic cleanup — evict old entries to prevent memory growth
|
|
62
|
+
this._cleanup();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get current rate limit status.
|
|
66
|
+
*/
|
|
67
|
+
status() {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const hourAgo = now - 3_600_000;
|
|
70
|
+
const dayAgo = now - 86_400_000;
|
|
71
|
+
const perContact = new Map();
|
|
72
|
+
for (const [contact, timestamps] of this.contactTimestamps) {
|
|
73
|
+
const count = timestamps.filter(t => t > hourAgo).length;
|
|
74
|
+
if (count > 0)
|
|
75
|
+
perContact.set(contact, count);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
perContact,
|
|
79
|
+
globalToday: this.globalTimestamps.filter(t => t > dayAgo).length,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get counts for a specific contact (for audit log inclusion).
|
|
84
|
+
*/
|
|
85
|
+
countsFor(recipient) {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const hourAgo = now - 3_600_000;
|
|
88
|
+
const dayAgo = now - 86_400_000;
|
|
89
|
+
const contactTs = this.contactTimestamps.get(recipient) || [];
|
|
90
|
+
return {
|
|
91
|
+
contactHour: contactTs.filter(t => t > hourAgo).length,
|
|
92
|
+
globalDay: this.globalTimestamps.filter(t => t > dayAgo).length,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
_cleanup() {
|
|
96
|
+
const dayAgo = Date.now() - 86_400_000;
|
|
97
|
+
// Clean global
|
|
98
|
+
this.globalTimestamps = this.globalTimestamps.filter(t => t > dayAgo);
|
|
99
|
+
// Clean per-contact
|
|
100
|
+
for (const [contact, timestamps] of this.contactTimestamps) {
|
|
101
|
+
const filtered = timestamps.filter(t => t > dayAgo);
|
|
102
|
+
if (filtered.length === 0) {
|
|
103
|
+
this.contactTimestamps.delete(contact);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.contactTimestamps.set(contact, filtered);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=OutboundRateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutboundRateLimiter.js","sourceRoot":"","sources":["../../../src/messaging/imessage/OutboundRateLimiter.ts"],"names":[],"mappings":"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"}
|