cipher-security 2.0.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.
Files changed (76) hide show
  1. package/bin/cipher.js +566 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +238 -0
  45. package/lib/brand.js +105 -0
  46. package/lib/commands.js +100 -0
  47. package/lib/complexity.js +377 -0
  48. package/lib/config.js +213 -0
  49. package/lib/gateway/client.js +309 -0
  50. package/lib/gateway/commands.js +991 -0
  51. package/lib/gateway/config-validate.js +109 -0
  52. package/lib/gateway/gateway.js +367 -0
  53. package/lib/gateway/index.js +62 -0
  54. package/lib/gateway/mode.js +309 -0
  55. package/lib/gateway/plugins.js +222 -0
  56. package/lib/gateway/prompt.js +214 -0
  57. package/lib/mcp/server.js +262 -0
  58. package/lib/memory/compressor.js +425 -0
  59. package/lib/memory/engine.js +763 -0
  60. package/lib/memory/evolution.js +668 -0
  61. package/lib/memory/index.js +58 -0
  62. package/lib/memory/orchestrator.js +506 -0
  63. package/lib/memory/retriever.js +515 -0
  64. package/lib/memory/synthesizer.js +333 -0
  65. package/lib/pipeline/async-scanner.js +510 -0
  66. package/lib/pipeline/binary-analysis.js +1043 -0
  67. package/lib/pipeline/dom-xss-scanner.js +435 -0
  68. package/lib/pipeline/github-actions.js +792 -0
  69. package/lib/pipeline/index.js +124 -0
  70. package/lib/pipeline/osint.js +498 -0
  71. package/lib/pipeline/sarif.js +373 -0
  72. package/lib/pipeline/scanner.js +880 -0
  73. package/lib/pipeline/template-manager.js +525 -0
  74. package/lib/pipeline/xss-scanner.js +353 -0
  75. package/lib/setup-wizard.js +288 -0
  76. package/package.json +31 -0
@@ -0,0 +1,685 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ /**
5
+ * CIPHER REST API Server — zero-dependency SaaS server.
6
+ *
7
+ * Built on node:http. Provides authenticated, rate-limited endpoints
8
+ * that expose CIPHER's scanning, memory, leaderboard, and workflow
9
+ * capabilities over HTTP/JSON.
10
+ */
11
+
12
+ import { createServer as httpCreateServer } from 'node:http';
13
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
14
+ import { URL } from 'node:url';
15
+ import { isIP } from 'node:net';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Configuration
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Server-level configuration with safe defaults. */
22
+ export class APIConfig {
23
+ constructor(opts = {}) {
24
+ this.host = opts.host ?? '127.0.0.1';
25
+ this.port = opts.port ?? 8443;
26
+ this.apiVersion = opts.apiVersion ?? 'v1';
27
+ this.rateLimitRpm = opts.rateLimitRpm ?? 60;
28
+ this.maxRequestSize = opts.maxRequestSize ?? 1_048_576; // 1 MB
29
+ this.corsOrigins = opts.corsOrigins ?? ['*'];
30
+ this.requireAuth = opts.requireAuth ?? true;
31
+ }
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Response envelope
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** Uniform JSON response wrapper. */
39
+ export class APIResponse {
40
+ constructor({ status = 200, data = null, error = null, meta = null } = {}) {
41
+ this.status = status;
42
+ this.data = data;
43
+ this.error = error;
44
+ this.meta = meta;
45
+ }
46
+
47
+ toJson() {
48
+ const payload = { status: this.status };
49
+ if (this.data !== null) payload.data = this.data;
50
+ if (this.error !== null) payload.error = this.error;
51
+ if (this.meta !== null) payload.meta = this.meta;
52
+ return JSON.stringify(payload);
53
+ }
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Rate limiter — sliding-window per client
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /** Sliding-window rate limiter. */
61
+ export class RateLimiter {
62
+ constructor(rpm = 60) {
63
+ this._rpm = rpm;
64
+ this._window = 60_000; // ms
65
+ /** @type {Map<string, number[]>} */
66
+ this._requests = new Map();
67
+ }
68
+
69
+ /**
70
+ * Return true if the request is allowed.
71
+ * @param {string} clientId
72
+ * @returns {boolean}
73
+ */
74
+ check(clientId) {
75
+ const now = Date.now();
76
+ const cutoff = now - this._window;
77
+ let timestamps = this._requests.get(clientId) || [];
78
+ timestamps = timestamps.filter((t) => t > cutoff);
79
+ if (timestamps.length >= this._rpm) {
80
+ this._requests.set(clientId, timestamps);
81
+ return false;
82
+ }
83
+ timestamps.push(now);
84
+ this._requests.set(clientId, timestamps);
85
+ return true;
86
+ }
87
+
88
+ /** Get current state for diagnostics. */
89
+ getState() {
90
+ return {
91
+ trackedClients: this._requests.size,
92
+ rpm: this._rpm,
93
+ };
94
+ }
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Auth — HMAC-SHA256 bearer tokens
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * HMAC-SHA256 token auth.
103
+ * Tokens: `<client_id>:<random_hex>:<hmac_hex>`
104
+ */
105
+ export class AuthHandler {
106
+ constructor() {
107
+ this._secret = process.env.CIPHER_API_SECRET || randomBytes(32).toString('hex');
108
+ /** @type {Map<string, string[]>} */
109
+ this._scopes = new Map();
110
+ }
111
+
112
+ /**
113
+ * Create a signed bearer token.
114
+ * @param {string} clientId
115
+ * @param {string[]} [scopes]
116
+ * @returns {string}
117
+ */
118
+ generateToken(clientId, scopes = ['read', 'write']) {
119
+ this._scopes.set(clientId, scopes);
120
+ const nonce = randomBytes(16).toString('hex');
121
+ const payload = `${clientId}:${nonce}`;
122
+ const sig = createHmac('sha256', this._secret).update(payload).digest('hex');
123
+ return `${payload}:${sig}`;
124
+ }
125
+
126
+ /**
127
+ * Validate a bearer token.
128
+ * @param {string} token
129
+ * @returns {{ clientId: string, scopes: string[] } | null}
130
+ */
131
+ validateToken(token) {
132
+ const parts = token.split(':');
133
+ if (parts.length !== 3) return null;
134
+ const [clientId, nonce, sig] = parts;
135
+ const expected = createHmac('sha256', this._secret)
136
+ .update(`${clientId}:${nonce}`)
137
+ .digest('hex');
138
+
139
+ // Length pre-check to avoid timing oracle on different-length strings
140
+ if (sig.length !== expected.length) return null;
141
+ const sigBuf = Buffer.from(sig, 'utf8');
142
+ const expBuf = Buffer.from(expected, 'utf8');
143
+ if (!timingSafeEqual(sigBuf, expBuf)) return null;
144
+
145
+ const scopes = this._scopes.get(clientId) || ['read'];
146
+ return { clientId, scopes };
147
+ }
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // SSRF validation
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Validate scan target to prevent SSRF against internal networks.
156
+ * @param {string} target
157
+ * @returns {boolean}
158
+ */
159
+ export function validateScanTarget(target) {
160
+ if (!target || typeof target !== 'string' || target.length > 2048) return false;
161
+
162
+ let host;
163
+ try {
164
+ const parsed = new URL(target.includes('://') ? target : `http://${target}`);
165
+ host = parsed.hostname;
166
+ } catch {
167
+ return false;
168
+ }
169
+ if (!host) return false;
170
+
171
+ const blockedHosts = new Set([
172
+ 'metadata.google.internal',
173
+ 'metadata.google',
174
+ '169.254.169.254',
175
+ 'fd00:ec2::254',
176
+ 'localhost',
177
+ 'localhost.localdomain',
178
+ '0.0.0.0',
179
+ ]);
180
+ if (blockedHosts.has(host.toLowerCase())) return false;
181
+
182
+ // Check if it looks like a private/loopback IP
183
+ if (isIP(host)) {
184
+ // Simple private range checks
185
+ if (host.startsWith('10.') || host.startsWith('192.168.') || host === '127.0.0.1') return false;
186
+ if (host.startsWith('172.')) {
187
+ const second = parseInt(host.split('.')[1], 10);
188
+ if (second >= 16 && second <= 31) return false;
189
+ }
190
+ if (host.startsWith('169.254.')) return false;
191
+ if (host === '::1' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) return false;
192
+ }
193
+
194
+ // Reject numeric-looking hosts that might be encoded IPs
195
+ if (/^[0-9x.]+$/i.test(host) && !isIP(host)) return false;
196
+
197
+ return true;
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Main server
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * CIPHER SaaS REST API server.
206
+ * @param {APIConfig} [config]
207
+ * @returns {{ server: import('node:http').Server, config: APIConfig, auth: AuthHandler, rateLimiter: RateLimiter, start: () => Promise<number>, stop: () => Promise<void> }}
208
+ */
209
+ export function createAPIServer(config) {
210
+ config = config instanceof APIConfig ? config : new APIConfig(config);
211
+ const auth = new AuthHandler();
212
+ const rateLimiter = new RateLimiter(config.rateLimitRpm);
213
+ const startTime = Date.now();
214
+ let requestCount = 0;
215
+ const v = config.apiVersion;
216
+
217
+ // -- Route table ----------------------------------------------------------
218
+
219
+ const routes = [
220
+ { method: 'GET', path: `/${v}/health`, handler: handleHealth, authRequired: false, scopes: [] },
221
+ { method: 'GET', path: `/${v}/stats`, handler: handleStats, authRequired: true, scopes: [] },
222
+ { method: 'POST', path: `/${v}/scan`, handler: handleScan, authRequired: true, scopes: ['write'] },
223
+ { method: 'POST', path: `/${v}/diff`, handler: handleDiff, authRequired: true, scopes: ['write'] },
224
+ { method: 'POST', path: `/${v}/secrets`, handler: handleSecrets, authRequired: true, scopes: ['write'] },
225
+ { method: 'GET', path: `/${v}/skills`, handler: handleSkillsList, authRequired: true, scopes: [] },
226
+ { method: 'GET', path: `/${v}/skills/search`, handler: handleSkillsSearch, authRequired: true, scopes: [] },
227
+ { method: 'POST', path: `/${v}/memory/store`, handler: handleMemoryStore, authRequired: true, scopes: ['write'] },
228
+ { method: 'POST', path: `/${v}/memory/search`, handler: handleMemorySearch, authRequired: true, scopes: [] },
229
+ { method: 'GET', path: `/${v}/memory/stats`, handler: handleMemoryStats, authRequired: true, scopes: [] },
230
+ { method: 'POST', path: `/${v}/score`, handler: handleScore, authRequired: true, scopes: ['write'] },
231
+ { method: 'GET', path: `/${v}/leaderboard`, handler: handleLeaderboard, authRequired: true, scopes: [] },
232
+ { method: 'GET', path: `/${v}/leaderboard/dashboard`, handler: handleLeaderboardDashboard, authRequired: true, scopes: [] },
233
+ { method: 'POST', path: `/${v}/workflow`, handler: handleWorkflow, authRequired: true, scopes: ['write'] },
234
+ ];
235
+
236
+ // -- Helpers --------------------------------------------------------------
237
+
238
+ function meta(t0) {
239
+ return { version: v, elapsed_ms: Math.round((Date.now() - t0) * 100) / 100 };
240
+ }
241
+
242
+ function clientIp(headers) {
243
+ const xff = headers['x-forwarded-for'];
244
+ if (xff) return xff.split(',')[0].trim();
245
+ return headers['x-real-ip'] || 'unknown';
246
+ }
247
+
248
+ function extractAuth(headers) {
249
+ const authHeader = headers['authorization'];
250
+ if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
251
+ return authHeader.slice(7).trim();
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function matchRoute(method, pathname) {
257
+ const clean = pathname.replace(/\/+$/, '') || '/';
258
+ return routes.find((r) => r.method === method && r.path === clean) || null;
259
+ }
260
+
261
+ function setCors(res, reqOrigin) {
262
+ const allowed = config.corsOrigins;
263
+ const origin = allowed.includes('*') || allowed.includes(reqOrigin) ? reqOrigin || '*' : allowed[0] || '';
264
+ res.setHeader('Access-Control-Allow-Origin', origin);
265
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
266
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
267
+ res.setHeader('Access-Control-Max-Age', '86400');
268
+ }
269
+
270
+ function sendJson(res, response) {
271
+ const body = response.toJson();
272
+ res.writeHead(response.status, {
273
+ 'Content-Type': 'application/json; charset=utf-8',
274
+ 'Content-Length': Buffer.byteLength(body),
275
+ });
276
+ res.end(body);
277
+ }
278
+
279
+ // -- Request parsing ------------------------------------------------------
280
+
281
+ function parseBody(req) {
282
+ return new Promise((resolve) => {
283
+ const chunks = [];
284
+ let size = 0;
285
+ req.on('data', (chunk) => {
286
+ size += chunk.length;
287
+ if (size > config.maxRequestSize) {
288
+ resolve({ __error__: 'payload too large' });
289
+ req.destroy();
290
+ return;
291
+ }
292
+ chunks.push(chunk);
293
+ });
294
+ req.on('end', () => {
295
+ if (size === 0) return resolve({});
296
+ try {
297
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
298
+ } catch {
299
+ resolve({ __error__: 'invalid JSON' });
300
+ }
301
+ });
302
+ req.on('error', () => resolve({ __error__: 'request error' }));
303
+ });
304
+ }
305
+
306
+ // -- Dispatch engine -------------------------------------------------------
307
+
308
+ async function handleRequest(req, res) {
309
+ const t0 = Date.now();
310
+ requestCount++;
311
+
312
+ const origin = req.headers['origin'] || '*';
313
+ setCors(res, origin);
314
+
315
+ // CORS preflight
316
+ if (req.method === 'OPTIONS') {
317
+ res.writeHead(204);
318
+ res.end();
319
+ return;
320
+ }
321
+
322
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
323
+ const pathname = parsedUrl.pathname;
324
+ const params = Object.fromEntries(
325
+ [...parsedUrl.searchParams].map(([k, v]) => [k, v]),
326
+ );
327
+
328
+ const body =
329
+ req.method === 'POST' || req.method === 'PUT' ? await parseBody(req) : {};
330
+
331
+ if (body.__error__) {
332
+ return sendJson(res, new APIResponse({ status: 400, error: body.__error__, meta: meta(t0) }));
333
+ }
334
+
335
+ const route = matchRoute(req.method, pathname);
336
+ if (!route) {
337
+ return sendJson(res, new APIResponse({ status: 404, error: 'not found', meta: meta(t0) }));
338
+ }
339
+
340
+ // Auth
341
+ let clientId;
342
+ if (route.authRequired && config.requireAuth) {
343
+ const token = extractAuth(req.headers);
344
+ if (!token) {
345
+ return sendJson(res, new APIResponse({ status: 401, error: 'missing authorization', meta: meta(t0) }));
346
+ }
347
+ const identity = auth.validateToken(token);
348
+ if (!identity) {
349
+ return sendJson(res, new APIResponse({ status: 401, error: 'invalid token', meta: meta(t0) }));
350
+ }
351
+ if (route.scopes.length && !route.scopes.some((s) => identity.scopes.includes(s))) {
352
+ return sendJson(res, new APIResponse({ status: 403, error: 'insufficient scope', meta: meta(t0) }));
353
+ }
354
+ clientId = identity.clientId;
355
+ } else {
356
+ clientId = clientIp(req.headers);
357
+ }
358
+
359
+ // Rate limit
360
+ if (!rateLimiter.check(clientId)) {
361
+ return sendJson(res, new APIResponse({ status: 429, error: 'rate limit exceeded', meta: meta(t0) }));
362
+ }
363
+
364
+ // Execute handler
365
+ try {
366
+ const response = await route.handler({ body, params, headers: req.headers });
367
+ response.meta = meta(t0);
368
+ sendJson(res, response);
369
+ } catch (err) {
370
+ sendJson(res, new APIResponse({ status: 500, error: 'internal server error', meta: meta(t0) }));
371
+ }
372
+ }
373
+
374
+ // -- Route handlers -------------------------------------------------------
375
+
376
+ function handleHealth() {
377
+ const uptime = (Date.now() - startTime) / 1000;
378
+ return new APIResponse({
379
+ data: { status: 'healthy', uptime_s: Math.round(uptime * 100) / 100, version: v },
380
+ });
381
+ }
382
+
383
+ function handleStats() {
384
+ const rss = process.memoryUsage?.().rss;
385
+ return new APIResponse({
386
+ data: {
387
+ requests_served: requestCount,
388
+ uptime_s: Math.round((Date.now() - startTime) / 10) / 100,
389
+ node_version: process.version,
390
+ rate_limit_rpm: config.rateLimitRpm,
391
+ memory_rss_kb: rss ? Math.round(rss / 1024) : -1,
392
+ },
393
+ });
394
+ }
395
+
396
+ async function handleScan({ body }) {
397
+ const target = body.target;
398
+ if (!target) return new APIResponse({ status: 400, error: "'target' is required" });
399
+ if (!validateScanTarget(String(target).trim())) {
400
+ return new APIResponse({
401
+ status: 400,
402
+ error: 'invalid scan target: internal/private addresses are not permitted',
403
+ });
404
+ }
405
+ const scanId = randomBytes(8).toString('hex');
406
+ // Lazy import — scanner may not be available in all environments
407
+ try {
408
+ const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
409
+ const runner = new NucleiRunner();
410
+ const profile = body.profile || 'standard';
411
+ const severity = body.severity || 'medium';
412
+ const result = await runner.scan(target, { profile: ScanProfile.fromDomain(profile), severity });
413
+ return new APIResponse({
414
+ data: {
415
+ scan_id: scanId,
416
+ target,
417
+ profile,
418
+ min_severity: severity,
419
+ status: 'completed',
420
+ findings: (result.findings || []).map((f) => ({
421
+ id: f.templateId,
422
+ name: f.name,
423
+ severity: f.severity,
424
+ url: f.matchedAt,
425
+ description: f.description,
426
+ })),
427
+ total_findings: result.findings?.length || 0,
428
+ },
429
+ });
430
+ } catch {
431
+ return new APIResponse({
432
+ data: {
433
+ scan_id: scanId,
434
+ target,
435
+ status: 'error',
436
+ error: 'scanner not available',
437
+ findings: [],
438
+ },
439
+ });
440
+ }
441
+ }
442
+
443
+ async function handleDiff({ body }) {
444
+ const diffText = body.diff_text;
445
+ if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
446
+ try {
447
+ const { SecurityDiffAnalyzer } = await import('../pipeline/github-actions.js');
448
+ const analyzer = new SecurityDiffAnalyzer();
449
+ const analysis = analyzer.analyzeDiff(diffText);
450
+ return new APIResponse({
451
+ data: {
452
+ risk_level: analysis.riskLevel?.value ?? String(analysis.riskLevel),
453
+ files_changed: analysis.filesChanged,
454
+ auth_changes: analysis.authChanges,
455
+ sql_changes: analysis.sqlChanges,
456
+ crypto_changes: analysis.cryptoChanges,
457
+ secrets_found: analysis.secrets?.length ?? 0,
458
+ endpoints_added: analysis.endpointsAdded,
459
+ has_security_findings: analysis.hasSecurityFindings,
460
+ summary: analysis.summary,
461
+ },
462
+ });
463
+ } catch {
464
+ return new APIResponse({ status: 500, error: 'diff analysis not available' });
465
+ }
466
+ }
467
+
468
+ function handleSecrets({ body }) {
469
+ const diffText = body.diff_text;
470
+ if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
471
+ const patterns = {
472
+ aws_key: /AKIA[0-9A-Z]{16}/g,
473
+ generic_secret: /(?:secret|password|token|apikey)\s*[=:]\s*['"][^\s'"]{8,}/gi,
474
+ private_key: /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH)\s+PRIVATE\s+KEY-----/g,
475
+ github_token: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
476
+ };
477
+ const findings = [];
478
+ for (const [name, pat] of Object.entries(patterns)) {
479
+ let m;
480
+ while ((m = pat.exec(diffText)) !== null) {
481
+ findings.push({
482
+ type: name,
483
+ line: diffText.slice(0, m.index).split('\n').length,
484
+ match: m[0].slice(0, 8) + '****',
485
+ });
486
+ }
487
+ }
488
+ return new APIResponse({ data: { secrets_found: findings.length, findings } });
489
+ }
490
+
491
+ async function handleSkillsList() {
492
+ try {
493
+ const { readdirSync, statSync, existsSync } = await import('node:fs');
494
+ const { join } = await import('node:path');
495
+ const skillsDir = 'skills';
496
+ const domains = {};
497
+ if (existsSync(skillsDir)) {
498
+ for (const d of readdirSync(skillsDir).sort()) {
499
+ const full = join(skillsDir, d);
500
+ if (!statSync(full).isDirectory() || d.startsWith('.')) continue;
501
+ const techs = join(full, 'techniques');
502
+ if (existsSync(techs)) {
503
+ domains[d] = readdirSync(techs).filter((t) => statSync(join(techs, t)).isDirectory()).length;
504
+ } else {
505
+ domains[d] = readdirSync(full).filter((t) => {
506
+ const p = join(full, t);
507
+ return statSync(p).isDirectory() && existsSync(join(p, 'SKILL.md'));
508
+ }).length;
509
+ }
510
+ }
511
+ }
512
+ return new APIResponse({
513
+ data: { domains, count: Object.keys(domains).length, total_techniques: Object.values(domains).reduce((a, b) => a + b, 0) },
514
+ });
515
+ } catch {
516
+ return new APIResponse({ data: { domains: {}, count: 0, total_techniques: 0 } });
517
+ }
518
+ }
519
+
520
+ async function handleSkillsSearch({ params }) {
521
+ const query = params.q || '';
522
+ if (!query) return new APIResponse({ status: 400, error: "'q' query parameter is required" });
523
+ try {
524
+ const { existsSync } = await import('node:fs');
525
+ const { join } = await import('node:path');
526
+ const { globSync } = await import('node:fs');
527
+ // Simple file-based search
528
+ const q = query.toLowerCase();
529
+ const results = [];
530
+ // Fallback: just return empty if skills dir doesn't exist
531
+ if (!existsSync('skills')) {
532
+ return new APIResponse({ data: { query, results: [], total: 0 } });
533
+ }
534
+ return new APIResponse({ data: { query, results, total: 0 } });
535
+ } catch {
536
+ return new APIResponse({ data: { query: params.q || '', results: [], total: 0 } });
537
+ }
538
+ }
539
+
540
+ async function handleMemoryStore({ body }) {
541
+ const content = body.content;
542
+ if (!content) return new APIResponse({ status: 400, error: "'content' is required" });
543
+ try {
544
+ const { CipherMemory } = await import('../memory/engine.js');
545
+ const memory = new CipherMemory();
546
+ const entryId = memory.store({
547
+ content,
548
+ memoryType: body.type || 'note',
549
+ severity: body.severity || '',
550
+ engagementId: body.engagement_id || '',
551
+ tags: body.tags || [],
552
+ });
553
+ memory.close();
554
+ return new APIResponse({ status: 201, data: { entry_id: entryId, type: body.type || 'note', stored: true } });
555
+ } catch {
556
+ return new APIResponse({ status: 500, error: 'memory engine not available' });
557
+ }
558
+ }
559
+
560
+ async function handleMemorySearch({ body }) {
561
+ const query = body.query;
562
+ if (!query) return new APIResponse({ status: 400, error: "'query' is required" });
563
+ try {
564
+ const { CipherMemory } = await import('../memory/engine.js');
565
+ const memory = new CipherMemory();
566
+ const results = memory.search(query, body.limit || 10);
567
+ memory.close();
568
+ return new APIResponse({
569
+ data: {
570
+ query,
571
+ results: results.map((r) => ({
572
+ content: r.content,
573
+ type: r.memoryType,
574
+ severity: r.severity,
575
+ created: r.createdAt,
576
+ })),
577
+ total: results.length,
578
+ },
579
+ });
580
+ } catch {
581
+ return new APIResponse({ data: { query, results: [], total: 0 } });
582
+ }
583
+ }
584
+
585
+ async function handleMemoryStats() {
586
+ try {
587
+ const { CipherMemory } = await import('../memory/engine.js');
588
+ const memory = new CipherMemory();
589
+ const stats = memory.stats();
590
+ memory.close();
591
+ return new APIResponse({ data: stats });
592
+ } catch {
593
+ return new APIResponse({ data: {} });
594
+ }
595
+ }
596
+
597
+ async function handleScore({ body }) {
598
+ const query = body.query;
599
+ const responseText = body.response;
600
+ if (!query || !responseText) {
601
+ return new APIResponse({ status: 400, error: "'query' and 'response' are required" });
602
+ }
603
+ try {
604
+ const { ResponseScorer } = await import('../memory/evolution.js');
605
+ const scorer = new ResponseScorer();
606
+ const scored = scorer.score({ query, response: responseText, mode: body.mode || '' });
607
+ return new APIResponse({ data: { score: scored.score, votes: scored.votes, feedback: scored.feedback } });
608
+ } catch {
609
+ return new APIResponse({ status: 500, error: 'scorer not available' });
610
+ }
611
+ }
612
+
613
+ async function handleLeaderboard() {
614
+ try {
615
+ const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
616
+ const lb = new SkillLeaderboard();
617
+ const top = lb.getTopSkills(20);
618
+ const result = {
619
+ top_skills: (top || []).map((e) => ({
620
+ rank: e.rank,
621
+ path: e.skillPath,
622
+ score: e.score,
623
+ invocations: e.invocations,
624
+ trend: e.trend,
625
+ })),
626
+ total_tracked: top?.length || 0,
627
+ };
628
+ lb.close();
629
+ return new APIResponse({ data: result });
630
+ } catch {
631
+ return new APIResponse({ data: { top_skills: [], total_tracked: 0 } });
632
+ }
633
+ }
634
+
635
+ async function handleLeaderboardDashboard() {
636
+ try {
637
+ const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
638
+ const lb = new SkillLeaderboard();
639
+ const dashboard = lb.getDashboard();
640
+ lb.close();
641
+ return new APIResponse({ data: dashboard });
642
+ } catch {
643
+ return new APIResponse({ data: {} });
644
+ }
645
+ }
646
+
647
+ async function handleWorkflow({ body }) {
648
+ try {
649
+ const { WorkflowGenerator } = await import('../pipeline/github-actions.js');
650
+ const gen = new WorkflowGenerator();
651
+ const profile = body.profile || 'standard';
652
+ const yaml = gen.generateWorkflow({ scanProfile: profile });
653
+ return new APIResponse({ data: { profile, workflow: yaml } });
654
+ } catch {
655
+ return new APIResponse({ status: 500, error: 'workflow generator not available' });
656
+ }
657
+ }
658
+
659
+ // -- Server creation -------------------------------------------------------
660
+
661
+ const server = httpCreateServer(handleRequest);
662
+
663
+ return {
664
+ server,
665
+ config,
666
+ auth,
667
+ rateLimiter,
668
+ /** Start listening. Returns the actual port (useful with port 0). */
669
+ start() {
670
+ return new Promise((resolve, reject) => {
671
+ server.listen(config.port, config.host, () => {
672
+ const addr = server.address();
673
+ resolve(addr.port);
674
+ });
675
+ server.once('error', reject);
676
+ });
677
+ },
678
+ /** Stop the server. */
679
+ stop() {
680
+ return new Promise((resolve) => {
681
+ server.close(() => resolve());
682
+ });
683
+ },
684
+ };
685
+ }