kiku-agent 1.0.0 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +293 -122
  2. package/package.json +60 -61
  3. package/server/agent/agentCore.js +892 -782
  4. package/server/agent/appPaths.js +21 -0
  5. package/server/agent/audioPlayer.js +739 -673
  6. package/server/agent/cli.js +1414 -1111
  7. package/server/agent/commandRegistry.js +74 -58
  8. package/server/agent/commands.js +1694 -788
  9. package/server/agent/config.js +264 -90
  10. package/server/agent/contextCompactor.js +79 -0
  11. package/server/agent/doctor.js +69 -0
  12. package/server/agent/eventBus.js +107 -103
  13. package/server/agent/gpuMetrics.js +189 -0
  14. package/server/agent/llmClient.js +369 -200
  15. package/server/agent/mcpClient.js +364 -0
  16. package/server/agent/memory.js +670 -566
  17. package/server/agent/modelManager.js +77 -0
  18. package/server/agent/research.js +202 -0
  19. package/server/agent/session.js +244 -239
  20. package/server/agent/subagent.js +116 -0
  21. package/server/agent/theme.js +125 -124
  22. package/server/agent/toolRouter.js +201 -0
  23. package/server/agent/tools.js +1679 -1528
  24. package/server/agent/ui.js +760 -469
  25. package/server/agent/youtubeAuth.js +144 -65
  26. package/server/mcp/claude-desktop-config.example.json +1 -1
  27. package/server/mcp/data/discography.js +36 -36
  28. package/server/mcp/dataAdapter.js +102 -0
  29. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  30. package/server/mcp/examples/claude-desktop.example.json +11 -0
  31. package/server/mcp/examples/cline.example.json +13 -0
  32. package/server/mcp/examples/cursor.example.json +14 -0
  33. package/server/mcp/examples/gemini-cli.example.json +17 -0
  34. package/server/mcp/examples/opencode.example.json +21 -0
  35. package/server/mcp/examples/vscode.example.json +19 -0
  36. package/server/mcp/examples/windsurf.example.json +14 -0
  37. package/server/mcp/handler.js +85 -159
  38. package/server/mcp/security.js +140 -138
  39. package/server/mcp/server.js +129 -100
  40. package/server/mcp/stdio.js +4 -2
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "kiku": {
5
+ "type": "local",
6
+ "command": ["npx", "-y", "-p", "kiku-agent", "kiku-mcp"],
7
+ "environment": {
8
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
9
+ },
10
+ "enabled": true
11
+ },
12
+ "kiku-remote": {
13
+ "type": "remote",
14
+ "url": "https://jujingyi-kikuuu.vercel.app/mcp",
15
+ "headers": {
16
+ "X-API-Key": "YOUR_API_KEY"
17
+ },
18
+ "enabled": true
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "servers": {
3
+ "kiku": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
7
+ "env": {
8
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
9
+ }
10
+ },
11
+ "kiku-remote": {
12
+ "type": "http",
13
+ "url": "https://jujingyi-kikuuu.vercel.app/mcp",
14
+ "headers": {
15
+ "X-API-Key": "YOUR_API_KEY"
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "command": "npx",
5
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
6
+ "env": {
7
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
8
+ }
9
+ },
10
+ "kiku-remote": {
11
+ "serverUrl": "https://jujingyi-kikuuu.vercel.app/mcp"
12
+ }
13
+ }
14
+ }
@@ -1,159 +1,85 @@
1
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
2
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
- import { createMcpServer } from './server.js';
4
- import { checkRateLimit, validateSessionId, securityMiddleware, MAX_REQUEST_BODY_SIZE } from './security.js';
5
-
6
- const sessions = new Map();
7
- const SESSION_TTL_MS = 30 * 60_000;
8
- const MAX_SESSIONS = 100;
9
-
10
- function sendJson(res, statusCode, payload, headers = {}) {
11
- if (res.headersSent) return;
12
- res.statusCode = statusCode;
13
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
14
- for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
15
- res.end(JSON.stringify(payload));
16
- }
17
-
18
- async function closeSession(sessionId) {
19
- const session = sessions.get(sessionId);
20
- if (!session) return;
21
- sessions.delete(sessionId);
22
- clearInterval(session.heartbeat);
23
- await Promise.allSettled([session.transport.close(), session.server.close()]);
24
- }
25
-
26
- function cleanupSessions() {
27
- const now = Date.now();
28
- for (const [id, session] of sessions) {
29
- if (now - session.lastSeen > SESSION_TTL_MS) void closeSession(id);
30
- }
31
- if (sessions.size > MAX_SESSIONS) {
32
- const excess = [...sessions.entries()]
33
- .sort((a, b) => a[1].lastSeen - b[1].lastSeen)
34
- .slice(0, sessions.size - MAX_SESSIONS);
35
- for (const [id] of excess) void closeSession(id);
36
- }
37
- }
38
-
39
- function validateRequestSize(req, res) {
40
- const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
41
- if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BODY_SIZE) {
42
- sendJson(res, 413, { error: 'Request body too large.' }, { 'Retry-After': '60' });
43
- return false;
44
- }
45
- return true;
46
- }
47
-
48
- export async function handleMcpSse(req, res) {
49
- cleanupSessions();
50
- const security = securityMiddleware(req, res);
51
- if (security.blocked) return;
52
-
53
- const transport = new SSEServerTransport('/api/v1/mcp/message', res);
54
- const sessionId = transport.sessionId;
55
- const server = createMcpServer();
56
- const session = {
57
- server,
58
- transport,
59
- createdAt: Date.now(),
60
- lastSeen: Date.now(),
61
- messageCount: 0,
62
- heartbeat: null,
63
- };
64
-
65
- sessions.set(sessionId, session);
66
- transport.onclose = () => { void closeSession(sessionId); };
67
- transport.onerror = error => console.error(`MCP SSE transport [${sessionId}]: ${error.message}`);
68
-
69
- try {
70
- await server.connect(transport);
71
- session.heartbeat = setInterval(() => {
72
- if (!res.writableEnded) res.write(': keep-alive\n\n');
73
- }, 15_000);
74
- session.heartbeat.unref?.();
75
- } catch (error) {
76
- await closeSession(sessionId);
77
- if (!res.headersSent) sendJson(res, 500, { error: 'Unable to establish MCP SSE transport.' });
78
- throw error;
79
- }
80
- }
81
-
82
- export async function handleMcpMessage(req, res) {
83
- const security = securityMiddleware(req, res);
84
- if (security.blocked || !validateRequestSize(req, res)) return;
85
-
86
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
87
- const sessionId = url.searchParams.get('sessionId');
88
- if (!sessionId || !validateSessionId(sessionId)) {
89
- return sendJson(res, 400, { error: 'Invalid or missing sessionId.' });
90
- }
91
-
92
- const session = sessions.get(sessionId);
93
- if (!session) {
94
- return sendJson(res, 404, { error: 'Session not found or expired. Reconnect through /api/v1/mcp/sse.' });
95
- }
96
-
97
- const rate = checkRateLimit(`mcp:${sessionId}`);
98
- if (!rate.allowed) {
99
- return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
100
- }
101
- res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
102
-
103
- session.messageCount++;
104
- session.lastSeen = Date.now();
105
- if (session.messageCount > 500) {
106
- await closeSession(sessionId);
107
- return sendJson(res, 429, { error: 'Session message limit reached. Reconnect to create a new session.' });
108
- }
109
-
110
- await session.transport.handlePostMessage(req, res);
111
- }
112
-
113
- export async function handleMcpHttp(req, res) {
114
- const security = securityMiddleware(req, res);
115
- if (security.blocked || !validateRequestSize(req, res)) return;
116
-
117
- if (req.method !== 'POST') {
118
- res.setHeader('Allow', 'POST, OPTIONS');
119
- return sendJson(res, 405, {
120
- jsonrpc: '2.0',
121
- error: { code: -32000, message: 'Method not allowed. Use POST for Streamable HTTP.' },
122
- id: null,
123
- });
124
- }
125
-
126
- const rate = checkRateLimit(`mcp-http:${req.socket?.remoteAddress || 'unknown'}`);
127
- if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
128
- res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
129
-
130
- const server = createMcpServer();
131
- const transport = new StreamableHTTPServerTransport({
132
- sessionIdGenerator: undefined,
133
- enableJsonResponse: true,
134
- });
135
-
136
- try {
137
- await server.connect(transport);
138
- await transport.handleRequest(req, res);
139
- } catch (error) {
140
- console.error(`MCP Streamable HTTP: ${error.message}`);
141
- sendJson(res, 500, {
142
- jsonrpc: '2.0',
143
- error: { code: -32603, message: 'Internal MCP server error.' },
144
- id: null,
145
- });
146
- } finally {
147
- await Promise.allSettled([transport.close(), server.close()]);
148
- }
149
- }
150
-
151
- export function getMcpSessionStats() {
152
- return { activeSessions: sessions.size, maxSessions: MAX_SESSIONS, ttlMs: SESSION_TTL_MS };
153
- }
154
-
155
- export function isMcpRoute(pathname) {
156
- return pathname === '/api/v1/mcp'
157
- || pathname === '/api/v1/mcp/sse'
158
- || pathname === '/api/v1/mcp/message';
159
- }
1
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
+ import { createMcpServer } from './server.js';
3
+ import { checkRateLimit, securityMiddleware, MAX_REQUEST_BODY_SIZE } from './security.js';
4
+
5
+ function sendJson(res, statusCode, payload, headers = {}) {
6
+ if (res.headersSent) return;
7
+ res.statusCode = statusCode;
8
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
9
+ for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
10
+ res.end(JSON.stringify(payload));
11
+ }
12
+
13
+ function validateRequestSize(req, res) {
14
+ const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
15
+ if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BODY_SIZE) {
16
+ sendJson(res, 413, { error: 'Request body too large.' }, { 'Retry-After': '60' });
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ export async function handleMcpSse(req, res) {
23
+ sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
24
+ }
25
+
26
+ export async function handleMcpMessage(req, res) {
27
+ sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
28
+ }
29
+
30
+ export async function handleMcpHttp(req, res) {
31
+ const security = securityMiddleware(req, res);
32
+ if (security.blocked || !validateRequestSize(req, res)) return;
33
+
34
+ if (req.method === 'OPTIONS') {
35
+ res.statusCode = 204;
36
+ res.setHeader('Access-Control-Allow-Origin', '*');
37
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
38
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
39
+ res.end();
40
+ return;
41
+ }
42
+
43
+ if (req.method !== 'POST') {
44
+ res.setHeader('Allow', 'GET, POST, OPTIONS');
45
+ return sendJson(res, 405, {
46
+ jsonrpc: '2.0',
47
+ error: { code: -32000, message: 'Method not allowed. Use POST for Streamable HTTP JSON-RPC, or GET /mcp for server discovery.' },
48
+ id: null,
49
+ });
50
+ }
51
+
52
+ const rate = checkRateLimit(`mcp-http:${req.socket?.remoteAddress || 'unknown'}`);
53
+ if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
54
+ res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
55
+
56
+ const server = createMcpServer();
57
+ const transport = new StreamableHTTPServerTransport({
58
+ sessionIdGenerator: undefined,
59
+ enableJsonResponse: true,
60
+ });
61
+
62
+ try {
63
+ await server.connect(transport);
64
+ await transport.handleRequest(req, res);
65
+ } catch (error) {
66
+ console.error(`MCP Streamable HTTP: ${error.message}`);
67
+ sendJson(res, 500, {
68
+ jsonrpc: '2.0',
69
+ error: { code: -32603, message: 'Internal MCP server error.' },
70
+ id: null,
71
+ });
72
+ } finally {
73
+ await Promise.allSettled([transport.close(), server.close()]);
74
+ }
75
+ }
76
+
77
+ export function getMcpSessionStats() {
78
+ return { mode: 'stateless', transport: 'streamable-http' };
79
+ }
80
+
81
+ export function isMcpRoute(pathname) {
82
+ return pathname === '/mcp'
83
+ || pathname === '/mcp/sse'
84
+ || pathname === '/mcp/message';
85
+ }
@@ -1,138 +1,140 @@
1
- import { timingSafeEqual } from 'node:crypto';
2
-
3
- const SECURITY_CONFIG = {
4
- apiKeyRequired: !!process.env.MCP_API_KEY,
5
-
6
- rateLimitWindowMs: 60_000,
7
- rateLimitMaxRequests: 30,
8
-
9
- maxConcurrentSessions: 100,
10
- sessionTtlMs: 30 * 60_000,
11
- maxMessagesPerSession: 500,
12
-
13
- corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '*').split(',').map(s => s.trim()),
14
- maxRequestBodyBytes: 1_048_576,
15
- };
16
-
17
- export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
18
-
19
- export function validateApiKey(req) {
20
- if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
21
-
22
- let key = req.headers['x-api-key'];
23
- if (!key) {
24
- try {
25
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
26
- key = url.searchParams.get('key') || url.searchParams.get('apiKey');
27
- } catch {
28
- }
29
- }
30
- if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or ?key= query parameter.' };
31
- const expected = Buffer.from(process.env.MCP_API_KEY || '');
32
- const received = Buffer.from(String(key));
33
- if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
34
- return { valid: false, error: 'Invalid API key.' };
35
- }
36
- return { valid: true };
37
- }
38
-
39
- const rateLimitStore = new Map();
40
-
41
- export function checkRateLimit(sessionId) {
42
- const now = Date.now();
43
- const entry = rateLimitStore.get(sessionId);
44
-
45
- if (!entry || now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs) {
46
- rateLimitStore.set(sessionId, { count: 1, windowStart: now });
47
- return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMaxRequests - 1 };
48
- }
49
-
50
- entry.count++;
51
- const remaining = SECURITY_CONFIG.rateLimitMaxRequests - entry.count;
52
- if (remaining < 0) {
53
- return { allowed: false, error: `Rate limit exceeded. Max ${SECURITY_CONFIG.rateLimitMaxRequests} requests per minute.` };
54
- }
55
- return { allowed: true, remaining };
56
- }
57
-
58
- const rateLimitCleanupTimer = setInterval(() => {
59
- const now = Date.now();
60
- for (const [id, entry] of rateLimitStore) {
61
- if (now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs * 2) {
62
- rateLimitStore.delete(id);
63
- }
64
- }
65
- }, 60_000);
66
- rateLimitCleanupTimer.unref?.();
67
-
68
- export function sanitizeInput(input) {
69
- if (typeof input === 'string') {
70
- return input
71
- .replace(/[<>]/g, '')
72
- .replace(/\x00/g, '')
73
- .slice(0, 1000);
74
- }
75
- if (typeof input === 'object' && input !== null) {
76
- const sanitized = {};
77
- for (const [key, value] of Object.entries(input)) {
78
- if (typeof key === 'string' && key.length <= 100) {
79
- sanitized[key] = sanitizeInput(value);
80
- }
81
- }
82
- return sanitized;
83
- }
84
- return input;
85
- }
86
-
87
- export function validateSessionId(sessionId) {
88
- if (!sessionId || typeof sessionId !== 'string') return false;
89
- return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
90
- }
91
-
92
- export function setCorsHeaders(res, origin) {
93
- const allowed = SECURITY_CONFIG.corsAllowedOrigins;
94
- const isAllowed = allowed.includes('*') || allowed.includes(origin);
95
-
96
- if (isAllowed) {
97
- res.setHeader('Access-Control-Allow-Origin', origin || '*');
98
- } else {
99
- res.setHeader('Access-Control-Allow-Origin', '');
100
- }
101
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
102
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
103
- res.setHeader('Access-Control-Max-Age', '86400');
104
-
105
- return isAllowed;
106
- }
107
-
108
- export function securityMiddleware(req, res) {
109
- const origin = req.headers.origin || '';
110
- const corsOk = setCorsHeaders(res, origin);
111
-
112
- if (req.method === 'OPTIONS') {
113
- res.statusCode = 204;
114
- res.end();
115
- return { blocked: true };
116
- }
117
-
118
- if (!corsOk) {
119
- res.statusCode = 403;
120
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
121
- res.end(JSON.stringify({ error: 'Origin not allowed by CORS policy.' }));
122
- return { blocked: true };
123
- }
124
-
125
- const auth = validateApiKey(req);
126
- if (!auth.valid) {
127
- res.statusCode = 401;
128
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
129
- res.end(JSON.stringify({ error: auth.error }));
130
- return { blocked: true };
131
- }
132
-
133
- return { blocked: false };
134
- }
135
-
136
- export function getSecurityConfig() {
137
- return { ...SECURITY_CONFIG, apiKeyRequired: SECURITY_CONFIG.apiKeyRequired };
138
- }
1
+ import { timingSafeEqual } from 'node:crypto';
2
+
3
+ const SECURITY_CONFIG = {
4
+ apiKeyRequired: !!process.env.MCP_API_KEY,
5
+
6
+ rateLimitWindowMs: 60_000,
7
+ rateLimitMaxRequests: 30,
8
+
9
+ maxConcurrentSessions: 100,
10
+ sessionTtlMs: 30 * 60_000,
11
+ maxMessagesPerSession: 500,
12
+
13
+ corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '*').split(',').map(s => s.trim()),
14
+ maxRequestBodyBytes: 1_048_576,
15
+ };
16
+
17
+ export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
18
+
19
+ export function validateApiKey(req) {
20
+ if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
21
+
22
+ let key = req.headers['x-api-key'];
23
+ if (!key) {
24
+ try {
25
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
26
+ key = url.searchParams.get('key') || url.searchParams.get('apiKey');
27
+ } catch {
28
+ }
29
+ }
30
+ if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or ?key= query parameter.' };
31
+ const expected = Buffer.from(process.env.MCP_API_KEY || '');
32
+ const received = Buffer.from(String(key));
33
+ if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
34
+ return { valid: false, error: 'Invalid API key.' };
35
+ }
36
+ return { valid: true };
37
+ }
38
+
39
+ const rateLimitStore = new Map();
40
+
41
+ export function checkRateLimit(sessionId) {
42
+ const now = Date.now();
43
+ const entry = rateLimitStore.get(sessionId);
44
+
45
+ if (!entry || now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs) {
46
+ rateLimitStore.set(sessionId, { count: 1, windowStart: now });
47
+ return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMaxRequests - 1 };
48
+ }
49
+
50
+ entry.count++;
51
+ const remaining = SECURITY_CONFIG.rateLimitMaxRequests - entry.count;
52
+ if (remaining < 0) {
53
+ return { allowed: false, error: `Rate limit exceeded. Max ${SECURITY_CONFIG.rateLimitMaxRequests} requests per minute.` };
54
+ }
55
+ return { allowed: true, remaining };
56
+ }
57
+
58
+ const rateLimitCleanupTimer = setInterval(() => {
59
+ const now = Date.now();
60
+ for (const [id, entry] of rateLimitStore) {
61
+ if (now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs * 2) {
62
+ rateLimitStore.delete(id);
63
+ }
64
+ }
65
+ }, 60_000);
66
+ rateLimitCleanupTimer.unref?.();
67
+
68
+ export function sanitizeInput(input) {
69
+ if (typeof input === 'string') {
70
+ return input
71
+ .replace(/[<>]/g, '')
72
+ .replace(/\x00/g, '')
73
+ .slice(0, 1000);
74
+ }
75
+ if (typeof input === 'object' && input !== null) {
76
+ const sanitized = {};
77
+ for (const [key, value] of Object.entries(input)) {
78
+ if (typeof key === 'string' && key.length <= 100) {
79
+ sanitized[key] = sanitizeInput(value);
80
+ }
81
+ }
82
+ return sanitized;
83
+ }
84
+ return input;
85
+ }
86
+
87
+ export function validateSessionId(sessionId) {
88
+ if (!sessionId || typeof sessionId !== 'string') return false;
89
+ return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
90
+ }
91
+
92
+ export function setCorsHeaders(res, origin) {
93
+ const allowed = SECURITY_CONFIG.corsAllowedOrigins;
94
+ const isAllowed = allowed.includes('*') || allowed.includes(origin);
95
+
96
+ if (isAllowed) {
97
+ res.setHeader('Access-Control-Allow-Origin', origin || '*');
98
+ } else {
99
+ res.setHeader('Access-Control-Allow-Origin', '');
100
+ }
101
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
102
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
103
+ res.setHeader('Access-Control-Max-Age', '86400');
104
+ res.setHeader('X-Content-Type-Options', 'nosniff');
105
+ res.setHeader('X-Frame-Options', 'DENY');
106
+
107
+ return isAllowed;
108
+ }
109
+
110
+ export function securityMiddleware(req, res) {
111
+ const origin = req.headers.origin || '';
112
+ const corsOk = setCorsHeaders(res, origin);
113
+
114
+ if (req.method === 'OPTIONS') {
115
+ res.statusCode = 204;
116
+ res.end();
117
+ return { blocked: true };
118
+ }
119
+
120
+ if (!corsOk) {
121
+ res.statusCode = 403;
122
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
123
+ res.end(JSON.stringify({ error: 'Origin not allowed by CORS policy.' }));
124
+ return { blocked: true };
125
+ }
126
+
127
+ const auth = validateApiKey(req);
128
+ if (!auth.valid) {
129
+ res.statusCode = 401;
130
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
131
+ res.end(JSON.stringify({ error: auth.error }));
132
+ return { blocked: true };
133
+ }
134
+
135
+ return { blocked: false };
136
+ }
137
+
138
+ export function getSecurityConfig() {
139
+ return { ...SECURITY_CONFIG, apiKeyRequired: SECURITY_CONFIG.apiKeyRequired };
140
+ }