@corsenai/corsen-context 1.2.0
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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/index.d.mts +797 -0
- package/dist/index.d.ts +797 -0
- package/dist/index.js +1880 -0
- package/dist/index.mjs +1792 -0
- package/package.json +83 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface SitemapEntry {
|
|
5
|
+
url: string;
|
|
6
|
+
lastmod?: string;
|
|
7
|
+
changefreq?: string;
|
|
8
|
+
priority?: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
}
|
|
11
|
+
interface PageContent {
|
|
12
|
+
url: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
markdown: string;
|
|
16
|
+
lastModified?: string;
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
interface SearchResult {
|
|
20
|
+
url: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
snippet: string;
|
|
24
|
+
score: number;
|
|
25
|
+
}
|
|
26
|
+
interface ContentListItem {
|
|
27
|
+
url: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
type: string;
|
|
31
|
+
lastModified?: string;
|
|
32
|
+
}
|
|
33
|
+
interface ContentList {
|
|
34
|
+
items: ContentListItem[];
|
|
35
|
+
total: number;
|
|
36
|
+
page: number;
|
|
37
|
+
limit: number;
|
|
38
|
+
hasMore: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface JSONRPCRequest {
|
|
41
|
+
jsonrpc: '2.0';
|
|
42
|
+
method: string;
|
|
43
|
+
params?: Record<string, unknown>;
|
|
44
|
+
id?: string | number | null;
|
|
45
|
+
}
|
|
46
|
+
interface JSONRPCResponse {
|
|
47
|
+
jsonrpc: '2.0';
|
|
48
|
+
result?: unknown;
|
|
49
|
+
error?: JSONRPCError;
|
|
50
|
+
id: string | number | null;
|
|
51
|
+
}
|
|
52
|
+
interface JSONRPCError {
|
|
53
|
+
code: number;
|
|
54
|
+
message: string;
|
|
55
|
+
data?: unknown;
|
|
56
|
+
}
|
|
57
|
+
interface MCPToolDefinition {
|
|
58
|
+
name: string;
|
|
59
|
+
description: string;
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object';
|
|
62
|
+
properties: Record<string, unknown>;
|
|
63
|
+
required?: string[];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
interface MCPResourceDefinition {
|
|
67
|
+
uri: string;
|
|
68
|
+
name: string;
|
|
69
|
+
description: string;
|
|
70
|
+
mimeType: string;
|
|
71
|
+
}
|
|
72
|
+
interface MCPCapabilities {
|
|
73
|
+
tools: MCPToolDefinition[];
|
|
74
|
+
resources: MCPResourceDefinition[];
|
|
75
|
+
}
|
|
76
|
+
interface ContentProvider {
|
|
77
|
+
getPages(): Promise<ContentListItem[]>;
|
|
78
|
+
getPageContent(url: string): Promise<PageContent | null>;
|
|
79
|
+
searchContent(query: string, limit: number): Promise<SearchResult[]>;
|
|
80
|
+
}
|
|
81
|
+
interface CacheDriver {
|
|
82
|
+
get<T>(key: string): Promise<T | null>;
|
|
83
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
84
|
+
delete(key: string): Promise<void>;
|
|
85
|
+
clear(): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
interface RateLimitResult {
|
|
88
|
+
allowed: boolean;
|
|
89
|
+
remaining: number;
|
|
90
|
+
resetAt: number;
|
|
91
|
+
retryAfter?: number;
|
|
92
|
+
}
|
|
93
|
+
interface RateLimitStore {
|
|
94
|
+
getTimestamps(key: string, windowStart: number): Promise<number[]>;
|
|
95
|
+
addTimestamp(key: string, timestamp: number): Promise<void>;
|
|
96
|
+
cleanup(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Optional atomic prune + record + count. When present, the RateLimiter uses
|
|
99
|
+
* this instead of the getTimestamps()/addTimestamp() pair to avoid the
|
|
100
|
+
* check-then-add race. Returns counts that INCLUDE the current request.
|
|
101
|
+
*/
|
|
102
|
+
hit?(key: string, windowStart: number, burstWindowStart: number, now: number): Promise<{
|
|
103
|
+
windowCount: number;
|
|
104
|
+
burstCount: number;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
interface RedisClient {
|
|
108
|
+
get(key: string): Promise<string | null>;
|
|
109
|
+
set(key: string, value: string, options?: {
|
|
110
|
+
ex?: number;
|
|
111
|
+
} | 'EX', ttlSeconds?: number): Promise<unknown>;
|
|
112
|
+
del(...keys: string[]): Promise<number>;
|
|
113
|
+
incr(key: string): Promise<number>;
|
|
114
|
+
expire(key: string, seconds: number): Promise<number | boolean>;
|
|
115
|
+
zadd(key: string, score: number, member: string): Promise<number>;
|
|
116
|
+
zremrangebyscore(key: string, min: number | string, max: number | string): Promise<number>;
|
|
117
|
+
zcard(key: string): Promise<number>;
|
|
118
|
+
zrangebyscore(key: string, min: number | string, max: number | string): Promise<string[]>;
|
|
119
|
+
}
|
|
120
|
+
interface ApiKeyRecord {
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
keyHash: string;
|
|
124
|
+
keySalt: string;
|
|
125
|
+
scopes: string[];
|
|
126
|
+
quotaPerDay: number;
|
|
127
|
+
requestsToday: number;
|
|
128
|
+
quotaResetAt: string;
|
|
129
|
+
createdAt: string;
|
|
130
|
+
lastUsedAt: string | null;
|
|
131
|
+
expiresAt: string | null;
|
|
132
|
+
revoked: boolean;
|
|
133
|
+
}
|
|
134
|
+
declare const CREDIT_LINE = "Powered by Corsen Context \u2022 Built by Corsen AI \u2022 github.com/CorsenAI/corsen-context";
|
|
135
|
+
declare const JSONRPC_ERRORS: {
|
|
136
|
+
readonly PARSE_ERROR: {
|
|
137
|
+
readonly code: -32700;
|
|
138
|
+
readonly message: "Parse error";
|
|
139
|
+
};
|
|
140
|
+
readonly INVALID_REQUEST: {
|
|
141
|
+
readonly code: -32600;
|
|
142
|
+
readonly message: "Invalid Request";
|
|
143
|
+
};
|
|
144
|
+
readonly METHOD_NOT_FOUND: {
|
|
145
|
+
readonly code: -32601;
|
|
146
|
+
readonly message: "Method not found";
|
|
147
|
+
};
|
|
148
|
+
readonly INVALID_PARAMS: {
|
|
149
|
+
readonly code: -32602;
|
|
150
|
+
readonly message: "Invalid params";
|
|
151
|
+
};
|
|
152
|
+
readonly INTERNAL_ERROR: {
|
|
153
|
+
readonly code: -32603;
|
|
154
|
+
readonly message: "Internal error";
|
|
155
|
+
};
|
|
156
|
+
readonly UNAUTHORIZED: {
|
|
157
|
+
readonly code: -32000;
|
|
158
|
+
readonly message: "Unauthorized";
|
|
159
|
+
};
|
|
160
|
+
readonly RATE_LIMITED: {
|
|
161
|
+
readonly code: -32000;
|
|
162
|
+
readonly message: "Rate limit exceeded";
|
|
163
|
+
};
|
|
164
|
+
readonly RESOURCE_NOT_FOUND: {
|
|
165
|
+
readonly code: -32002;
|
|
166
|
+
readonly message: "Resource not found";
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
declare const SECURITY_HEADERS: Record<string, string>;
|
|
170
|
+
|
|
171
|
+
declare const corsenContextConfigSchema: z.ZodObject<{
|
|
172
|
+
siteUrl: z.ZodString;
|
|
173
|
+
siteName: z.ZodOptional<z.ZodString>;
|
|
174
|
+
description: z.ZodOptional<z.ZodString>;
|
|
175
|
+
content: z.ZodDefault<z.ZodObject<{
|
|
176
|
+
postTypes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
177
|
+
excludePaths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
178
|
+
maxPages: z.ZodDefault<z.ZodNumber>;
|
|
179
|
+
}, "strip", z.ZodTypeAny, {
|
|
180
|
+
postTypes: string[];
|
|
181
|
+
excludePaths: string[];
|
|
182
|
+
maxPages: number;
|
|
183
|
+
}, {
|
|
184
|
+
postTypes?: string[] | undefined;
|
|
185
|
+
excludePaths?: string[] | undefined;
|
|
186
|
+
maxPages?: number | undefined;
|
|
187
|
+
}>>;
|
|
188
|
+
mcp: z.ZodDefault<z.ZodObject<{
|
|
189
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
190
|
+
endpoint: z.ZodDefault<z.ZodString>;
|
|
191
|
+
tools: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
192
|
+
}, "strip", z.ZodTypeAny, {
|
|
193
|
+
enabled: boolean;
|
|
194
|
+
endpoint: string;
|
|
195
|
+
tools: string[];
|
|
196
|
+
}, {
|
|
197
|
+
enabled?: boolean | undefined;
|
|
198
|
+
endpoint?: string | undefined;
|
|
199
|
+
tools?: string[] | undefined;
|
|
200
|
+
}>>;
|
|
201
|
+
static: z.ZodDefault<z.ZodObject<{
|
|
202
|
+
generateLlmsTxt: z.ZodDefault<z.ZodBoolean>;
|
|
203
|
+
includeFullContent: z.ZodDefault<z.ZodBoolean>;
|
|
204
|
+
}, "strip", z.ZodTypeAny, {
|
|
205
|
+
generateLlmsTxt: boolean;
|
|
206
|
+
includeFullContent: boolean;
|
|
207
|
+
}, {
|
|
208
|
+
generateLlmsTxt?: boolean | undefined;
|
|
209
|
+
includeFullContent?: boolean | undefined;
|
|
210
|
+
}>>;
|
|
211
|
+
security: z.ZodDefault<z.ZodObject<{
|
|
212
|
+
rateLimit: z.ZodDefault<z.ZodNumber>;
|
|
213
|
+
burstLimit: z.ZodDefault<z.ZodNumber>;
|
|
214
|
+
allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
215
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
216
|
+
trustProxy: z.ZodDefault<z.ZodBoolean>;
|
|
217
|
+
exposeVersion: z.ZodDefault<z.ZodBoolean>;
|
|
218
|
+
}, "strip", z.ZodTypeAny, {
|
|
219
|
+
rateLimit: number;
|
|
220
|
+
burstLimit: number;
|
|
221
|
+
allowedOrigins: string[];
|
|
222
|
+
trustProxy: boolean;
|
|
223
|
+
exposeVersion: boolean;
|
|
224
|
+
apiKey?: string | undefined;
|
|
225
|
+
}, {
|
|
226
|
+
rateLimit?: number | undefined;
|
|
227
|
+
burstLimit?: number | undefined;
|
|
228
|
+
allowedOrigins?: string[] | undefined;
|
|
229
|
+
apiKey?: string | undefined;
|
|
230
|
+
trustProxy?: boolean | undefined;
|
|
231
|
+
exposeVersion?: boolean | undefined;
|
|
232
|
+
}>>;
|
|
233
|
+
cache: z.ZodDefault<z.ZodObject<{
|
|
234
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
235
|
+
ttl: z.ZodDefault<z.ZodNumber>;
|
|
236
|
+
driver: z.ZodDefault<z.ZodEnum<["memory", "redis"]>>;
|
|
237
|
+
}, "strip", z.ZodTypeAny, {
|
|
238
|
+
enabled: boolean;
|
|
239
|
+
ttl: number;
|
|
240
|
+
driver: "memory" | "redis";
|
|
241
|
+
}, {
|
|
242
|
+
enabled?: boolean | undefined;
|
|
243
|
+
ttl?: number | undefined;
|
|
244
|
+
driver?: "memory" | "redis" | undefined;
|
|
245
|
+
}>>;
|
|
246
|
+
credit: z.ZodDefault<z.ZodBoolean>;
|
|
247
|
+
}, "strip", z.ZodTypeAny, {
|
|
248
|
+
siteUrl: string;
|
|
249
|
+
content: {
|
|
250
|
+
postTypes: string[];
|
|
251
|
+
excludePaths: string[];
|
|
252
|
+
maxPages: number;
|
|
253
|
+
};
|
|
254
|
+
mcp: {
|
|
255
|
+
enabled: boolean;
|
|
256
|
+
endpoint: string;
|
|
257
|
+
tools: string[];
|
|
258
|
+
};
|
|
259
|
+
static: {
|
|
260
|
+
generateLlmsTxt: boolean;
|
|
261
|
+
includeFullContent: boolean;
|
|
262
|
+
};
|
|
263
|
+
security: {
|
|
264
|
+
rateLimit: number;
|
|
265
|
+
burstLimit: number;
|
|
266
|
+
allowedOrigins: string[];
|
|
267
|
+
trustProxy: boolean;
|
|
268
|
+
exposeVersion: boolean;
|
|
269
|
+
apiKey?: string | undefined;
|
|
270
|
+
};
|
|
271
|
+
cache: {
|
|
272
|
+
enabled: boolean;
|
|
273
|
+
ttl: number;
|
|
274
|
+
driver: "memory" | "redis";
|
|
275
|
+
};
|
|
276
|
+
credit: boolean;
|
|
277
|
+
siteName?: string | undefined;
|
|
278
|
+
description?: string | undefined;
|
|
279
|
+
}, {
|
|
280
|
+
siteUrl: string;
|
|
281
|
+
siteName?: string | undefined;
|
|
282
|
+
description?: string | undefined;
|
|
283
|
+
content?: {
|
|
284
|
+
postTypes?: string[] | undefined;
|
|
285
|
+
excludePaths?: string[] | undefined;
|
|
286
|
+
maxPages?: number | undefined;
|
|
287
|
+
} | undefined;
|
|
288
|
+
mcp?: {
|
|
289
|
+
enabled?: boolean | undefined;
|
|
290
|
+
endpoint?: string | undefined;
|
|
291
|
+
tools?: string[] | undefined;
|
|
292
|
+
} | undefined;
|
|
293
|
+
static?: {
|
|
294
|
+
generateLlmsTxt?: boolean | undefined;
|
|
295
|
+
includeFullContent?: boolean | undefined;
|
|
296
|
+
} | undefined;
|
|
297
|
+
security?: {
|
|
298
|
+
rateLimit?: number | undefined;
|
|
299
|
+
burstLimit?: number | undefined;
|
|
300
|
+
allowedOrigins?: string[] | undefined;
|
|
301
|
+
apiKey?: string | undefined;
|
|
302
|
+
trustProxy?: boolean | undefined;
|
|
303
|
+
exposeVersion?: boolean | undefined;
|
|
304
|
+
} | undefined;
|
|
305
|
+
cache?: {
|
|
306
|
+
enabled?: boolean | undefined;
|
|
307
|
+
ttl?: number | undefined;
|
|
308
|
+
driver?: "memory" | "redis" | undefined;
|
|
309
|
+
} | undefined;
|
|
310
|
+
credit?: boolean | undefined;
|
|
311
|
+
}>;
|
|
312
|
+
type CorsenContextConfig = z.input<typeof corsenContextConfigSchema>;
|
|
313
|
+
type ResolvedConfig = z.output<typeof corsenContextConfigSchema>;
|
|
314
|
+
declare function resolveConfig(input: CorsenContextConfig): ResolvedConfig;
|
|
315
|
+
|
|
316
|
+
/** Current API version */
|
|
317
|
+
declare const API_VERSION = "v1";
|
|
318
|
+
/** Maximum JSON-RPC request body size in bytes (100 KB) */
|
|
319
|
+
declare const MAX_BODY_SIZE: number;
|
|
320
|
+
/** Maximum JSON nesting depth */
|
|
321
|
+
declare const MAX_JSON_DEPTH = 10;
|
|
322
|
+
/** Request timeout in milliseconds */
|
|
323
|
+
declare const REQUEST_TIMEOUT_MS = 8000;
|
|
324
|
+
declare class MCPServer {
|
|
325
|
+
private config;
|
|
326
|
+
private provider;
|
|
327
|
+
private rateLimiter;
|
|
328
|
+
private cache;
|
|
329
|
+
private log;
|
|
330
|
+
constructor(config: ResolvedConfig, provider: ContentProvider, options?: {
|
|
331
|
+
cache?: CacheDriver;
|
|
332
|
+
rateLimitStore?: RateLimitStore;
|
|
333
|
+
logger?: pino.Logger;
|
|
334
|
+
});
|
|
335
|
+
getSecurityHeaders(): Record<string, string>;
|
|
336
|
+
getCorsHeaders(origin?: string): Record<string, string>;
|
|
337
|
+
checkRateLimit(clientIp: string, apiKey?: string): Promise<{
|
|
338
|
+
allowed: boolean;
|
|
339
|
+
headers: Record<string, string>;
|
|
340
|
+
}>;
|
|
341
|
+
checkAuth(apiKeyHeader?: string): boolean;
|
|
342
|
+
handleRequest(body: unknown, clientIp?: string, apiKey?: string, options?: {
|
|
343
|
+
skipRateLimit?: boolean;
|
|
344
|
+
}): Promise<JSONRPCResponse | null>;
|
|
345
|
+
private dispatch;
|
|
346
|
+
private handleInitialize;
|
|
347
|
+
private handleListTools;
|
|
348
|
+
private handleCallTool;
|
|
349
|
+
/** Page size for resources/list cursor pagination. */
|
|
350
|
+
private static readonly RESOURCES_PAGE_SIZE;
|
|
351
|
+
private handleListResources;
|
|
352
|
+
private decodeCursor;
|
|
353
|
+
private handleReadResource;
|
|
354
|
+
private get cacheEnabled();
|
|
355
|
+
private cacheGet;
|
|
356
|
+
private cacheSet;
|
|
357
|
+
/**
|
|
358
|
+
* Drop the cached body for a single page URL. Call this from your CMS's
|
|
359
|
+
* publish/update/delete hooks so edits and unpublishes propagate before the
|
|
360
|
+
* TTL expires (otherwise stale content can be served for up to cache.ttl).
|
|
361
|
+
*/
|
|
362
|
+
invalidatePage(url: string): Promise<void>;
|
|
363
|
+
/**
|
|
364
|
+
* Clear all cached MCP responses (search, page, list, sitemap). Call after
|
|
365
|
+
* bulk content changes. No-op for cache drivers without prefix enumeration
|
|
366
|
+
* (see RedisCache.clear notes).
|
|
367
|
+
*/
|
|
368
|
+
clearCache(): Promise<void>;
|
|
369
|
+
searchSite(query: string, limit?: number): Promise<{} | undefined>;
|
|
370
|
+
getPageContent(uri: string): Promise<{} | null | undefined>;
|
|
371
|
+
listContent(type: string, page?: number, limit?: number): Promise<{} | undefined>;
|
|
372
|
+
getSitemap(): Promise<{} | undefined>;
|
|
373
|
+
getToolDefinitions(): MCPToolDefinition[];
|
|
374
|
+
getCapabilities(): MCPCapabilities;
|
|
375
|
+
private successResponse;
|
|
376
|
+
private errorResponse;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Single source of truth for version strings.
|
|
381
|
+
*
|
|
382
|
+
* Keep CORSEN_CONTEXT_VERSION in sync with each package.json `version` on release
|
|
383
|
+
* (the release tooling does this). serverInfo and the CLI both read from here so
|
|
384
|
+
* the version can never silently drift between what is published and what is
|
|
385
|
+
* reported over MCP.
|
|
386
|
+
*/
|
|
387
|
+
/** Corsen Context release version. */
|
|
388
|
+
declare const CORSEN_CONTEXT_VERSION = "1.2.0";
|
|
389
|
+
/** MCP protocol version implemented by this server. */
|
|
390
|
+
declare const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
391
|
+
|
|
392
|
+
/** Structured logger type used across Corsen Context (Pino). */
|
|
393
|
+
type Logger = pino.Logger;
|
|
394
|
+
type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
395
|
+
interface LoggerOptions {
|
|
396
|
+
level?: LogLevel;
|
|
397
|
+
name?: string;
|
|
398
|
+
pretty?: boolean;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Create a structured logger for Corsen Context.
|
|
402
|
+
*
|
|
403
|
+
* Uses Pino for high-performance, JSON-based structured logging.
|
|
404
|
+
* Logs include: timestamp, level, module, and message + data fields.
|
|
405
|
+
*
|
|
406
|
+
* In production, pipe through `pino-pretty` for human-readable output:
|
|
407
|
+
* node server.js | npx pino-pretty
|
|
408
|
+
*/
|
|
409
|
+
declare function createLogger(options?: LoggerOptions): pino.Logger;
|
|
410
|
+
declare function getLogger(): pino.Logger;
|
|
411
|
+
declare function setLogger(logger: pino.Logger): void;
|
|
412
|
+
declare function securityLogger(parent?: pino.Logger): pino.Logger;
|
|
413
|
+
declare function mcpLogger(parent?: pino.Logger): pino.Logger;
|
|
414
|
+
|
|
415
|
+
/** A page for the in-memory provider — a PageContent plus an optional content type. */
|
|
416
|
+
interface InMemoryPage extends PageContent {
|
|
417
|
+
type?: string;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Batteries-included provider backed by an in-memory array of pages.
|
|
421
|
+
*
|
|
422
|
+
* Ideal for static sites, markdown-file blogs, or tests: pass your pages and
|
|
423
|
+
* you get getPages/getPageContent plus a simple case-insensitive full-text
|
|
424
|
+
* search for free — no need to hand-write the ContentProvider interface.
|
|
425
|
+
*/
|
|
426
|
+
declare function createInMemoryProvider(pages: InMemoryPage[]): ContentProvider;
|
|
427
|
+
/**
|
|
428
|
+
* Provider that fetches a live site's pages via its sitemap and converts each
|
|
429
|
+
* page to clean markdown on demand. SSRF-safe (uses safeFetch + isPrivateUrl).
|
|
430
|
+
* Search is unsupported (returns []) since there is no index to query.
|
|
431
|
+
*/
|
|
432
|
+
declare function createSitemapProvider(siteUrl: string, options?: {
|
|
433
|
+
maxPages?: number;
|
|
434
|
+
}): ContentProvider;
|
|
435
|
+
|
|
436
|
+
/** Minimal shape needed to build discovery artifacts. */
|
|
437
|
+
interface DiscoveryConfig {
|
|
438
|
+
siteUrl: string;
|
|
439
|
+
/** MCP endpoint path or absolute URL. Defaults to `/v1/mcp`. */
|
|
440
|
+
mcpEndpoint?: string;
|
|
441
|
+
/** Optional sitemap URL to advertise in robots.txt. */
|
|
442
|
+
sitemapUrl?: string;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Build robots.txt lines that advertise the MCP endpoint (and sitemap) so AI
|
|
446
|
+
* agents can discover the site. Append these to your existing robots.txt.
|
|
447
|
+
*/
|
|
448
|
+
declare function generateRobotsTxt(config: DiscoveryConfig): string;
|
|
449
|
+
/**
|
|
450
|
+
* Build the JSON document to serve at /.well-known/mcp — the standard MCP
|
|
451
|
+
* discovery endpoint.
|
|
452
|
+
*/
|
|
453
|
+
declare function generateWellKnownMcp(config: DiscoveryConfig): {
|
|
454
|
+
mcpEndpoint: string;
|
|
455
|
+
protocolVersion: string;
|
|
456
|
+
transport: string;
|
|
457
|
+
};
|
|
458
|
+
/** Build the `<link rel="mcp">` tag for the HTML head. */
|
|
459
|
+
declare function mcpLinkTag(config: DiscoveryConfig): string;
|
|
460
|
+
|
|
461
|
+
declare function generateLlmsTxt(config: ResolvedConfig, provider: ContentProvider): Promise<string>;
|
|
462
|
+
declare function generateLlmsFullTxt(config: ResolvedConfig, provider: ContentProvider): Promise<string>;
|
|
463
|
+
|
|
464
|
+
declare function parseSitemap(sitemapUrl: string, maxPages?: number): Promise<SitemapEntry[]>;
|
|
465
|
+
declare function discoverSitemap(siteUrl: string): Promise<string | null>;
|
|
466
|
+
|
|
467
|
+
declare function htmlToMarkdown(html: string): string;
|
|
468
|
+
declare function extractMetadata(html: string): Record<string, string>;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* In-memory cache with TTL expiry, max size limit, and LRU eviction.
|
|
472
|
+
*
|
|
473
|
+
* - Entries expire after their TTL
|
|
474
|
+
* - When maxEntries is reached, the least-recently-accessed entry is evicted
|
|
475
|
+
* - Automatic periodic cleanup of expired entries
|
|
476
|
+
*
|
|
477
|
+
* For production multi-instance deployments, use RedisCache instead.
|
|
478
|
+
*/
|
|
479
|
+
declare class MemoryCache implements CacheDriver {
|
|
480
|
+
private store;
|
|
481
|
+
private maxEntries;
|
|
482
|
+
private cleanupTimer;
|
|
483
|
+
constructor(options?: {
|
|
484
|
+
maxEntries?: number;
|
|
485
|
+
autoCleanup?: boolean;
|
|
486
|
+
});
|
|
487
|
+
get<T>(key: string): Promise<T | null>;
|
|
488
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
489
|
+
delete(key: string): Promise<void>;
|
|
490
|
+
clear(): Promise<void>;
|
|
491
|
+
/** Current number of entries */
|
|
492
|
+
get size(): number;
|
|
493
|
+
/** Stop automatic cleanup (for graceful shutdown or testing) */
|
|
494
|
+
destroy(): void;
|
|
495
|
+
/** Remove all expired entries */
|
|
496
|
+
cleanup(): void;
|
|
497
|
+
/** Evict the least-recently-accessed entry */
|
|
498
|
+
private evictLRU;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Redis-backed cache driver for distributed / multi-instance deployments.
|
|
503
|
+
* Compatible with ioredis, @upstash/redis, or any client implementing RedisClient.
|
|
504
|
+
*
|
|
505
|
+
* Usage:
|
|
506
|
+
* import Redis from 'ioredis';
|
|
507
|
+
* const redis = new Redis();
|
|
508
|
+
* const cache = new RedisCache(redis);
|
|
509
|
+
* const ctx = new CorsenContext(config, provider, cache);
|
|
510
|
+
*/
|
|
511
|
+
declare class RedisCache implements CacheDriver {
|
|
512
|
+
private redis;
|
|
513
|
+
private prefix;
|
|
514
|
+
constructor(redis: RedisClient, options?: {
|
|
515
|
+
prefix?: string;
|
|
516
|
+
});
|
|
517
|
+
get<T>(key: string): Promise<T | null>;
|
|
518
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
519
|
+
delete(key: string): Promise<void>;
|
|
520
|
+
clear(): Promise<void>;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Redis-backed rate limit store using Sorted Sets.
|
|
525
|
+
*
|
|
526
|
+
* Each request is stored as a sorted-set member with score = timestamp; the
|
|
527
|
+
* window is pruned with ZREMRANGEBYSCORE and counted with ZCARD/ZRANGEBYSCORE.
|
|
528
|
+
* Every individual command is atomic, but the prune → add → count sequence is
|
|
529
|
+
* not wrapped in a single transaction, so under extreme concurrency the count
|
|
530
|
+
* can momentarily overshoot the limit by roughly the in-flight request count.
|
|
531
|
+
* The `hit()` path adds first and counts after (fail-safe: a rejected request
|
|
532
|
+
* still occupies a slot), which keeps the overshoot small and bounded. For a
|
|
533
|
+
* strict guarantee, back this with a client exposing an atomic script (Lua).
|
|
534
|
+
*
|
|
535
|
+
* Compatible with ioredis, @upstash/redis, or any client implementing RedisClient.
|
|
536
|
+
*/
|
|
537
|
+
declare class RedisRateLimitStore implements RateLimitStore {
|
|
538
|
+
private redis;
|
|
539
|
+
private prefix;
|
|
540
|
+
private windowMs;
|
|
541
|
+
constructor(redis: RedisClient, options?: {
|
|
542
|
+
prefix?: string;
|
|
543
|
+
windowMs?: number;
|
|
544
|
+
});
|
|
545
|
+
getTimestamps(key: string, windowStart: number): Promise<number[]>;
|
|
546
|
+
addTimestamp(key: string, timestamp: number): Promise<void>;
|
|
547
|
+
/**
|
|
548
|
+
* Combined add-then-count. Records the hit first, prunes the window, then
|
|
549
|
+
* returns the window and burst counts (both including the current request).
|
|
550
|
+
* Fewer round-trips than getTimestamps()+addTimestamp() and add-first so a
|
|
551
|
+
* rejected request still counts against the window.
|
|
552
|
+
*/
|
|
553
|
+
hit(key: string, windowStart: number, burstWindowStart: number, now: number): Promise<{
|
|
554
|
+
windowCount: number;
|
|
555
|
+
burstCount: number;
|
|
556
|
+
}>;
|
|
557
|
+
cleanup(): Promise<void>;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check if an IP address is private/internal.
|
|
562
|
+
* Sync function — works on resolved IPs, not hostnames.
|
|
563
|
+
*/
|
|
564
|
+
declare function isPrivateIp(ip: string): boolean;
|
|
565
|
+
/**
|
|
566
|
+
* Check if a URL points to a private/internal address.
|
|
567
|
+
* ASYNC — resolves DNS to prevent DNS rebinding attacks (OWASP SSRF 2026).
|
|
568
|
+
*
|
|
569
|
+
* Flow: parse URL → check hostname literally → resolve DNS → check resolved IPs.
|
|
570
|
+
*/
|
|
571
|
+
declare function isPrivateUrl(url: string): Promise<boolean>;
|
|
572
|
+
/**
|
|
573
|
+
* Fetch a URL with SSRF protection.
|
|
574
|
+
*
|
|
575
|
+
* 1. Parse URL, reject literal localhost / private IPs.
|
|
576
|
+
* 2. Resolve DNS and verify EVERY resolved IP is public (fail-closed).
|
|
577
|
+
* 3. If undici is available, pin the connection to the vetted IP at the socket
|
|
578
|
+
* level (keeping the hostname for TLS/SNI) so a rebind can't redirect the
|
|
579
|
+
* request. Otherwise fetch the original URL — TLS still validates against the
|
|
580
|
+
* real hostname, with a narrow residual rebinding window.
|
|
581
|
+
* 4. Never follow redirects (`redirect: 'error'`).
|
|
582
|
+
*/
|
|
583
|
+
declare function safeFetch(url: string, options?: RequestInit & {
|
|
584
|
+
timeout?: number;
|
|
585
|
+
}): Promise<Response>;
|
|
586
|
+
/**
|
|
587
|
+
* In-memory rate limit store. Default for development / single-instance.
|
|
588
|
+
* For production multi-instance, use RedisRateLimitStore.
|
|
589
|
+
*
|
|
590
|
+
* The store is long-lived (shared across requests), so it self-bounds: expired
|
|
591
|
+
* keys are pruned on a periodic timer and the key count is capped (oldest-first
|
|
592
|
+
* eviction). This prevents an attacker who rotates the rate-limit key — e.g. a
|
|
593
|
+
* fresh Authorization header per request — from growing the Map without bound.
|
|
594
|
+
*/
|
|
595
|
+
declare class MemoryRateLimitStore implements RateLimitStore {
|
|
596
|
+
private store;
|
|
597
|
+
private readonly maxKeys;
|
|
598
|
+
private cleanupTimer;
|
|
599
|
+
constructor(options?: {
|
|
600
|
+
maxKeys?: number;
|
|
601
|
+
autoCleanup?: boolean;
|
|
602
|
+
});
|
|
603
|
+
/** Number of tracked keys. */
|
|
604
|
+
get size(): number;
|
|
605
|
+
/** Stop the cleanup timer (for graceful shutdown or tests). */
|
|
606
|
+
destroy(): void;
|
|
607
|
+
/** Evict the oldest-inserted keys until under the cap (after a prune pass). */
|
|
608
|
+
private enforceCap;
|
|
609
|
+
getTimestamps(key: string, windowStart: number): Promise<number[]>;
|
|
610
|
+
addTimestamp(key: string, timestamp: number): Promise<void>;
|
|
611
|
+
/**
|
|
612
|
+
* Atomic prune + record + count. Runs synchronously (no await between the
|
|
613
|
+
* prune and the append), so concurrent requests on the same tick cannot
|
|
614
|
+
* interleave a check between another request's read and write — the TOCTOU
|
|
615
|
+
* race the split getTimestamps()/addTimestamp() path has.
|
|
616
|
+
*/
|
|
617
|
+
hit(key: string, windowStart: number, burstWindowStart: number, now: number): Promise<{
|
|
618
|
+
windowCount: number;
|
|
619
|
+
burstCount: number;
|
|
620
|
+
}>;
|
|
621
|
+
cleanup(): Promise<void>;
|
|
622
|
+
}
|
|
623
|
+
declare class RateLimiter {
|
|
624
|
+
private store;
|
|
625
|
+
private readonly windowMs;
|
|
626
|
+
private readonly maxRequests;
|
|
627
|
+
private readonly burstLimit;
|
|
628
|
+
constructor(maxRequestsPerMinute?: number, burstPerSecond?: number, store?: RateLimitStore);
|
|
629
|
+
check(key: string): Promise<RateLimitResult>;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Extract the real client IP from request headers.
|
|
633
|
+
*
|
|
634
|
+
* Forwarding headers (CF-Connecting-IP, X-Real-IP, X-Forwarded-For) are only
|
|
635
|
+
* honored when `trustProxy` is true — i.e. the server sits behind a reverse
|
|
636
|
+
* proxy that sets them. Otherwise they are attacker-controllable: a client can
|
|
637
|
+
* send a fresh spoofed value per request and land in a new rate-limit bucket
|
|
638
|
+
* every time, defeating the limiter. When untrusted, we key on the socket IP.
|
|
639
|
+
*/
|
|
640
|
+
declare function extractClientIp(headers: Record<string, string | string[] | undefined>, socketIp?: string, trustProxy?: boolean): string;
|
|
641
|
+
/**
|
|
642
|
+
* Build rate limit key. Uses API key if present (more accurate), falls back to IP.
|
|
643
|
+
*
|
|
644
|
+
* The API key is run through a keyed hash (HMAC) purely to derive a stable,
|
|
645
|
+
* non-reversible bucket id — it is a store/log key, not credential storage.
|
|
646
|
+
* The fixed label keeps the id deterministic across instances so a distributed
|
|
647
|
+
* (Redis) limiter shares state for the same key.
|
|
648
|
+
*/
|
|
649
|
+
declare function buildRateLimitKey(clientIp: string, apiKey?: string): string;
|
|
650
|
+
declare const searchParamsSchema: z.ZodObject<{
|
|
651
|
+
query: z.ZodString;
|
|
652
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
653
|
+
}, "strip", z.ZodTypeAny, {
|
|
654
|
+
query: string;
|
|
655
|
+
limit: number;
|
|
656
|
+
}, {
|
|
657
|
+
query: string;
|
|
658
|
+
limit?: number | undefined;
|
|
659
|
+
}>;
|
|
660
|
+
declare const getPageParamsSchema: z.ZodObject<{
|
|
661
|
+
uri: z.ZodString;
|
|
662
|
+
}, "strip", z.ZodTypeAny, {
|
|
663
|
+
uri: string;
|
|
664
|
+
}, {
|
|
665
|
+
uri: string;
|
|
666
|
+
}>;
|
|
667
|
+
declare const listContentParamsSchema: z.ZodObject<{
|
|
668
|
+
type: z.ZodDefault<z.ZodString>;
|
|
669
|
+
page: z.ZodDefault<z.ZodNumber>;
|
|
670
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
671
|
+
}, "strip", z.ZodTypeAny, {
|
|
672
|
+
type: string;
|
|
673
|
+
page: number;
|
|
674
|
+
limit: number;
|
|
675
|
+
}, {
|
|
676
|
+
type?: string | undefined;
|
|
677
|
+
page?: number | undefined;
|
|
678
|
+
limit?: number | undefined;
|
|
679
|
+
}>;
|
|
680
|
+
declare function validateOrigin(origin: string | undefined, allowed: string[]): boolean;
|
|
681
|
+
declare function validateHost(hostHeader: string | undefined, expectedHost: string): boolean;
|
|
682
|
+
/**
|
|
683
|
+
* Hash an API key with SHA-256 + salt for secure storage.
|
|
684
|
+
*/
|
|
685
|
+
declare function hashApiKey(key: string, salt?: string): {
|
|
686
|
+
hash: string;
|
|
687
|
+
salt: string;
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Timing-safe API key validation via the double-HMAC compare pattern.
|
|
691
|
+
*
|
|
692
|
+
* Both keys are HMAC'd with a fresh per-call random key, yielding fixed-length
|
|
693
|
+
* digests that are compared with timingSafeEqual. This leaks neither key length
|
|
694
|
+
* nor content through timing, and (unlike a bare hash) an attacker cannot
|
|
695
|
+
* precompute the digests.
|
|
696
|
+
*/
|
|
697
|
+
declare function validateApiKey(provided: string | undefined, expected: string | undefined): boolean;
|
|
698
|
+
/**
|
|
699
|
+
* Enhanced API key validation with hashed keys, scopes, quotas, expiry.
|
|
700
|
+
* For production multi-key setups.
|
|
701
|
+
*/
|
|
702
|
+
declare class ApiKeyManager {
|
|
703
|
+
private keys;
|
|
704
|
+
/**
|
|
705
|
+
* Register a new API key. Returns the plain key (show to user once).
|
|
706
|
+
*/
|
|
707
|
+
generateKey(options: {
|
|
708
|
+
name: string;
|
|
709
|
+
scopes?: string[];
|
|
710
|
+
quotaPerDay?: number;
|
|
711
|
+
expiresAt?: Date;
|
|
712
|
+
}): {
|
|
713
|
+
plainKey: string;
|
|
714
|
+
record: ApiKeyRecord;
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Validate a provided API key against stored records.
|
|
718
|
+
* Returns the matching record or null.
|
|
719
|
+
*/
|
|
720
|
+
validate(providedKey: string, requiredScope?: string): ApiKeyRecord | null;
|
|
721
|
+
/**
|
|
722
|
+
* Revoke an API key by ID.
|
|
723
|
+
*/
|
|
724
|
+
revoke(keyId: string): boolean;
|
|
725
|
+
/**
|
|
726
|
+
* List all keys (without secrets).
|
|
727
|
+
*/
|
|
728
|
+
listKeys(): Omit<ApiKeyRecord, 'keyHash' | 'keySalt'>[];
|
|
729
|
+
/**
|
|
730
|
+
* Load keys from external storage (e.g., Redis, database).
|
|
731
|
+
*/
|
|
732
|
+
loadKeys(records: ApiKeyRecord[]): void;
|
|
733
|
+
private nextMidnight;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
declare function resolvePublicPageUrl(input: string, config: ResolvedConfig): string | null;
|
|
737
|
+
declare function isPublicListItem(item: ContentListItem, config: ResolvedConfig): boolean;
|
|
738
|
+
declare function filterPublicPages(pages: ContentListItem[], config: ResolvedConfig): ContentListItem[];
|
|
739
|
+
declare function isPublicPageContent(content: PageContent, config: ResolvedConfig): boolean;
|
|
740
|
+
declare function filterPublicSearchResults(results: SearchResult[], config: ResolvedConfig, limit: number): SearchResult[];
|
|
741
|
+
|
|
742
|
+
declare class CorsenContext {
|
|
743
|
+
private config;
|
|
744
|
+
private provider;
|
|
745
|
+
private cache;
|
|
746
|
+
private rateLimitStore;
|
|
747
|
+
constructor(userConfig: CorsenContextConfig, provider: ContentProvider, cache?: CacheDriver, rateLimitStore?: RateLimitStore);
|
|
748
|
+
generateLlmsTxt(): Promise<string>;
|
|
749
|
+
generateLlmsFullTxt(): Promise<string>;
|
|
750
|
+
createMCPServer(options?: {
|
|
751
|
+
rateLimitStore?: RateLimitStore;
|
|
752
|
+
logger?: pino.Logger;
|
|
753
|
+
}): MCPServer;
|
|
754
|
+
/** Drop the cached body for a single page URL (wire to CMS update/delete hooks). */
|
|
755
|
+
invalidatePage(url: string): Promise<void>;
|
|
756
|
+
/** Clear all cached MCP responses. Call after bulk content changes. */
|
|
757
|
+
clearCache(): Promise<void>;
|
|
758
|
+
discoverSitemap(url?: string): Promise<string | null>;
|
|
759
|
+
parseSitemap(sitemapUrl: string): Promise<SitemapEntry[]>;
|
|
760
|
+
convertToMarkdown(html: string): string;
|
|
761
|
+
extractMetadata(html: string): Record<string, string>;
|
|
762
|
+
getConfig(): {
|
|
763
|
+
siteUrl: string;
|
|
764
|
+
content: {
|
|
765
|
+
postTypes: string[];
|
|
766
|
+
excludePaths: string[];
|
|
767
|
+
maxPages: number;
|
|
768
|
+
};
|
|
769
|
+
mcp: {
|
|
770
|
+
enabled: boolean;
|
|
771
|
+
endpoint: string;
|
|
772
|
+
tools: string[];
|
|
773
|
+
};
|
|
774
|
+
static: {
|
|
775
|
+
generateLlmsTxt: boolean;
|
|
776
|
+
includeFullContent: boolean;
|
|
777
|
+
};
|
|
778
|
+
security: {
|
|
779
|
+
rateLimit: number;
|
|
780
|
+
burstLimit: number;
|
|
781
|
+
allowedOrigins: string[];
|
|
782
|
+
trustProxy: boolean;
|
|
783
|
+
exposeVersion: boolean;
|
|
784
|
+
apiKey?: string | undefined;
|
|
785
|
+
};
|
|
786
|
+
cache: {
|
|
787
|
+
enabled: boolean;
|
|
788
|
+
ttl: number;
|
|
789
|
+
driver: "memory" | "redis";
|
|
790
|
+
};
|
|
791
|
+
credit: boolean;
|
|
792
|
+
siteName?: string | undefined;
|
|
793
|
+
description?: string | undefined;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export { API_VERSION, ApiKeyManager, type ApiKeyRecord, CORSEN_CONTEXT_VERSION, CREDIT_LINE, type CacheDriver, type ContentList, type ContentListItem, type ContentProvider, CorsenContext, type CorsenContextConfig, type DiscoveryConfig, type InMemoryPage, type JSONRPCRequest, type JSONRPCResponse, JSONRPC_ERRORS, type LogLevel, type Logger, type LoggerOptions, MAX_BODY_SIZE, MAX_JSON_DEPTH, type MCPCapabilities, type MCPResourceDefinition, MCPServer, type MCPToolDefinition, MCP_PROTOCOL_VERSION, MemoryCache, MemoryRateLimitStore, type PageContent, REQUEST_TIMEOUT_MS, type RateLimitResult, type RateLimitStore, RateLimiter, RedisCache, type RedisClient, RedisRateLimitStore, type ResolvedConfig, SECURITY_HEADERS, type SearchResult, type SitemapEntry, buildRateLimitKey, corsenContextConfigSchema, createInMemoryProvider, createLogger, createSitemapProvider, discoverSitemap, extractClientIp, extractMetadata, filterPublicPages, filterPublicSearchResults, generateLlmsFullTxt, generateLlmsTxt, generateRobotsTxt, generateWellKnownMcp, getLogger, getPageParamsSchema, hashApiKey, htmlToMarkdown, isPrivateIp, isPrivateUrl, isPublicListItem, isPublicPageContent, listContentParamsSchema, mcpLinkTag, mcpLogger, parseSitemap, resolveConfig, resolvePublicPageUrl, safeFetch, searchParamsSchema, securityLogger, setLogger, validateApiKey, validateHost, validateOrigin };
|