@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound message gate — unified pre-processing gate for inbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates spam filter, rate limiter, and token budget as a single
|
|
5
|
+
* evaluation point before any message processing (embedding, linking, etc.).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: All state (rate limiter windows, token budget counters) is held
|
|
8
|
+
* in-memory and does not persist across process restarts or span multiple
|
|
9
|
+
* instances. This is acceptable for single-instance deployments. For
|
|
10
|
+
* multi-instance deployments, the rate limiter and token budget would need
|
|
11
|
+
* to be backed by skill_store or an external store (e.g. Redis) to share
|
|
12
|
+
* state across processes.
|
|
13
|
+
*
|
|
14
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
15
|
+
*/
|
|
16
|
+
import { type SpamFilterConfig, type InboundMessage } from './spam-filter.js';
|
|
17
|
+
import { type RateLimiterConfig, type SenderTrust } from './rate-limiter.js';
|
|
18
|
+
import { type TokenBudgetConfig } from './token-budget.js';
|
|
19
|
+
import type { Logger } from '../logger.js';
|
|
20
|
+
/** Re-export InboundMessage for convenience */
|
|
21
|
+
export type { InboundMessage } from './spam-filter.js';
|
|
22
|
+
/** Actions the gate can take on a message */
|
|
23
|
+
export type GateAction = 'allow' | 'reject' | 'rate_limited' | 'budget_exceeded' | 'defer';
|
|
24
|
+
/** Result of the inbound gate evaluation */
|
|
25
|
+
export interface InboundGateDecision {
|
|
26
|
+
/** Action to take on the message */
|
|
27
|
+
action: GateAction;
|
|
28
|
+
/** Human-readable reason for the decision, null if allowed */
|
|
29
|
+
reason: string | null;
|
|
30
|
+
/** Whether to skip embedding for this message */
|
|
31
|
+
skipEmbedding: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Configuration for the inbound gate */
|
|
34
|
+
export interface InboundGateConfig {
|
|
35
|
+
/** Spam filter configuration */
|
|
36
|
+
spamFilter: SpamFilterConfig;
|
|
37
|
+
/** Rate limiter configuration */
|
|
38
|
+
rateLimiter: RateLimiterConfig;
|
|
39
|
+
/** Token budget configuration */
|
|
40
|
+
tokenBudget: TokenBudgetConfig;
|
|
41
|
+
/** Whether to defer processing for unknown senders */
|
|
42
|
+
deferUnknownSenders?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Aggregate gate statistics */
|
|
45
|
+
export interface InboundGateStats {
|
|
46
|
+
/** Total messages evaluated */
|
|
47
|
+
totalEvaluated: number;
|
|
48
|
+
/** Messages allowed through */
|
|
49
|
+
allowed: number;
|
|
50
|
+
/** Messages rejected as spam */
|
|
51
|
+
rejected: number;
|
|
52
|
+
/** Messages rate-limited */
|
|
53
|
+
rateLimited: number;
|
|
54
|
+
/** Messages denied due to budget */
|
|
55
|
+
budgetExceeded: number;
|
|
56
|
+
/** Messages deferred */
|
|
57
|
+
deferred: number;
|
|
58
|
+
}
|
|
59
|
+
/** Inbound gate instance */
|
|
60
|
+
export interface InboundGate {
|
|
61
|
+
/**
|
|
62
|
+
* Evaluate a message and return a gate decision.
|
|
63
|
+
* When estimatedTokens > 0 and the message is allowed, the token budget
|
|
64
|
+
* is atomically consumed during evaluation — callers must NOT separately
|
|
65
|
+
* record token usage or the budget will be double-counted.
|
|
66
|
+
*/
|
|
67
|
+
evaluate(message: InboundMessage, trust: SenderTrust, estimatedTokens?: number): InboundGateDecision;
|
|
68
|
+
/** Get aggregate statistics */
|
|
69
|
+
getStats(): InboundGateStats;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create an inbound message gate.
|
|
73
|
+
*
|
|
74
|
+
* Checks are applied in this order:
|
|
75
|
+
* 1. Spam filter (reject bulk/marketing/known spam)
|
|
76
|
+
* 2. Rate limiter (per-sender and per-recipient caps)
|
|
77
|
+
* 3. Token budget (cost protection)
|
|
78
|
+
* 4. Deferred processing (unknown senders, if configured)
|
|
79
|
+
*
|
|
80
|
+
* @param config - Gate configuration
|
|
81
|
+
* @param logger - Logger for recording decisions
|
|
82
|
+
* @returns InboundGate instance
|
|
83
|
+
*/
|
|
84
|
+
export declare function createInboundGate(config: InboundGateConfig, logger: Logger): InboundGate;
|
|
85
|
+
//# sourceMappingURL=inbound-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound-gate.d.ts","sourceRoot":"","sources":["../../src/utils/inbound-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAU,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAqB,KAAK,iBAAiB,EAAoB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClH,OAAO,EAAqB,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAChG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,+CAA+C;AAC/C,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAE3F,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iDAAiD;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gCAAgC;IAChC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,iCAAiC;IACjC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,iCAAiC;IACjC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,4BAA4B;AAC5B,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACrG,+BAA+B;IAC/B,QAAQ,IAAI,gBAAgB,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CA4GxF"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound message gate — unified pre-processing gate for inbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates spam filter, rate limiter, and token budget as a single
|
|
5
|
+
* evaluation point before any message processing (embedding, linking, etc.).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: All state (rate limiter windows, token budget counters) is held
|
|
8
|
+
* in-memory and does not persist across process restarts or span multiple
|
|
9
|
+
* instances. This is acceptable for single-instance deployments. For
|
|
10
|
+
* multi-instance deployments, the rate limiter and token budget would need
|
|
11
|
+
* to be backed by skill_store or an external store (e.g. Redis) to share
|
|
12
|
+
* state across processes.
|
|
13
|
+
*
|
|
14
|
+
* Part of Issue #1225 — rate limiting and spam protection.
|
|
15
|
+
*/
|
|
16
|
+
import { isSpam } from './spam-filter.js';
|
|
17
|
+
import { createRateLimiter } from './rate-limiter.js';
|
|
18
|
+
import { createTokenBudget } from './token-budget.js';
|
|
19
|
+
/**
|
|
20
|
+
* Create an inbound message gate.
|
|
21
|
+
*
|
|
22
|
+
* Checks are applied in this order:
|
|
23
|
+
* 1. Spam filter (reject bulk/marketing/known spam)
|
|
24
|
+
* 2. Rate limiter (per-sender and per-recipient caps)
|
|
25
|
+
* 3. Token budget (cost protection)
|
|
26
|
+
* 4. Deferred processing (unknown senders, if configured)
|
|
27
|
+
*
|
|
28
|
+
* @param config - Gate configuration
|
|
29
|
+
* @param logger - Logger for recording decisions
|
|
30
|
+
* @returns InboundGate instance
|
|
31
|
+
*/
|
|
32
|
+
export function createInboundGate(config, logger) {
|
|
33
|
+
const rateLimiter = createRateLimiter(config.rateLimiter);
|
|
34
|
+
const tokenBudget = createTokenBudget(config.tokenBudget);
|
|
35
|
+
let totalEvaluated = 0;
|
|
36
|
+
let allowed = 0;
|
|
37
|
+
let rejected = 0;
|
|
38
|
+
let rateLimited = 0;
|
|
39
|
+
let budgetExceeded = 0;
|
|
40
|
+
let deferred = 0;
|
|
41
|
+
return {
|
|
42
|
+
evaluate(message, trust, estimatedTokens = 0) {
|
|
43
|
+
totalEvaluated++;
|
|
44
|
+
// Step 1: Spam filter
|
|
45
|
+
const spamResult = isSpam(message, config.spamFilter);
|
|
46
|
+
if (spamResult.isSpam) {
|
|
47
|
+
rejected++;
|
|
48
|
+
logger.info('inbound gate: spam rejected', {
|
|
49
|
+
sender: message.sender,
|
|
50
|
+
channel: message.channel,
|
|
51
|
+
reason: spamResult.reason,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
action: 'reject',
|
|
55
|
+
reason: `spam: ${spamResult.reason}`,
|
|
56
|
+
skipEmbedding: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Step 2: Rate limiter (pass channel for sender normalization)
|
|
60
|
+
const rateResult = rateLimiter.check(message.sender, message.recipient, trust, message.channel);
|
|
61
|
+
if (!rateResult.allowed) {
|
|
62
|
+
rateLimited++;
|
|
63
|
+
logger.warn('inbound gate: rate limited', {
|
|
64
|
+
sender: message.sender,
|
|
65
|
+
channel: message.channel,
|
|
66
|
+
trust,
|
|
67
|
+
reason: rateResult.reason,
|
|
68
|
+
remaining: rateResult.remaining,
|
|
69
|
+
retryAfterMs: rateResult.retryAfterMs,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
action: 'rate_limited',
|
|
73
|
+
reason: `rate limited: ${rateResult.reason}`,
|
|
74
|
+
skipEmbedding: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Step 3: Token budget (uses tryConsume for atomic check-and-record)
|
|
78
|
+
if (estimatedTokens > 0) {
|
|
79
|
+
const budgetResult = tokenBudget.tryConsume(estimatedTokens);
|
|
80
|
+
if (!budgetResult.allowed) {
|
|
81
|
+
budgetExceeded++;
|
|
82
|
+
logger.warn('inbound gate: token budget exceeded', {
|
|
83
|
+
sender: message.sender,
|
|
84
|
+
channel: message.channel,
|
|
85
|
+
estimatedTokens,
|
|
86
|
+
reason: budgetResult.reason,
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
action: 'budget_exceeded',
|
|
90
|
+
reason: `budget exceeded: ${budgetResult.reason}`,
|
|
91
|
+
skipEmbedding: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Step 4: Deferred processing for unknown senders
|
|
96
|
+
if (config.deferUnknownSenders && trust === 'unknown') {
|
|
97
|
+
deferred++;
|
|
98
|
+
logger.debug('inbound gate: deferring unknown sender', {
|
|
99
|
+
sender: message.sender,
|
|
100
|
+
channel: message.channel,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
action: 'defer',
|
|
104
|
+
reason: 'unknown sender deferred for batch processing',
|
|
105
|
+
skipEmbedding: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Message is allowed
|
|
109
|
+
allowed++;
|
|
110
|
+
logger.debug('inbound gate: allowed', {
|
|
111
|
+
sender: message.sender,
|
|
112
|
+
channel: message.channel,
|
|
113
|
+
trust,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
action: 'allow',
|
|
117
|
+
reason: null,
|
|
118
|
+
skipEmbedding: false,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
getStats() {
|
|
122
|
+
return {
|
|
123
|
+
totalEvaluated,
|
|
124
|
+
allowed,
|
|
125
|
+
rejected,
|
|
126
|
+
rateLimited,
|
|
127
|
+
budgetExceeded,
|
|
128
|
+
deferred,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=inbound-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound-gate.js","sourceRoot":"","sources":["../../src/utils/inbound-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt injection protection utilities for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides layered defence for external message content before LLM exposure:
|
|
5
|
+
* 1. Unicode/control character sanitisation
|
|
6
|
+
* 2. Data boundary marking (spotlighting)
|
|
7
|
+
* 3. Suspicious pattern detection and logging
|
|
8
|
+
*
|
|
9
|
+
* Issue #1224
|
|
10
|
+
*/
|
|
11
|
+
/** Result of injection pattern detection */
|
|
12
|
+
export interface InjectionDetectionResult {
|
|
13
|
+
/** Whether any injection patterns were detected */
|
|
14
|
+
detected: boolean;
|
|
15
|
+
/** Names of the patterns that matched */
|
|
16
|
+
patterns: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Options for wrapping external messages */
|
|
19
|
+
export interface WrapOptions {
|
|
20
|
+
/** Communication channel (sms, email) */
|
|
21
|
+
channel?: string;
|
|
22
|
+
/** Sender name or identifier */
|
|
23
|
+
sender?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Options for context sanitisation */
|
|
26
|
+
export interface ContextSanitizeOptions {
|
|
27
|
+
/** Message direction */
|
|
28
|
+
direction?: 'inbound' | 'outbound';
|
|
29
|
+
/** Communication channel */
|
|
30
|
+
channel?: string;
|
|
31
|
+
/** Sender name */
|
|
32
|
+
sender?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize external message content by removing control characters
|
|
36
|
+
* and invisible Unicode characters that could be used for obfuscation.
|
|
37
|
+
*
|
|
38
|
+
* Preserves legitimate Unicode (emoji, CJK, Arabic, etc.) and whitespace
|
|
39
|
+
* characters (tab, newline, carriage return).
|
|
40
|
+
*/
|
|
41
|
+
export declare function sanitizeExternalMessage(text: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize a metadata field (sender, channel) for safe insertion into
|
|
44
|
+
* the boundary wrapper header. Strips control chars, invisible Unicode,
|
|
45
|
+
* newlines (which could break out of the header line), and boundary markers.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sanitizeMetadataField(field: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Wrap external message content with data boundary markers.
|
|
50
|
+
*
|
|
51
|
+
* Uses the "spotlighting" / data marking pattern to clearly delineate
|
|
52
|
+
* untrusted external content from system instructions. This tells the LLM
|
|
53
|
+
* "this is external data, not instructions to follow."
|
|
54
|
+
*
|
|
55
|
+
* Content, sender, and channel are all sanitized and have boundary markers
|
|
56
|
+
* escaped before wrapping to prevent breakout attacks.
|
|
57
|
+
*/
|
|
58
|
+
export declare function wrapExternalMessage(content: string, options?: WrapOptions): string;
|
|
59
|
+
/**
|
|
60
|
+
* Detect suspicious prompt injection patterns in message content.
|
|
61
|
+
*
|
|
62
|
+
* Returns detection results for logging/monitoring purposes.
|
|
63
|
+
* This function does NOT block or modify the message — it is purely
|
|
64
|
+
* for detection and alerting. Blocking legitimate messages based on
|
|
65
|
+
* pattern matching has too high a false positive rate.
|
|
66
|
+
*
|
|
67
|
+
* The content is sanitized (invisible chars removed) before scanning
|
|
68
|
+
* to prevent Unicode obfuscation from bypassing detection.
|
|
69
|
+
*/
|
|
70
|
+
export declare function detectInjectionPatterns(text: string): InjectionDetectionResult;
|
|
71
|
+
/**
|
|
72
|
+
* Sanitize message content for safe inclusion in LLM context.
|
|
73
|
+
*
|
|
74
|
+
* For inbound (external) messages: sanitizes and wraps with boundary markers.
|
|
75
|
+
* For outbound messages: sanitizes only (no boundary wrapping needed).
|
|
76
|
+
*
|
|
77
|
+
* This is the primary function to call when preparing message content
|
|
78
|
+
* for any LLM-facing output (auto-recall context, tool results, etc.).
|
|
79
|
+
*/
|
|
80
|
+
export declare function sanitizeMessageForContext(content: string, options?: ContextSanitizeOptions): string;
|
|
81
|
+
//# sourceMappingURL=injection-protection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection-protection.d.ts","sourceRoot":"","sources":["../../src/utils/injection-protection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsFH,4CAA4C;AAC5C,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,QAAQ,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACrC,wBAAwB;IACxB,SAAS,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACnC,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5D;AAcD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAmBtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,wBAAwB,CAc9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAQR"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt injection protection utilities for inbound message processing.
|
|
3
|
+
*
|
|
4
|
+
* Provides layered defence for external message content before LLM exposure:
|
|
5
|
+
* 1. Unicode/control character sanitisation
|
|
6
|
+
* 2. Data boundary marking (spotlighting)
|
|
7
|
+
* 3. Suspicious pattern detection and logging
|
|
8
|
+
*
|
|
9
|
+
* Issue #1224
|
|
10
|
+
*/
|
|
11
|
+
/** Boundary markers for external message content */
|
|
12
|
+
const EXTERNAL_MSG_START = '[EXTERNAL_MSG_START]';
|
|
13
|
+
const EXTERNAL_MSG_END = '[EXTERNAL_MSG_END]';
|
|
14
|
+
/**
|
|
15
|
+
* Regex matching control characters to strip (ASCII 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F).
|
|
16
|
+
* Preserves tab (0x09), newline (0x0A), and carriage return (0x0D).
|
|
17
|
+
*/
|
|
18
|
+
const CONTROL_CHARS_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
19
|
+
/**
|
|
20
|
+
* Regex matching Unicode characters commonly used for text direction manipulation
|
|
21
|
+
* and zero-width obfuscation:
|
|
22
|
+
* - U+200B Zero-width space
|
|
23
|
+
* - U+200C Zero-width non-joiner
|
|
24
|
+
* - U+200D Zero-width joiner
|
|
25
|
+
* - U+200E Left-to-right mark
|
|
26
|
+
* - U+200F Right-to-left mark
|
|
27
|
+
* - U+202A Left-to-right embedding
|
|
28
|
+
* - U+202B Right-to-left embedding
|
|
29
|
+
* - U+202C Pop directional formatting
|
|
30
|
+
* - U+202D Left-to-right override
|
|
31
|
+
* - U+202E Right-to-left override
|
|
32
|
+
* - U+2066 Left-to-right isolate
|
|
33
|
+
* - U+2067 Right-to-left isolate
|
|
34
|
+
* - U+2068 First strong isolate
|
|
35
|
+
* - U+2069 Pop directional isolate
|
|
36
|
+
* - U+FEFF Byte order mark / zero-width no-break space
|
|
37
|
+
*/
|
|
38
|
+
const UNICODE_INVISIBLE_REGEX = /[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g;
|
|
39
|
+
const INJECTION_PATTERNS = [
|
|
40
|
+
{
|
|
41
|
+
name: 'instruction_override',
|
|
42
|
+
regex: /\b(?:ignore|disregard|forget|override|bypass)\b.{0,30}\b(?:previous|prior|above|earlier|all|your|system|safety)\b.{0,30}\b(?:instructions?|rules?|guidelines?|prompts?|constraints?|directives?|and\b|do\b|instead\b)/i,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'role_reassignment',
|
|
46
|
+
regex: /\b(?:you are now|act as|pretend (?:you are|to be)|roleplay as|behave as)\b.{0,50}\b(?:ai|assistant|bot|agent|unrestricted|dan|jailbreak|without.*?(?:restrictions?|limits?|safety|guidelines?))\b/i,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'new_instructions',
|
|
50
|
+
regex: /\b(?:new instructions?|updated? instructions?|revised? instructions?|important (?:system )?update)\b\s*[:]/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'system_prompt_override',
|
|
54
|
+
regex: /^(?:\s*[-=]{3,}\s*\n)?(?:SYSTEM|ADMIN|DEVELOPER|ROOT)\s*[:>]/im,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'forget_everything',
|
|
58
|
+
regex: /\bforget (?:everything|all)\b.{0,30}\b(?:you (?:know|learned|were told)|and (?:start|begin))\b/i,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'prompt_delimiter_exploit',
|
|
62
|
+
regex: /```\s*(?:system|admin|instructions?|prompt)\b/i,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'tool_call_injection',
|
|
66
|
+
regex: /\b(?:call|use|invoke|execute|run)\b.{0,20}\b(?:the\s+)?(?:sms_send|email_send|memory_store|memory_forget|todo_create|contact_create|memory_recall)\b.{0,20}\btool\b/i,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'data_exfiltration',
|
|
70
|
+
regex: /\b(?:send|forward|share|export|transmit|email|text)\b.{0,30}\b(?:all|every|the)\b.{0,20}\b(?:memories?|contacts?|data|information|messages?|threads?|projects?|todos?)\b.{0,30}\b(?:to|@)\b/i,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'system_note_injection',
|
|
74
|
+
regex: /\[\s*(?:SYSTEM|ADMIN|INTERNAL)\s+(?:NOTE|MESSAGE|OVERRIDE|UPDATE)\s*[:]/i,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'remember_for_later',
|
|
78
|
+
regex: /\bremember (?:this|that)\b.{0,30}\b(?:for later|when (?:someone|anyone|a user|the user))\b.{0,30}\b(?:send|forward|share|export)\b/i,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* Sanitize external message content by removing control characters
|
|
83
|
+
* and invisible Unicode characters that could be used for obfuscation.
|
|
84
|
+
*
|
|
85
|
+
* Preserves legitimate Unicode (emoji, CJK, Arabic, etc.) and whitespace
|
|
86
|
+
* characters (tab, newline, carriage return).
|
|
87
|
+
*/
|
|
88
|
+
export function sanitizeExternalMessage(text) {
|
|
89
|
+
return text
|
|
90
|
+
.replace(CONTROL_CHARS_REGEX, '')
|
|
91
|
+
.replace(UNICODE_INVISIBLE_REGEX, '')
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Escape boundary marker keywords in a string to prevent breakout attacks.
|
|
96
|
+
* Replaces the keyword portion (EXTERNAL_MSG_START / EXTERNAL_MSG_END)
|
|
97
|
+
* regardless of surrounding brackets, since formatting code may supply
|
|
98
|
+
* brackets that complete a partial marker (e.g., channel `[...START` + `]`).
|
|
99
|
+
*/
|
|
100
|
+
function escapeBoundaryMarkers(text) {
|
|
101
|
+
return text
|
|
102
|
+
.replace(/EXTERNAL_MSG_START/g, 'EXTERNAL_MSG_START_ESCAPED')
|
|
103
|
+
.replace(/EXTERNAL_MSG_END/g, 'EXTERNAL_MSG_END_ESCAPED');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Sanitize a metadata field (sender, channel) for safe insertion into
|
|
107
|
+
* the boundary wrapper header. Strips control chars, invisible Unicode,
|
|
108
|
+
* newlines (which could break out of the header line), and boundary markers.
|
|
109
|
+
*/
|
|
110
|
+
export function sanitizeMetadataField(field) {
|
|
111
|
+
return escapeBoundaryMarkers(sanitizeExternalMessage(field).replace(/[\r\n]/g, ' '));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap external message content with data boundary markers.
|
|
115
|
+
*
|
|
116
|
+
* Uses the "spotlighting" / data marking pattern to clearly delineate
|
|
117
|
+
* untrusted external content from system instructions. This tells the LLM
|
|
118
|
+
* "this is external data, not instructions to follow."
|
|
119
|
+
*
|
|
120
|
+
* Content, sender, and channel are all sanitized and have boundary markers
|
|
121
|
+
* escaped before wrapping to prevent breakout attacks.
|
|
122
|
+
*/
|
|
123
|
+
export function wrapExternalMessage(content, options = {}) {
|
|
124
|
+
const sanitized = sanitizeExternalMessage(content);
|
|
125
|
+
// Escape any existing boundary markers in the content to prevent breakout
|
|
126
|
+
const escaped = escapeBoundaryMarkers(sanitized);
|
|
127
|
+
const attribution = [];
|
|
128
|
+
if (options.channel) {
|
|
129
|
+
attribution.push(`[${sanitizeMetadataField(options.channel)}]`);
|
|
130
|
+
}
|
|
131
|
+
if (options.sender) {
|
|
132
|
+
attribution.push(`from: ${sanitizeMetadataField(options.sender)}`);
|
|
133
|
+
}
|
|
134
|
+
const header = attribution.length > 0
|
|
135
|
+
? `${EXTERNAL_MSG_START} ${attribution.join(' ')}`
|
|
136
|
+
: EXTERNAL_MSG_START;
|
|
137
|
+
return `${header}\n${escaped}\n${EXTERNAL_MSG_END}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Detect suspicious prompt injection patterns in message content.
|
|
141
|
+
*
|
|
142
|
+
* Returns detection results for logging/monitoring purposes.
|
|
143
|
+
* This function does NOT block or modify the message — it is purely
|
|
144
|
+
* for detection and alerting. Blocking legitimate messages based on
|
|
145
|
+
* pattern matching has too high a false positive rate.
|
|
146
|
+
*
|
|
147
|
+
* The content is sanitized (invisible chars removed) before scanning
|
|
148
|
+
* to prevent Unicode obfuscation from bypassing detection.
|
|
149
|
+
*/
|
|
150
|
+
export function detectInjectionPatterns(text) {
|
|
151
|
+
const sanitized = sanitizeExternalMessage(text);
|
|
152
|
+
const matched = [];
|
|
153
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
154
|
+
if (pattern.regex.test(sanitized)) {
|
|
155
|
+
matched.push(pattern.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
detected: matched.length > 0,
|
|
160
|
+
patterns: matched,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sanitize message content for safe inclusion in LLM context.
|
|
165
|
+
*
|
|
166
|
+
* For inbound (external) messages: sanitizes and wraps with boundary markers.
|
|
167
|
+
* For outbound messages: sanitizes only (no boundary wrapping needed).
|
|
168
|
+
*
|
|
169
|
+
* This is the primary function to call when preparing message content
|
|
170
|
+
* for any LLM-facing output (auto-recall context, tool results, etc.).
|
|
171
|
+
*/
|
|
172
|
+
export function sanitizeMessageForContext(content, options = {}) {
|
|
173
|
+
const { direction = 'inbound', channel, sender } = options;
|
|
174
|
+
if (direction === 'outbound') {
|
|
175
|
+
return sanitizeExternalMessage(content);
|
|
176
|
+
}
|
|
177
|
+
return wrapExternalMessage(content, { channel, sender });
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=injection-protection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection-protection.js","sourceRoot":"","sources":["../../src/utils/injection-protection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nominatim reverse geocoding client with LRU cache.
|
|
3
|
+
* Resolves lat/lng to human-readable address and place label.
|
|
4
|
+
*/
|
|
5
|
+
export interface GeocodedLocation {
|
|
6
|
+
address: string;
|
|
7
|
+
placeLabel: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reverse geocode a lat/lng pair via Nominatim.
|
|
11
|
+
* Returns null on any failure (timeout, network error, bad response).
|
|
12
|
+
*/
|
|
13
|
+
export declare function reverseGeocode(lat: number, lng: number, nominatimUrl: string): Promise<GeocodedLocation | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Clears the geocode cache (for testing).
|
|
16
|
+
*/
|
|
17
|
+
export declare function clearGeocodeCache(): void;
|
|
18
|
+
//# sourceMappingURL=nominatim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nominatim.d.ts","sourceRoot":"","sources":["../../src/utils/nominatim.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAYD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA2ClC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nominatim reverse geocoding client with LRU cache.
|
|
3
|
+
* Resolves lat/lng to human-readable address and place label.
|
|
4
|
+
*/
|
|
5
|
+
const geocodeCache = new Map();
|
|
6
|
+
const MAX_CACHE_SIZE = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Rounds coordinates to ~100m precision for cache key deduplication.
|
|
9
|
+
*/
|
|
10
|
+
function cacheKey(lat, lng) {
|
|
11
|
+
return `${Math.round(lat * 1000) / 1000},${Math.round(lng * 1000) / 1000}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reverse geocode a lat/lng pair via Nominatim.
|
|
15
|
+
* Returns null on any failure (timeout, network error, bad response).
|
|
16
|
+
*/
|
|
17
|
+
export async function reverseGeocode(lat, lng, nominatimUrl) {
|
|
18
|
+
const key = cacheKey(lat, lng);
|
|
19
|
+
if (geocodeCache.has(key))
|
|
20
|
+
return geocodeCache.get(key);
|
|
21
|
+
try {
|
|
22
|
+
const url = `${nominatimUrl}/reverse?format=jsonv2&lat=${lat}&lon=${lng}`;
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
headers: { 'User-Agent': 'openclaw-projects/1.0' },
|
|
25
|
+
signal: AbortSignal.timeout(5000),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
console.warn(`[Nominatim] Reverse geocode failed: HTTP ${response.status} for (${lat}, ${lng})`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const data = (await response.json());
|
|
32
|
+
const result = {
|
|
33
|
+
address: data.display_name ?? '',
|
|
34
|
+
placeLabel: data.name || data.address?.suburb || data.address?.city || '',
|
|
35
|
+
};
|
|
36
|
+
if (geocodeCache.size >= MAX_CACHE_SIZE) {
|
|
37
|
+
const oldest = geocodeCache.keys().next().value;
|
|
38
|
+
if (oldest !== undefined)
|
|
39
|
+
geocodeCache.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
geocodeCache.set(key, result);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
46
|
+
console.warn(`[Nominatim] Reverse geocode error for (${lat}, ${lng}): ${msg}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Clears the geocode cache (for testing).
|
|
52
|
+
*/
|
|
53
|
+
export function clearGeocodeCache() {
|
|
54
|
+
geocodeCache.clear();
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=nominatim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nominatim.js","sourceRoot":"","sources":["../../src/utils/nominatim.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,MAAM,YAAY,GAAG,IAAI,GAAG,EAA4B,CAAC;AACzD,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B;;GAEG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,GAAW;IACxC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,GAAW,EACX,YAAoB;IAEpB,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC/B,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;IAEzD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,GAAG,YAAY,8BAA8B,GAAG,QAAQ,GAAG,EAAE,CAAC;QAC1E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,OAAO,EAAE,EAAE,YAAY,EAAE,uBAAuB,EAAE;YAClD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,4CAA4C,QAAQ,CAAC,MAAM,SAAS,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC;YACjG,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAUlC,CAAC;QAEF,MAAM,MAAM,GAAqB;YAC/B,OAAO,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE;YAChC,UAAU,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE;SAC1E,CAAC;QAEF,IAAI,YAAY,CAAC,IAAI,IAAI,cAAc,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAChD,IAAI,MAAM,KAAK,SAAS;gBAAE,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACxD,CAAC;QACD,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO,CAAC,IAAI,CAAC,0CAA0C,GAAG,KAAK,GAAG,MAAM,GAAG,EAAE,CAAC,CAAC;QAC/E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,YAAY,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { type MessageChannel } from './spam-filter.js';
|
|
16
|
+
/** Trust levels for sender classification */
|
|
17
|
+
export type SenderTrust = 'trusted' | 'known' | 'unknown' | 'blocked';
|
|
18
|
+
/** Configuration for the rate limiter */
|
|
19
|
+
export interface RateLimiterConfig {
|
|
20
|
+
/** Max messages per window for trusted senders (active thread, replied) */
|
|
21
|
+
trustedSenderLimit: number;
|
|
22
|
+
/** Max messages per window for known contacts */
|
|
23
|
+
knownSenderLimit: number;
|
|
24
|
+
/** Max messages per window for unknown senders */
|
|
25
|
+
unknownSenderLimit: number;
|
|
26
|
+
/** Global max messages per window for a single recipient */
|
|
27
|
+
recipientGlobalLimit: number;
|
|
28
|
+
/** Sliding window duration in milliseconds */
|
|
29
|
+
windowMs: number;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum number of tracked sender entries before forced eviction.
|
|
32
|
+
* Prevents unbounded memory growth if traffic burst creates many keys
|
|
33
|
+
* and then stops (entries would persist until the next cleanup cycle).
|
|
34
|
+
* When exceeded, the oldest entries are evicted during the next check.
|
|
35
|
+
*/
|
|
36
|
+
maxSenderEntries?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Result of a rate limit check */
|
|
39
|
+
export interface RateLimitResult {
|
|
40
|
+
/** Whether the message is allowed */
|
|
41
|
+
allowed: boolean;
|
|
42
|
+
/** Human-readable reason if denied, null if allowed */
|
|
43
|
+
reason: string | null;
|
|
44
|
+
/** Messages remaining in current window */
|
|
45
|
+
remaining: number;
|
|
46
|
+
/** The limit that applies to this sender */
|
|
47
|
+
limit: number;
|
|
48
|
+
/** Milliseconds until the earliest entry expires (for retry-after) */
|
|
49
|
+
retryAfterMs: number | null;
|
|
50
|
+
}
|
|
51
|
+
/** Statistics about the rate limiter state */
|
|
52
|
+
export interface RateLimiterStats {
|
|
53
|
+
/** Number of currently tracked senders */
|
|
54
|
+
activeSenders: number;
|
|
55
|
+
/** Number of currently tracked recipients */
|
|
56
|
+
activeRecipients: number;
|
|
57
|
+
}
|
|
58
|
+
/** Rate limiter instance */
|
|
59
|
+
export interface RateLimiter {
|
|
60
|
+
/**
|
|
61
|
+
* Check if a message is allowed and record it if so.
|
|
62
|
+
* Sender and recipient are normalized for consistent matching
|
|
63
|
+
* (phone number formatting, email alias stripping).
|
|
64
|
+
*/
|
|
65
|
+
check(sender: string, recipient: string, trust: SenderTrust, channel?: MessageChannel): RateLimitResult;
|
|
66
|
+
/** Get current stats about the limiter */
|
|
67
|
+
getStats(): RateLimiterStats;
|
|
68
|
+
}
|
|
69
|
+
/** Default rate limiter configuration */
|
|
70
|
+
export declare const DEFAULT_RATE_LIMITER_CONFIG: RateLimiterConfig;
|
|
71
|
+
/**
|
|
72
|
+
* Create a rate limiter instance.
|
|
73
|
+
*
|
|
74
|
+
* Uses a sliding window approach: each message records a timestamp,
|
|
75
|
+
* and only timestamps within the current window are counted.
|
|
76
|
+
*
|
|
77
|
+
* @param config - Rate limiter configuration (uses defaults if omitted)
|
|
78
|
+
* @returns RateLimiter instance
|
|
79
|
+
*/
|
|
80
|
+
export declare function createRateLimiter(config?: RateLimiterConfig): RateLimiter;
|
|
81
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|