@troykelly/openclaw-projects 0.0.11 → 0.0.12
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/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +17 -3
- package/dist/hooks.js.map +1 -1
- package/dist/register-openclaw.d.ts +3 -1
- package/dist/register-openclaw.d.ts.map +1 -1
- package/dist/register-openclaw.js +611 -55
- package/dist/register-openclaw.js.map +1 -1
- package/dist/tools/context-search.d.ts +79 -0
- package/dist/tools/context-search.d.ts.map +1 -0
- package/dist/tools/context-search.js +265 -0
- package/dist/tools/context-search.js.map +1 -0
- package/dist/tools/email-send.d.ts.map +1 -1
- package/dist/tools/email-send.js +1 -14
- package/dist/tools/email-send.js.map +1 -1
- package/dist/tools/entity-links.d.ts +117 -0
- package/dist/tools/entity-links.d.ts.map +1 -0
- package/dist/tools/entity-links.js +446 -0
- package/dist/tools/entity-links.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-recall.d.ts +28 -0
- package/dist/tools/memory-recall.d.ts.map +1 -1
- package/dist/tools/memory-recall.js +42 -3
- package/dist/tools/memory-recall.js.map +1 -1
- package/dist/tools/memory-store.d.ts +57 -0
- package/dist/tools/memory-store.d.ts.map +1 -1
- package/dist/tools/memory-store.js +29 -2
- package/dist/tools/memory-store.js.map +1 -1
- package/dist/tools/message-search.d.ts +1 -1
- package/dist/tools/message-search.d.ts.map +1 -1
- package/dist/tools/message-search.js +20 -2
- package/dist/tools/message-search.js.map +1 -1
- package/dist/tools/notes.d.ts +2 -2
- package/dist/tools/project-search.d.ts +92 -0
- package/dist/tools/project-search.d.ts.map +1 -0
- package/dist/tools/project-search.js +160 -0
- package/dist/tools/project-search.js.map +1 -0
- package/dist/tools/skill-store.d.ts +12 -12
- package/dist/tools/threads.d.ts +2 -2
- package/dist/tools/threads.d.ts.map +1 -1
- package/dist/tools/threads.js +30 -6
- package/dist/tools/threads.js.map +1 -1
- package/dist/tools/todo-search.d.ts +95 -0
- package/dist/tools/todo-search.d.ts.map +1 -0
- package/dist/tools/todo-search.js +164 -0
- package/dist/tools/todo-search.js.map +1 -0
- package/dist/types/openclaw-api.d.ts +15 -0
- package/dist/types/openclaw-api.d.ts.map +1 -1
- package/dist/utils/auto-linker.d.ts +66 -0
- package/dist/utils/auto-linker.d.ts.map +1 -0
- package/dist/utils/auto-linker.js +354 -0
- package/dist/utils/auto-linker.js.map +1 -0
- package/dist/utils/geo.d.ts +24 -0
- package/dist/utils/geo.d.ts.map +1 -0
- package/dist/utils/geo.js +38 -0
- package/dist/utils/geo.js.map +1 -0
- package/dist/utils/inbound-gate.d.ts +85 -0
- package/dist/utils/inbound-gate.d.ts.map +1 -0
- package/dist/utils/inbound-gate.js +133 -0
- package/dist/utils/inbound-gate.js.map +1 -0
- package/dist/utils/injection-protection.d.ts +81 -0
- package/dist/utils/injection-protection.d.ts.map +1 -0
- package/dist/utils/injection-protection.js +179 -0
- package/dist/utils/injection-protection.js.map +1 -0
- package/dist/utils/nominatim.d.ts +18 -0
- package/dist/utils/nominatim.d.ts.map +1 -0
- package/dist/utils/nominatim.js +56 -0
- package/dist/utils/nominatim.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +81 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +188 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/spam-filter.d.ts +79 -0
- package/dist/utils/spam-filter.d.ts.map +1 -0
- package/dist/utils/spam-filter.js +237 -0
- package/dist/utils/spam-filter.js.map +1 -0
- package/dist/utils/token-budget.d.ts +68 -0
- package/dist/utils/token-budget.d.ts.map +1 -0
- package/dist/utils/token-budget.js +142 -0
- package/dist/utils/token-budget.js.map +1 -0
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtE,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,4BAA4B;AAC5B,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,eAAe,CAAC;IACxG,0CAA0C;IAC1C,QAAQ,IAAI,gBAAgB,CAAC;CAC9B;AAED,yCAAyC;AACzC,eAAO,MAAM,2BAA2B,EAAE,iBAOzC,CAAC;AAKF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,iBAA+C,GAAG,WAAW,CA0KtG"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window rate limiter for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides per-sender and per-recipient rate limiting with
|
|
5
|
+
* contact-trust-level awareness. Uses in-memory sliding window
|
|
6
|
+
* with automatic cleanup of expired entries.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: State is in-memory and does not persist across process restarts
|
|
9
|
+
* or span multiple instances. For multi-instance deployments, a
|
|
10
|
+
* skill_store-backed implementation would be needed to share rate
|
|
11
|
+
* limit state across processes.
|
|
12
|
+
*
|
|
13
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
14
|
+
*/
|
|
15
|
+
import { normalizeSender } from './spam-filter.js';
|
|
16
|
+
/** Default rate limiter configuration */
|
|
17
|
+
export const DEFAULT_RATE_LIMITER_CONFIG = {
|
|
18
|
+
trustedSenderLimit: 100,
|
|
19
|
+
knownSenderLimit: 50,
|
|
20
|
+
unknownSenderLimit: 5,
|
|
21
|
+
recipientGlobalLimit: 200,
|
|
22
|
+
windowMs: 3_600_000, // 1 hour
|
|
23
|
+
maxSenderEntries: 10_000,
|
|
24
|
+
};
|
|
25
|
+
/** Interval for running cleanup of expired entries */
|
|
26
|
+
const CLEANUP_INTERVAL_CHECKS = 100;
|
|
27
|
+
/**
|
|
28
|
+
* Create a rate limiter instance.
|
|
29
|
+
*
|
|
30
|
+
* Uses a sliding window approach: each message records a timestamp,
|
|
31
|
+
* and only timestamps within the current window are counted.
|
|
32
|
+
*
|
|
33
|
+
* @param config - Rate limiter configuration (uses defaults if omitted)
|
|
34
|
+
* @returns RateLimiter instance
|
|
35
|
+
*/
|
|
36
|
+
export function createRateLimiter(config = DEFAULT_RATE_LIMITER_CONFIG) {
|
|
37
|
+
/** Map of sender -> array of message timestamps */
|
|
38
|
+
const senderWindows = new Map();
|
|
39
|
+
/** Map of recipient -> array of message timestamps */
|
|
40
|
+
const recipientWindows = new Map();
|
|
41
|
+
/** Counter for triggering periodic cleanup */
|
|
42
|
+
let checkCount = 0;
|
|
43
|
+
/**
|
|
44
|
+
* Get the rate limit for a given trust level.
|
|
45
|
+
*/
|
|
46
|
+
function getLimitForTrust(trust) {
|
|
47
|
+
switch (trust) {
|
|
48
|
+
case 'trusted':
|
|
49
|
+
return config.trustedSenderLimit;
|
|
50
|
+
case 'known':
|
|
51
|
+
return config.knownSenderLimit;
|
|
52
|
+
case 'unknown':
|
|
53
|
+
return config.unknownSenderLimit;
|
|
54
|
+
case 'blocked':
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Prune timestamps older than the window from an array.
|
|
60
|
+
* Returns only timestamps within the current window.
|
|
61
|
+
*/
|
|
62
|
+
function pruneWindow(timestamps, now) {
|
|
63
|
+
const cutoff = now - config.windowMs;
|
|
64
|
+
return timestamps.filter((ts) => ts > cutoff);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clean up expired entries from all windows.
|
|
68
|
+
*/
|
|
69
|
+
function cleanup(now) {
|
|
70
|
+
for (const [key, timestamps] of senderWindows) {
|
|
71
|
+
const pruned = pruneWindow(timestamps, now);
|
|
72
|
+
if (pruned.length === 0) {
|
|
73
|
+
senderWindows.delete(key);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
senderWindows.set(key, pruned);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [key, timestamps] of recipientWindows) {
|
|
80
|
+
const pruned = pruneWindow(timestamps, now);
|
|
81
|
+
if (pruned.length === 0) {
|
|
82
|
+
recipientWindows.delete(key);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
recipientWindows.set(key, pruned);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Evict oldest sender entries when max capacity is exceeded.
|
|
91
|
+
* Prevents unbounded memory growth from traffic bursts that create
|
|
92
|
+
* many keys and then stop before the next cleanup cycle runs.
|
|
93
|
+
*
|
|
94
|
+
* TODO: This only evicts senderWindows, not recipientWindows. In practice
|
|
95
|
+
* recipientWindows is bounded by the number of distinct recipients (typically
|
|
96
|
+
* small), but a similar cap should be added if this assumption changes.
|
|
97
|
+
*/
|
|
98
|
+
function evictIfOverCapacity() {
|
|
99
|
+
const maxEntries = config.maxSenderEntries ?? 10_000;
|
|
100
|
+
if (senderWindows.size <= maxEntries) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Find entries with the oldest most-recent timestamp and evict them
|
|
104
|
+
const entries = Array.from(senderWindows.entries()).map(([key, timestamps]) => ({
|
|
105
|
+
key,
|
|
106
|
+
newestTs: timestamps.length > 0 ? timestamps[timestamps.length - 1] : 0,
|
|
107
|
+
}));
|
|
108
|
+
entries.sort((a, b) => a.newestTs - b.newestTs);
|
|
109
|
+
const toEvict = senderWindows.size - maxEntries;
|
|
110
|
+
for (let i = 0; i < toEvict; i++) {
|
|
111
|
+
senderWindows.delete(entries[i].key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
check(sender, recipient, trust, channel) {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
checkCount++;
|
|
118
|
+
// Periodic cleanup and eviction to prevent memory leaks
|
|
119
|
+
if (checkCount % CLEANUP_INTERVAL_CHECKS === 0) {
|
|
120
|
+
cleanup(now);
|
|
121
|
+
evictIfOverCapacity();
|
|
122
|
+
}
|
|
123
|
+
const limit = getLimitForTrust(trust);
|
|
124
|
+
// Blocked senders are always denied
|
|
125
|
+
if (trust === 'blocked') {
|
|
126
|
+
return {
|
|
127
|
+
allowed: false,
|
|
128
|
+
reason: 'sender is blocked (zero rate limit)',
|
|
129
|
+
remaining: 0,
|
|
130
|
+
limit: 0,
|
|
131
|
+
retryAfterMs: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Normalize sender/recipient for consistent tracking across format variants
|
|
135
|
+
const ch = channel ?? (sender.includes('@') ? 'email' : 'sms');
|
|
136
|
+
const senderKey = normalizeSender(sender, ch);
|
|
137
|
+
let senderTimestamps = senderWindows.get(senderKey) ?? [];
|
|
138
|
+
senderTimestamps = pruneWindow(senderTimestamps, now);
|
|
139
|
+
if (senderTimestamps.length >= limit) {
|
|
140
|
+
// Calculate retry-after from the oldest entry in window
|
|
141
|
+
const oldestTs = senderTimestamps[0];
|
|
142
|
+
const retryAfterMs = oldestTs + config.windowMs - now;
|
|
143
|
+
return {
|
|
144
|
+
allowed: false,
|
|
145
|
+
reason: `per-sender rate limit exceeded (${senderTimestamps.length}/${limit} in window)`,
|
|
146
|
+
remaining: 0,
|
|
147
|
+
limit,
|
|
148
|
+
retryAfterMs: Math.max(0, retryAfterMs),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Check per-recipient global limit
|
|
152
|
+
const recipientCh = channel ?? (recipient.includes('@') ? 'email' : 'sms');
|
|
153
|
+
const recipientKey = normalizeSender(recipient, recipientCh);
|
|
154
|
+
let recipientTimestamps = recipientWindows.get(recipientKey) ?? [];
|
|
155
|
+
recipientTimestamps = pruneWindow(recipientTimestamps, now);
|
|
156
|
+
if (recipientTimestamps.length >= config.recipientGlobalLimit) {
|
|
157
|
+
const oldestTs = recipientTimestamps[0];
|
|
158
|
+
const retryAfterMs = oldestTs + config.windowMs - now;
|
|
159
|
+
return {
|
|
160
|
+
allowed: false,
|
|
161
|
+
reason: `global recipient rate limit exceeded (${recipientTimestamps.length}/${config.recipientGlobalLimit} in window)`,
|
|
162
|
+
remaining: 0,
|
|
163
|
+
limit: config.recipientGlobalLimit,
|
|
164
|
+
retryAfterMs: Math.max(0, retryAfterMs),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Message is allowed — record it
|
|
168
|
+
senderTimestamps.push(now);
|
|
169
|
+
senderWindows.set(senderKey, senderTimestamps);
|
|
170
|
+
recipientTimestamps.push(now);
|
|
171
|
+
recipientWindows.set(recipientKey, recipientTimestamps);
|
|
172
|
+
return {
|
|
173
|
+
allowed: true,
|
|
174
|
+
reason: null,
|
|
175
|
+
remaining: limit - senderTimestamps.length,
|
|
176
|
+
limit,
|
|
177
|
+
retryAfterMs: null,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
getStats() {
|
|
181
|
+
return {
|
|
182
|
+
activeSenders: senderWindows.size,
|
|
183
|
+
activeRecipients: recipientWindows.size,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spam filtering utility for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides pre-processing gate that detects bulk/marketing email,
|
|
5
|
+
* SMS spam signals, and supports configurable allowlist/blocklist.
|
|
6
|
+
*
|
|
7
|
+
* LIMITATION: Header-based spam detection is inherently best-effort.
|
|
8
|
+
* Sophisticated spammers can omit or forge headers to bypass these checks.
|
|
9
|
+
* This filter catches the majority of bulk/marketing email and common SMS
|
|
10
|
+
* spam patterns, but should be complemented with content-based analysis
|
|
11
|
+
* and external reputation services for production use at scale.
|
|
12
|
+
*
|
|
13
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
14
|
+
*/
|
|
15
|
+
/** Supported inbound message channels */
|
|
16
|
+
export type MessageChannel = 'email' | 'sms';
|
|
17
|
+
/** Inbound message to evaluate for spam */
|
|
18
|
+
export interface InboundMessage {
|
|
19
|
+
/** Message channel (email or sms) */
|
|
20
|
+
channel: MessageChannel;
|
|
21
|
+
/** Sender identifier (email address or phone number) */
|
|
22
|
+
sender: string;
|
|
23
|
+
/** Recipient identifier */
|
|
24
|
+
recipient: string;
|
|
25
|
+
/** Message body text */
|
|
26
|
+
body: string;
|
|
27
|
+
/** Email headers (only present for email channel) */
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** Result of spam evaluation */
|
|
31
|
+
export interface SpamFilterResult {
|
|
32
|
+
/** Whether the message is classified as spam */
|
|
33
|
+
isSpam: boolean;
|
|
34
|
+
/** Human-readable reason for the classification, null if not spam */
|
|
35
|
+
reason: string | null;
|
|
36
|
+
/** The channel of the evaluated message */
|
|
37
|
+
channel: MessageChannel;
|
|
38
|
+
/** The sender of the evaluated message */
|
|
39
|
+
sender: string;
|
|
40
|
+
}
|
|
41
|
+
/** Configurable spam filter settings */
|
|
42
|
+
export interface SpamFilterConfig {
|
|
43
|
+
/** Senders that are always allowed (bypass all checks) */
|
|
44
|
+
allowlist: string[];
|
|
45
|
+
/** Senders that are always blocked */
|
|
46
|
+
blocklist: string[];
|
|
47
|
+
/** Threshold for X-Spam-Score header (emails scoring above this are spam) */
|
|
48
|
+
spamScoreThreshold: number;
|
|
49
|
+
/** Patterns in X-Mailer header that indicate bulk mailers */
|
|
50
|
+
bulkMailerPatterns: string[];
|
|
51
|
+
/** Patterns in SMS body that indicate spam */
|
|
52
|
+
smsSpamPatterns: string[];
|
|
53
|
+
}
|
|
54
|
+
/** Default spam filter configuration */
|
|
55
|
+
export declare const DEFAULT_SPAM_FILTER_CONFIG: SpamFilterConfig;
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a sender identifier for consistent comparison.
|
|
58
|
+
*
|
|
59
|
+
* - Email: lowercased, with `+` alias portion stripped (e.g. user+tag@gmail.com -> user@gmail.com)
|
|
60
|
+
* - Phone: non-digit characters stripped, leading country code '1' normalized to '+1'
|
|
61
|
+
*
|
|
62
|
+
* This prevents bypass via formatting variants like `+1...` vs `1...`
|
|
63
|
+
* or `user+spam@gmail.com` vs `user@gmail.com`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function normalizeSender(sender: string, channel: MessageChannel): string;
|
|
66
|
+
/**
|
|
67
|
+
* Evaluate whether an inbound message is spam.
|
|
68
|
+
*
|
|
69
|
+
* Checks are applied in this order:
|
|
70
|
+
* 1. Allowlist (always passes)
|
|
71
|
+
* 2. Blocklist (always fails)
|
|
72
|
+
* 3. Channel-specific spam checks (email headers, SMS signals)
|
|
73
|
+
*
|
|
74
|
+
* @param message - The inbound message to evaluate
|
|
75
|
+
* @param config - Optional spam filter configuration (uses defaults if omitted)
|
|
76
|
+
* @returns SpamFilterResult with classification and reason
|
|
77
|
+
*/
|
|
78
|
+
export declare function isSpam(message: InboundMessage, config?: SpamFilterConfig): SpamFilterResult;
|
|
79
|
+
//# sourceMappingURL=spam-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spam-filter.d.ts","sourceRoot":"","sources":["../../src/utils/spam-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,yCAAyC;AACzC,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,KAAK,CAAC;AAE7C,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,OAAO,EAAE,cAAc,CAAC;IACxB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,MAAM,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2CAA2C;IAC3C,OAAO,EAAE,cAAc,CAAC;IACxB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sCAAsC;IACtC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,6EAA6E;IAC7E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,8CAA8C;IAC9C,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,wCAAwC;AACxC,eAAO,MAAM,0BAA0B,EAAE,gBA4BxC,CAAC;AAWF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAK/E;AAwDD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,GAAE,gBAA6C,GAAG,gBAAgB,CAiCvH"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spam filtering utility for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides pre-processing gate that detects bulk/marketing email,
|
|
5
|
+
* SMS spam signals, and supports configurable allowlist/blocklist.
|
|
6
|
+
*
|
|
7
|
+
* LIMITATION: Header-based spam detection is inherently best-effort.
|
|
8
|
+
* Sophisticated spammers can omit or forge headers to bypass these checks.
|
|
9
|
+
* This filter catches the majority of bulk/marketing email and common SMS
|
|
10
|
+
* spam patterns, but should be complemented with content-based analysis
|
|
11
|
+
* and external reputation services for production use at scale.
|
|
12
|
+
*
|
|
13
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
14
|
+
*/
|
|
15
|
+
/** Default spam filter configuration */
|
|
16
|
+
export const DEFAULT_SPAM_FILTER_CONFIG = {
|
|
17
|
+
allowlist: [],
|
|
18
|
+
blocklist: [],
|
|
19
|
+
spamScoreThreshold: 5.0,
|
|
20
|
+
bulkMailerPatterns: [
|
|
21
|
+
'mailchimp',
|
|
22
|
+
'sendgrid',
|
|
23
|
+
'constantcontact',
|
|
24
|
+
'mailgun',
|
|
25
|
+
'campaign monitor',
|
|
26
|
+
'hubspot',
|
|
27
|
+
'marketo',
|
|
28
|
+
'pardot',
|
|
29
|
+
'sendinblue',
|
|
30
|
+
'brevo',
|
|
31
|
+
],
|
|
32
|
+
smsSpamPatterns: [
|
|
33
|
+
'you have won',
|
|
34
|
+
'you have been selected',
|
|
35
|
+
'click here',
|
|
36
|
+
'free gift',
|
|
37
|
+
'act now',
|
|
38
|
+
'limited time',
|
|
39
|
+
'congratulations',
|
|
40
|
+
'claim your',
|
|
41
|
+
'verify now',
|
|
42
|
+
'your account has been',
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
/** Email header values indicating bulk/list mail */
|
|
46
|
+
const BULK_PRECEDENCE_VALUES = new Set(['bulk', 'list', 'junk']);
|
|
47
|
+
/** SMS opt-out keywords that indicate marketing messages */
|
|
48
|
+
const SMS_OPT_OUT_KEYWORDS = ['stop', 'unsubscribe', 'opt-out', 'opt out', 'cancel', 'quit', 'end'];
|
|
49
|
+
/** Maximum length for a short code sender (SMS) */
|
|
50
|
+
const SHORT_CODE_MAX_LENGTH = 6;
|
|
51
|
+
/**
|
|
52
|
+
* Normalize a sender identifier for consistent comparison.
|
|
53
|
+
*
|
|
54
|
+
* - Email: lowercased, with `+` alias portion stripped (e.g. user+tag@gmail.com -> user@gmail.com)
|
|
55
|
+
* - Phone: non-digit characters stripped, leading country code '1' normalized to '+1'
|
|
56
|
+
*
|
|
57
|
+
* This prevents bypass via formatting variants like `+1...` vs `1...`
|
|
58
|
+
* or `user+spam@gmail.com` vs `user@gmail.com`.
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeSender(sender, channel) {
|
|
61
|
+
if (channel === 'email') {
|
|
62
|
+
return normalizeEmail(sender);
|
|
63
|
+
}
|
|
64
|
+
return normalizePhone(sender);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Normalize an email address for consistent comparison.
|
|
68
|
+
* Lowercases and strips `+` alias tags (e.g. user+tag@example.com -> user@example.com).
|
|
69
|
+
*/
|
|
70
|
+
function normalizeEmail(email) {
|
|
71
|
+
const lower = email.toLowerCase().trim();
|
|
72
|
+
const atIndex = lower.indexOf('@');
|
|
73
|
+
if (atIndex === -1) {
|
|
74
|
+
return lower;
|
|
75
|
+
}
|
|
76
|
+
const localPart = lower.slice(0, atIndex);
|
|
77
|
+
const domain = lower.slice(atIndex);
|
|
78
|
+
// Strip + alias
|
|
79
|
+
const plusIndex = localPart.indexOf('+');
|
|
80
|
+
if (plusIndex !== -1) {
|
|
81
|
+
return localPart.slice(0, plusIndex) + domain;
|
|
82
|
+
}
|
|
83
|
+
return lower;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalize a phone number for consistent comparison.
|
|
87
|
+
* Strips non-digit characters and normalizes country code.
|
|
88
|
+
*
|
|
89
|
+
* TODO: Non-US phone normalization is best-effort. Numbers like `442079460958`
|
|
90
|
+
* (without +) and `+442079460958` (with +) will normalize differently because
|
|
91
|
+
* we can only reliably detect US/NANP 10/11-digit patterns. A full solution
|
|
92
|
+
* would require a phone number parsing library (e.g. libphonenumber) to handle
|
|
93
|
+
* international formats consistently.
|
|
94
|
+
*/
|
|
95
|
+
function normalizePhone(phone) {
|
|
96
|
+
// Strip everything except digits and leading +
|
|
97
|
+
const hasPlus = phone.startsWith('+');
|
|
98
|
+
const digits = phone.replace(/[^0-9]/g, '');
|
|
99
|
+
if (digits.length === 0) {
|
|
100
|
+
return phone.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
// Normalize US numbers: 10 digits -> +1XXXXXXXXXX, 11 digits starting with 1 -> +1XXXXXXXXXX
|
|
103
|
+
if (digits.length === 10) {
|
|
104
|
+
return `+1${digits}`;
|
|
105
|
+
}
|
|
106
|
+
if (digits.length === 11 && digits.startsWith('1')) {
|
|
107
|
+
return `+${digits}`;
|
|
108
|
+
}
|
|
109
|
+
// For other formats, preserve the + prefix if it was present
|
|
110
|
+
return hasPlus ? `+${digits}` : digits;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Evaluate whether an inbound message is spam.
|
|
114
|
+
*
|
|
115
|
+
* Checks are applied in this order:
|
|
116
|
+
* 1. Allowlist (always passes)
|
|
117
|
+
* 2. Blocklist (always fails)
|
|
118
|
+
* 3. Channel-specific spam checks (email headers, SMS signals)
|
|
119
|
+
*
|
|
120
|
+
* @param message - The inbound message to evaluate
|
|
121
|
+
* @param config - Optional spam filter configuration (uses defaults if omitted)
|
|
122
|
+
* @returns SpamFilterResult with classification and reason
|
|
123
|
+
*/
|
|
124
|
+
export function isSpam(message, config = DEFAULT_SPAM_FILTER_CONFIG) {
|
|
125
|
+
const normalizedSender = normalizeSender(message.sender, message.channel);
|
|
126
|
+
const result = {
|
|
127
|
+
isSpam: false,
|
|
128
|
+
reason: null,
|
|
129
|
+
channel: message.channel,
|
|
130
|
+
sender: message.sender,
|
|
131
|
+
};
|
|
132
|
+
// Check allowlist first (always passes) — normalize both sides for consistent matching
|
|
133
|
+
if (config.allowlist.some((allowed) => normalizeSender(allowed, message.channel) === normalizedSender)) {
|
|
134
|
+
result.reason = 'allowlisted sender';
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
// Check blocklist (always fails) — normalize both sides for consistent matching
|
|
138
|
+
if (config.blocklist.some((blocked) => normalizeSender(blocked, message.channel) === normalizedSender)) {
|
|
139
|
+
result.isSpam = true;
|
|
140
|
+
result.reason = 'blocklisted sender';
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
// Channel-specific checks
|
|
144
|
+
if (message.channel === 'email') {
|
|
145
|
+
return checkEmailSpam(message, config, result);
|
|
146
|
+
}
|
|
147
|
+
if (message.channel === 'sms') {
|
|
148
|
+
return checkSmsSpam(message, config, result);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check email-specific spam signals.
|
|
154
|
+
*/
|
|
155
|
+
function checkEmailSpam(message, config, result) {
|
|
156
|
+
const headers = normalizeHeaders(message.headers);
|
|
157
|
+
// Check Precedence header for bulk/list
|
|
158
|
+
const precedence = headers.precedence;
|
|
159
|
+
if (precedence && BULK_PRECEDENCE_VALUES.has(precedence.toLowerCase())) {
|
|
160
|
+
result.isSpam = true;
|
|
161
|
+
result.reason = 'bulk precedence header detected';
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
// Check List-Unsubscribe header
|
|
165
|
+
if (headers['list-unsubscribe']) {
|
|
166
|
+
result.isSpam = true;
|
|
167
|
+
result.reason = 'list-unsubscribe header detected (mailing list)';
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
// Check X-Spam-Score
|
|
171
|
+
const spamScore = headers['x-spam-score'];
|
|
172
|
+
if (spamScore) {
|
|
173
|
+
const score = Number.parseFloat(spamScore);
|
|
174
|
+
if (!Number.isNaN(score) && score >= config.spamScoreThreshold) {
|
|
175
|
+
result.isSpam = true;
|
|
176
|
+
result.reason = `high spam score (${score} >= ${config.spamScoreThreshold})`;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Check X-Mailer for known bulk mailers
|
|
181
|
+
const xMailer = headers['x-mailer'];
|
|
182
|
+
if (xMailer) {
|
|
183
|
+
const mailerLower = xMailer.toLowerCase();
|
|
184
|
+
for (const pattern of config.bulkMailerPatterns) {
|
|
185
|
+
if (mailerLower.includes(pattern.toLowerCase())) {
|
|
186
|
+
result.isSpam = true;
|
|
187
|
+
result.reason = `bulk mailer detected: ${pattern}`;
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check SMS-specific spam signals.
|
|
196
|
+
*/
|
|
197
|
+
function checkSmsSpam(message, config, result) {
|
|
198
|
+
// Check for short code sender
|
|
199
|
+
const senderDigits = message.sender.replace(/[^0-9]/g, '');
|
|
200
|
+
if (senderDigits.length > 0 && senderDigits.length <= SHORT_CODE_MAX_LENGTH) {
|
|
201
|
+
result.isSpam = true;
|
|
202
|
+
result.reason = 'short code sender detected';
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
const bodyLower = message.body.toLowerCase();
|
|
206
|
+
// Check for opt-out keywords (indicates marketing/automated message)
|
|
207
|
+
for (const keyword of SMS_OPT_OUT_KEYWORDS) {
|
|
208
|
+
if (bodyLower.includes(keyword)) {
|
|
209
|
+
result.isSpam = true;
|
|
210
|
+
result.reason = `opt-out keyword detected: ${keyword}`;
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Check for common SMS spam patterns
|
|
215
|
+
for (const pattern of config.smsSpamPatterns) {
|
|
216
|
+
if (bodyLower.includes(pattern.toLowerCase())) {
|
|
217
|
+
result.isSpam = true;
|
|
218
|
+
result.reason = `spam pattern detected: ${pattern}`;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Normalize email headers to lowercase keys for consistent lookup.
|
|
226
|
+
*/
|
|
227
|
+
function normalizeHeaders(headers) {
|
|
228
|
+
if (!headers) {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
const normalized = {};
|
|
232
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
233
|
+
normalized[key.toLowerCase()] = value;
|
|
234
|
+
}
|
|
235
|
+
return normalized;
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=spam-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spam-filter.js","sourceRoot":"","sources":["../../src/utils/spam-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token budget tracking utility for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides configurable daily and monthly caps on token expenditure
|
|
5
|
+
* to protect against cost overruns from bulk inbound messages.
|
|
6
|
+
*
|
|
7
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
8
|
+
*/
|
|
9
|
+
/** Configuration for token budget tracking */
|
|
10
|
+
export interface TokenBudgetConfig {
|
|
11
|
+
/** Whether token budget enforcement is enabled */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Maximum tokens per day (optional, no limit if omitted) */
|
|
14
|
+
dailyTokenLimit?: number;
|
|
15
|
+
/** Maximum tokens per month (optional, no limit if omitted) */
|
|
16
|
+
monthlyTokenLimit?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Result of a token budget check */
|
|
19
|
+
export interface TokenBudgetResult {
|
|
20
|
+
/** Whether the token request is allowed */
|
|
21
|
+
allowed: boolean;
|
|
22
|
+
/** Human-readable reason if denied, null if allowed */
|
|
23
|
+
reason: string | null;
|
|
24
|
+
/** Tokens remaining in daily budget (undefined if no daily limit) */
|
|
25
|
+
remainingDaily?: number;
|
|
26
|
+
/** Tokens remaining in monthly budget (undefined if no monthly limit) */
|
|
27
|
+
remainingMonthly?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Token budget usage statistics */
|
|
30
|
+
export interface TokenBudgetStats {
|
|
31
|
+
/** Tokens used today */
|
|
32
|
+
dailyUsed: number;
|
|
33
|
+
/** Tokens used this month */
|
|
34
|
+
monthlyUsed: number;
|
|
35
|
+
/** Daily token limit (undefined if not set) */
|
|
36
|
+
dailyLimit?: number;
|
|
37
|
+
/** Monthly token limit (undefined if not set) */
|
|
38
|
+
monthlyLimit?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Token budget instance */
|
|
41
|
+
export interface TokenBudget {
|
|
42
|
+
/** Check if a token expenditure is allowed (read-only, does not record) */
|
|
43
|
+
check(tokens: number): TokenBudgetResult;
|
|
44
|
+
/** Record actual token usage */
|
|
45
|
+
record(tokens: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically check and record token usage.
|
|
48
|
+
* Prevents TOCTOU races where concurrent check() calls both pass
|
|
49
|
+
* before either records, allowing the budget to be exceeded.
|
|
50
|
+
* Returns the check result; if allowed, tokens are already recorded.
|
|
51
|
+
*/
|
|
52
|
+
tryConsume(tokens: number): TokenBudgetResult;
|
|
53
|
+
/** Get current usage statistics */
|
|
54
|
+
getStats(): TokenBudgetStats;
|
|
55
|
+
}
|
|
56
|
+
/** Default configuration (budget disabled) */
|
|
57
|
+
export declare const DEFAULT_TOKEN_BUDGET_CONFIG: TokenBudgetConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Create a token budget tracker.
|
|
60
|
+
*
|
|
61
|
+
* Tracks daily and monthly token usage with automatic reset at
|
|
62
|
+
* UTC midnight (daily) and UTC month boundary (monthly).
|
|
63
|
+
*
|
|
64
|
+
* @param config - Token budget configuration
|
|
65
|
+
* @returns TokenBudget instance
|
|
66
|
+
*/
|
|
67
|
+
export declare function createTokenBudget(config?: TokenBudgetConfig): TokenBudget;
|
|
68
|
+
//# sourceMappingURL=token-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/utils/token-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,4BAA4B;AAC5B,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAAC;IACzC,gCAAgC;IAChC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAAC;IAC9C,mCAAmC;IACnC,QAAQ,IAAI,gBAAgB,CAAC;CAC9B;AAED,8CAA8C;AAC9C,eAAO,MAAM,2BAA2B,EAAE,iBAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,iBAA+C,GAAG,WAAW,CAyItG"}
|