webpeel 0.1.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 +415 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +140 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/fetcher.d.ts +32 -0
- package/dist/core/fetcher.d.ts.map +1 -0
- package/dist/core/fetcher.js +479 -0
- package/dist/core/fetcher.js.map +1 -0
- package/dist/core/markdown.d.ts +17 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +143 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/metadata.d.ts +17 -0
- package/dist/core/metadata.d.ts.map +1 -0
- package/dist/core/metadata.js +159 -0
- package/dist/core/metadata.js.map +1 -0
- package/dist/core/strategies.d.ts +30 -0
- package/dist/core/strategies.d.ts.map +1 -0
- package/dist/core/strategies.js +67 -0
- package/dist/core/strategies.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +248 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +89 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/auth-store.d.ts +28 -0
- package/dist/server/auth-store.d.ts.map +1 -0
- package/dist/server/auth-store.js +87 -0
- package/dist/server/auth-store.js.map +1 -0
- package/dist/server/middleware/auth.d.ts +18 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/auth.js +55 -0
- package/dist/server/middleware/auth.js.map +1 -0
- package/dist/server/middleware/rate-limit.d.ts +23 -0
- package/dist/server/middleware/rate-limit.d.ts.map +1 -0
- package/dist/server/middleware/rate-limit.js +85 -0
- package/dist/server/middleware/rate-limit.js.map +1 -0
- package/dist/server/routes/fetch.d.ts +7 -0
- package/dist/server/routes/fetch.d.ts.map +1 -0
- package/dist/server/routes/fetch.js +127 -0
- package/dist/server/routes/fetch.js.map +1 -0
- package/dist/server/routes/health.d.ts +6 -0
- package/dist/server/routes/health.d.ts.map +1 -0
- package/dist/server/routes/health.js +19 -0
- package/dist/server/routes/health.js.map +1 -0
- package/dist/server/routes/search.d.ts +7 -0
- package/dist/server/routes/search.d.ts.map +1 -0
- package/dist/server/routes/search.js +124 -0
- package/dist/server/routes/search.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/llms.txt +60 -0
- package/package.json +80 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for WebPeel
|
|
4
|
+
* Provides webpeel_fetch and webpeel_search tools for Claude Desktop / Cursor
|
|
5
|
+
*/
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { peel } from '../index.js';
|
|
10
|
+
import { fetch as undiciFetch } from 'undici';
|
|
11
|
+
import { load } from 'cheerio';
|
|
12
|
+
const server = new Server({
|
|
13
|
+
name: 'webpeel',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
}, {
|
|
16
|
+
capabilities: {
|
|
17
|
+
tools: {},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Search DuckDuckGo HTML and return structured results
|
|
22
|
+
*/
|
|
23
|
+
async function searchWeb(query, count = 5) {
|
|
24
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
25
|
+
try {
|
|
26
|
+
const response = await undiciFetch(searchUrl, {
|
|
27
|
+
headers: {
|
|
28
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`Search failed: HTTP ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
const html = await response.text();
|
|
35
|
+
const $ = load(html);
|
|
36
|
+
const results = [];
|
|
37
|
+
$('.result').each((_i, elem) => {
|
|
38
|
+
if (results.length >= count)
|
|
39
|
+
return;
|
|
40
|
+
const $result = $(elem);
|
|
41
|
+
let title = $result.find('.result__title').text().trim();
|
|
42
|
+
let url = $result.find('.result__url').attr('href') || '';
|
|
43
|
+
let snippet = $result.find('.result__snippet').text().trim();
|
|
44
|
+
// SECURITY: Validate and sanitize results
|
|
45
|
+
if (!title || !url)
|
|
46
|
+
return;
|
|
47
|
+
// Only allow HTTP/HTTPS URLs
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url);
|
|
50
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Limit text lengths to prevent bloat
|
|
58
|
+
title = title.slice(0, 200);
|
|
59
|
+
snippet = snippet.slice(0, 500);
|
|
60
|
+
results.push({ title, url, snippet });
|
|
61
|
+
});
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const err = error;
|
|
66
|
+
throw new Error(`Search failed: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const tools = [
|
|
70
|
+
{
|
|
71
|
+
name: 'webpeel_fetch',
|
|
72
|
+
description: 'Fetch a URL and return clean, AI-ready markdown content. Handles JavaScript rendering and anti-bot protections automatically. Use this when you need to read the content of a web page.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
url: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'The URL to fetch',
|
|
79
|
+
},
|
|
80
|
+
render: {
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
description: 'Force browser rendering (slower but handles JavaScript-heavy sites)',
|
|
83
|
+
default: false,
|
|
84
|
+
},
|
|
85
|
+
wait: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
description: 'Milliseconds to wait for dynamic content (only used with render=true)',
|
|
88
|
+
default: 0,
|
|
89
|
+
},
|
|
90
|
+
format: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
enum: ['markdown', 'text', 'html'],
|
|
93
|
+
description: 'Output format: markdown (default), text, or html',
|
|
94
|
+
default: 'markdown',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ['url'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'webpeel_search',
|
|
102
|
+
description: 'Search the web using DuckDuckGo and return results with titles, URLs, and snippets. Use this to find relevant web pages before fetching them.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
query: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Search query',
|
|
109
|
+
},
|
|
110
|
+
count: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Number of results to return (1-10)',
|
|
113
|
+
default: 5,
|
|
114
|
+
minimum: 1,
|
|
115
|
+
maximum: 10,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['query'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
123
|
+
tools,
|
|
124
|
+
}));
|
|
125
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
126
|
+
const { name, arguments: args } = request.params;
|
|
127
|
+
try {
|
|
128
|
+
if (name === 'webpeel_fetch') {
|
|
129
|
+
const { url, render, wait, format } = args;
|
|
130
|
+
// SECURITY: Validate input parameters
|
|
131
|
+
if (!url || typeof url !== 'string') {
|
|
132
|
+
throw new Error('Invalid URL parameter');
|
|
133
|
+
}
|
|
134
|
+
if (url.length > 2048) {
|
|
135
|
+
throw new Error('URL too long (max 2048 characters)');
|
|
136
|
+
}
|
|
137
|
+
if (wait !== undefined) {
|
|
138
|
+
if (typeof wait !== 'number' || isNaN(wait) || wait < 0 || wait > 60000) {
|
|
139
|
+
throw new Error('Invalid wait parameter: must be between 0 and 60000ms');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (format !== undefined && !['markdown', 'text', 'html'].includes(format)) {
|
|
143
|
+
throw new Error('Invalid format parameter: must be "markdown", "text", or "html"');
|
|
144
|
+
}
|
|
145
|
+
const options = {
|
|
146
|
+
render: render || false,
|
|
147
|
+
wait: wait || 0,
|
|
148
|
+
format: format || 'markdown',
|
|
149
|
+
};
|
|
150
|
+
// SECURITY: Wrap in timeout (60 seconds max)
|
|
151
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
152
|
+
setTimeout(() => reject(new Error('MCP operation timed out after 60s')), 60000);
|
|
153
|
+
});
|
|
154
|
+
const result = await Promise.race([
|
|
155
|
+
peel(url, options),
|
|
156
|
+
timeoutPromise,
|
|
157
|
+
]);
|
|
158
|
+
// SECURITY: Handle JSON serialization errors
|
|
159
|
+
let resultText;
|
|
160
|
+
try {
|
|
161
|
+
resultText = JSON.stringify(result, null, 2);
|
|
162
|
+
}
|
|
163
|
+
catch (jsonError) {
|
|
164
|
+
resultText = JSON.stringify({
|
|
165
|
+
error: 'serialization_error',
|
|
166
|
+
message: 'Failed to serialize result',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: resultText,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (name === 'webpeel_search') {
|
|
179
|
+
const { query, count } = args;
|
|
180
|
+
// SECURITY: Validate input parameters
|
|
181
|
+
if (!query || typeof query !== 'string') {
|
|
182
|
+
throw new Error('Invalid query parameter');
|
|
183
|
+
}
|
|
184
|
+
if (query.length > 500) {
|
|
185
|
+
throw new Error('Query too long (max 500 characters)');
|
|
186
|
+
}
|
|
187
|
+
if (count !== undefined) {
|
|
188
|
+
if (typeof count !== 'number' || isNaN(count) || count < 1 || count > 10) {
|
|
189
|
+
throw new Error('Invalid count parameter: must be between 1 and 10');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const resultCount = Math.min(Math.max(count || 5, 1), 10);
|
|
193
|
+
// SECURITY: Wrap in timeout (30 seconds max)
|
|
194
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
195
|
+
setTimeout(() => reject(new Error('Search operation timed out after 30s')), 30000);
|
|
196
|
+
});
|
|
197
|
+
const results = await Promise.race([
|
|
198
|
+
searchWeb(query, resultCount),
|
|
199
|
+
timeoutPromise,
|
|
200
|
+
]);
|
|
201
|
+
// SECURITY: Handle JSON serialization errors
|
|
202
|
+
let resultText;
|
|
203
|
+
try {
|
|
204
|
+
resultText = JSON.stringify(results, null, 2);
|
|
205
|
+
}
|
|
206
|
+
catch (jsonError) {
|
|
207
|
+
resultText = JSON.stringify({
|
|
208
|
+
error: 'serialization_error',
|
|
209
|
+
message: 'Failed to serialize results',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: resultText,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const err = error;
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: JSON.stringify({
|
|
230
|
+
error: err.name || 'Error',
|
|
231
|
+
message: err.message || 'Unknown error occurred',
|
|
232
|
+
}, null, 2),
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
async function main() {
|
|
240
|
+
const transport = new StdioServerTransport();
|
|
241
|
+
await server.connect(transport);
|
|
242
|
+
console.error('WebPeel MCP server running on stdio');
|
|
243
|
+
}
|
|
244
|
+
main().catch((error) => {
|
|
245
|
+
console.error('Fatal error:', error);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebPeel API Server
|
|
3
|
+
* Express-based REST API for hosted deployments
|
|
4
|
+
*/
|
|
5
|
+
import { Express } from 'express';
|
|
6
|
+
export interface ServerConfig {
|
|
7
|
+
port?: number;
|
|
8
|
+
corsOrigins?: string[];
|
|
9
|
+
rateLimitWindowMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createApp(config?: ServerConfig): Express;
|
|
12
|
+
export declare function startServer(config?: ServerConfig): void;
|
|
13
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAgB,EAAE,OAAO,EAAmC,MAAM,SAAS,CAAC;AAS5E,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,SAAS,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CA0D5D;AAED,wBAAgB,WAAW,CAAC,MAAM,GAAE,YAAiB,GAAG,IAAI,CA4B3D"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebPeel API Server
|
|
3
|
+
* Express-based REST API for hosted deployments
|
|
4
|
+
*/
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import { InMemoryAuthStore } from './auth-store.js';
|
|
8
|
+
import { createAuthMiddleware } from './middleware/auth.js';
|
|
9
|
+
import { createRateLimitMiddleware, RateLimiter } from './middleware/rate-limit.js';
|
|
10
|
+
import { createHealthRouter } from './routes/health.js';
|
|
11
|
+
import { createFetchRouter } from './routes/fetch.js';
|
|
12
|
+
import { createSearchRouter } from './routes/search.js';
|
|
13
|
+
export function createApp(config = {}) {
|
|
14
|
+
const app = express();
|
|
15
|
+
// Middleware
|
|
16
|
+
// SECURITY: Limit request body size to prevent DoS
|
|
17
|
+
app.use(express.json({ limit: '1mb' }));
|
|
18
|
+
// SECURITY: Restrict CORS - require explicit origin whitelist
|
|
19
|
+
const corsOrigins = config.corsOrigins || [];
|
|
20
|
+
app.use(cors({
|
|
21
|
+
origin: corsOrigins.length > 0 ? corsOrigins : false,
|
|
22
|
+
credentials: true,
|
|
23
|
+
}));
|
|
24
|
+
// Trust proxy (for rate limiting by IP in production)
|
|
25
|
+
app.set('trust proxy', 1);
|
|
26
|
+
// Auth store (in-memory for now, swap to PostgreSQL later)
|
|
27
|
+
const authStore = new InMemoryAuthStore();
|
|
28
|
+
// Rate limiter
|
|
29
|
+
const rateLimiter = new RateLimiter(config.rateLimitWindowMs || 60000);
|
|
30
|
+
// Clean up rate limiter every 5 minutes
|
|
31
|
+
setInterval(() => {
|
|
32
|
+
rateLimiter.cleanup();
|
|
33
|
+
}, 5 * 60 * 1000);
|
|
34
|
+
// Apply auth middleware globally
|
|
35
|
+
app.use(createAuthMiddleware(authStore));
|
|
36
|
+
// Apply rate limiting middleware globally
|
|
37
|
+
app.use(createRateLimitMiddleware(rateLimiter));
|
|
38
|
+
// Routes
|
|
39
|
+
app.use(createHealthRouter());
|
|
40
|
+
app.use(createFetchRouter(authStore));
|
|
41
|
+
app.use(createSearchRouter(authStore));
|
|
42
|
+
// 404 handler
|
|
43
|
+
app.use((req, res) => {
|
|
44
|
+
res.status(404).json({
|
|
45
|
+
error: 'not_found',
|
|
46
|
+
message: `Route not found: ${req.method} ${req.path}`,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// Error handler
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
51
|
+
app.use((err, _req, res, _next) => {
|
|
52
|
+
console.error('Unhandled error:', err);
|
|
53
|
+
res.status(500).json({
|
|
54
|
+
error: 'internal_error',
|
|
55
|
+
message: err.message || 'An unexpected error occurred',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return app;
|
|
59
|
+
}
|
|
60
|
+
export function startServer(config = {}) {
|
|
61
|
+
const app = createApp(config);
|
|
62
|
+
const port = config.port || parseInt(process.env.PORT || '3000', 10);
|
|
63
|
+
const server = app.listen(port, () => {
|
|
64
|
+
console.log(`WebPeel API server listening on port ${port}`);
|
|
65
|
+
console.log(`Health check: http://localhost:${port}/health`);
|
|
66
|
+
console.log(`Fetch: http://localhost:${port}/v1/fetch?url=<url>`);
|
|
67
|
+
console.log(`Search: http://localhost:${port}/v1/search?q=<query>`);
|
|
68
|
+
});
|
|
69
|
+
// Graceful shutdown
|
|
70
|
+
const shutdown = () => {
|
|
71
|
+
console.log('\nShutting down gracefully...');
|
|
72
|
+
server.close(() => {
|
|
73
|
+
console.log('Server closed');
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
// Force shutdown after 10 seconds
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
console.error('Forced shutdown after timeout');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}, 10000);
|
|
81
|
+
};
|
|
82
|
+
process.on('SIGTERM', shutdown);
|
|
83
|
+
process.on('SIGINT', shutdown);
|
|
84
|
+
}
|
|
85
|
+
// Start server if run directly
|
|
86
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
87
|
+
startServer();
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth store abstraction for API key validation and usage tracking
|
|
3
|
+
* Designed to easily swap from in-memory to PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiKeyInfo {
|
|
6
|
+
key: string;
|
|
7
|
+
tier: 'free' | 'starter' | 'pro' | 'enterprise';
|
|
8
|
+
rateLimit: number;
|
|
9
|
+
accountId?: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
}
|
|
12
|
+
export interface AuthStore {
|
|
13
|
+
validateKey(key: string): Promise<ApiKeyInfo | null>;
|
|
14
|
+
trackUsage(key: string, credits: number): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* In-memory auth store for development and self-hosted deployments
|
|
18
|
+
*/
|
|
19
|
+
export declare class InMemoryAuthStore implements AuthStore {
|
|
20
|
+
private keys;
|
|
21
|
+
private usage;
|
|
22
|
+
constructor();
|
|
23
|
+
validateKey(key: string): Promise<ApiKeyInfo | null>;
|
|
24
|
+
trackUsage(key: string, credits: number): Promise<void>;
|
|
25
|
+
addKey(keyInfo: ApiKeyInfo): void;
|
|
26
|
+
getUsage(key: string): number;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=auth-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../src/server/auth-store.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACrD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAwCD;;GAEG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,IAAI,CAAiC;IAC7C,OAAO,CAAC,KAAK,CAA6B;;IAepC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAiBpD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAQjC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAG9B"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth store abstraction for API key validation and usage tracking
|
|
3
|
+
* Designed to easily swap from in-memory to PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
import { timingSafeEqual } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Validate API key format and strength
|
|
8
|
+
* SECURITY: Enforce minimum complexity
|
|
9
|
+
*/
|
|
10
|
+
function validateKeyFormat(key) {
|
|
11
|
+
// Minimum 32 characters
|
|
12
|
+
if (key.length < 32) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// Must contain alphanumeric characters
|
|
16
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Timing-safe key comparison
|
|
23
|
+
* SECURITY: Prevent timing attacks on key validation
|
|
24
|
+
*/
|
|
25
|
+
function timingSafeKeyCompare(a, b) {
|
|
26
|
+
// Ensure equal length for comparison
|
|
27
|
+
if (a.length !== b.length) {
|
|
28
|
+
// Compare against dummy to prevent timing leak
|
|
29
|
+
const dummy = 'x'.repeat(Math.max(a.length, b.length));
|
|
30
|
+
timingSafeEqual(Buffer.from(dummy), Buffer.from(dummy));
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* In-memory auth store for development and self-hosted deployments
|
|
42
|
+
*/
|
|
43
|
+
export class InMemoryAuthStore {
|
|
44
|
+
keys = new Map();
|
|
45
|
+
usage = new Map();
|
|
46
|
+
constructor() {
|
|
47
|
+
// SECURITY: Demo key only in development mode
|
|
48
|
+
// Removed hardcoded demo key - use addKey() or environment variables
|
|
49
|
+
if (process.env.NODE_ENV === 'development' && process.env.DEMO_KEY) {
|
|
50
|
+
this.addKey({
|
|
51
|
+
key: process.env.DEMO_KEY,
|
|
52
|
+
tier: 'pro',
|
|
53
|
+
rateLimit: 300,
|
|
54
|
+
createdAt: new Date(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async validateKey(key) {
|
|
59
|
+
// Basic validation
|
|
60
|
+
if (!key || typeof key !== 'string') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// SECURITY: Timing-safe comparison to prevent timing attacks
|
|
64
|
+
for (const [storedKey, keyInfo] of this.keys.entries()) {
|
|
65
|
+
if (timingSafeKeyCompare(key, storedKey)) {
|
|
66
|
+
return keyInfo;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Constant-time operation for invalid key
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
async trackUsage(key, credits) {
|
|
73
|
+
const current = this.usage.get(key) || 0;
|
|
74
|
+
this.usage.set(key, current + credits);
|
|
75
|
+
}
|
|
76
|
+
addKey(keyInfo) {
|
|
77
|
+
// SECURITY: Validate key format before adding
|
|
78
|
+
if (!validateKeyFormat(keyInfo.key)) {
|
|
79
|
+
throw new Error('Invalid API key format: must be at least 32 characters, alphanumeric with - or _');
|
|
80
|
+
}
|
|
81
|
+
this.keys.set(keyInfo.key, keyInfo);
|
|
82
|
+
}
|
|
83
|
+
getUsage(key) {
|
|
84
|
+
return this.usage.get(key) || 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=auth-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../../src/server/auth-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key authentication middleware
|
|
3
|
+
*/
|
|
4
|
+
import { Request, Response, NextFunction } from 'express';
|
|
5
|
+
import { AuthStore, ApiKeyInfo } from '../auth-store.js';
|
|
6
|
+
declare global {
|
|
7
|
+
namespace Express {
|
|
8
|
+
interface Request {
|
|
9
|
+
auth?: {
|
|
10
|
+
keyInfo: ApiKeyInfo | null;
|
|
11
|
+
tier: 'free' | 'starter' | 'pro' | 'enterprise';
|
|
12
|
+
rateLimit: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export declare function createAuthMiddleware(authStore: AuthStore): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
18
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE;gBACL,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;gBAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,CAAC;gBAChD,SAAS,EAAE,MAAM,CAAC;aACnB,CAAC;SACH;KACF;CACF;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,IACzC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAsD9D"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key authentication middleware
|
|
3
|
+
*/
|
|
4
|
+
export function createAuthMiddleware(authStore) {
|
|
5
|
+
return async (req, res, next) => {
|
|
6
|
+
try {
|
|
7
|
+
// Extract API key from Authorization header or X-API-Key header
|
|
8
|
+
const authHeader = req.headers.authorization;
|
|
9
|
+
const apiKeyHeader = req.headers['x-api-key'];
|
|
10
|
+
let apiKey = null;
|
|
11
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
12
|
+
apiKey = authHeader.slice(7);
|
|
13
|
+
}
|
|
14
|
+
else if (apiKeyHeader && typeof apiKeyHeader === 'string') {
|
|
15
|
+
apiKey = apiKeyHeader;
|
|
16
|
+
}
|
|
17
|
+
// SECURITY: Require API key for all non-health endpoints
|
|
18
|
+
const isHealthEndpoint = req.path === '/health';
|
|
19
|
+
if (!apiKey && !isHealthEndpoint) {
|
|
20
|
+
res.status(401).json({
|
|
21
|
+
error: 'missing_key',
|
|
22
|
+
message: 'API key is required. Provide via Authorization: Bearer <key> or X-API-Key header.',
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Validate API key if provided
|
|
27
|
+
let keyInfo = null;
|
|
28
|
+
if (apiKey) {
|
|
29
|
+
keyInfo = await authStore.validateKey(apiKey);
|
|
30
|
+
if (!keyInfo) {
|
|
31
|
+
res.status(401).json({
|
|
32
|
+
error: 'invalid_key',
|
|
33
|
+
message: 'Invalid API key',
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Set auth context on request
|
|
39
|
+
req.auth = {
|
|
40
|
+
keyInfo,
|
|
41
|
+
tier: keyInfo?.tier || 'free',
|
|
42
|
+
rateLimit: keyInfo?.rateLimit || 10,
|
|
43
|
+
};
|
|
44
|
+
next();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const err = error;
|
|
48
|
+
res.status(500).json({
|
|
49
|
+
error: 'auth_error',
|
|
50
|
+
message: err.message || 'Authentication failed',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/server/middleware/auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiBH,MAAM,UAAU,oBAAoB,CAAC,SAAoB;IACvD,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,IAAI,CAAC;YACH,gEAAgE;YAChE,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;YAC7C,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YAE9C,IAAI,MAAM,GAAkB,IAAI,CAAC;YAEjC,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACtC,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/B,CAAC;iBAAM,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;gBAC5D,MAAM,GAAG,YAAY,CAAC;YACxB,CAAC;YAED,yDAAyD;YACzD,MAAM,gBAAgB,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC;YAEhD,IAAI,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACjC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,KAAK,EAAE,aAAa;oBACpB,OAAO,EAAE,mFAAmF;iBAC7F,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,+BAA+B;YAC/B,IAAI,OAAO,GAAsB,IAAI,CAAC;YACtC,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,KAAK,EAAE,aAAa;wBACpB,OAAO,EAAE,iBAAiB;qBAC3B,CAAC,CAAC;oBACH,OAAO;gBACT,CAAC;YACH,CAAC;YAED,8BAA8B;YAC9B,GAAG,CAAC,IAAI,GAAG;gBACT,OAAO;gBACP,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,MAAM;gBAC7B,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE;aACpC,CAAC;YAEF,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAc,CAAC;YAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,YAAY;gBACnB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,uBAAuB;aAChD,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window rate limiting middleware
|
|
3
|
+
*/
|
|
4
|
+
import { Request, Response, NextFunction } from 'express';
|
|
5
|
+
export declare class RateLimiter {
|
|
6
|
+
private store;
|
|
7
|
+
private windowMs;
|
|
8
|
+
constructor(windowMs?: number);
|
|
9
|
+
/**
|
|
10
|
+
* Check if request is allowed under rate limit
|
|
11
|
+
*/
|
|
12
|
+
checkLimit(identifier: string, limit: number): {
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
remaining: number;
|
|
15
|
+
retryAfter?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Clean up old entries (call periodically)
|
|
19
|
+
*/
|
|
20
|
+
cleanup(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createRateLimitMiddleware(limiter: RateLimiter): (req: Request, res: Response, next: NextFunction) => void;
|
|
23
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM1D,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,GAAE,MAAc;IAIpC;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;QAC7C,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB;IAmCD;;OAEG;IACH,OAAO,IAAI,IAAI;CAWhB;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,IACpD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,UA+BxD"}
|