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.
- package/bin/cipher.js +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- package/package.json +31 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CIPHER OpenAI-compatible completion proxy.
|
|
6
|
+
*
|
|
7
|
+
* Intercepts chat completion requests, injects relevant security skills
|
|
8
|
+
* as system context, forwards to the upstream LLM, and logs interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer as httpCreateServer, request as httpRequest } from 'node:http';
|
|
12
|
+
import { URL } from 'node:url';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Proxy config
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export class ProxyConfig {
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.upstreamUrl = opts.upstreamUrl || process.env.CIPHER_UPSTREAM_URL || 'http://localhost:11434/v1';
|
|
23
|
+
this.host = opts.host || process.env.CIPHER_PROXY_HOST || '127.0.0.1';
|
|
24
|
+
this.port = opts.port ?? parseInt(process.env.CIPHER_PROXY_PORT || '8100', 10);
|
|
25
|
+
this.injectSkills = opts.injectSkills ?? true;
|
|
26
|
+
this.maxSkills = opts.maxSkills ?? 3;
|
|
27
|
+
this.logInteractions = opts.logInteractions ?? true;
|
|
28
|
+
this.modelOverride = opts.modelOverride || '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Skill injection
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Security keyword → domain mapping for skill matching. */
|
|
37
|
+
const KEYWORD_DOMAINS = {
|
|
38
|
+
xss: ['web-application-security', 'browser-security'],
|
|
39
|
+
'sql injection': ['web-application-security', 'database-security'],
|
|
40
|
+
sqli: ['web-application-security', 'database-security'],
|
|
41
|
+
'privilege escalation': ['linux-security', 'windows-security'],
|
|
42
|
+
privesc: ['linux-security', 'windows-security'],
|
|
43
|
+
'active directory': ['active-directory-security'],
|
|
44
|
+
kerberos: ['active-directory-security'],
|
|
45
|
+
malware: ['malware-analysis'],
|
|
46
|
+
'reverse engineer': ['reverse-engineering'],
|
|
47
|
+
incident: ['incident-response'],
|
|
48
|
+
forensic: ['digital-forensics', 'cloud-forensics'],
|
|
49
|
+
osint: ['osint-techniques'],
|
|
50
|
+
reconnaissance: ['osint-techniques', 'network-security'],
|
|
51
|
+
container: ['container-security'],
|
|
52
|
+
docker: ['container-security'],
|
|
53
|
+
kubernetes: ['container-security'],
|
|
54
|
+
cloud: ['cloud-security'],
|
|
55
|
+
aws: ['cloud-security'],
|
|
56
|
+
azure: ['cloud-security'],
|
|
57
|
+
api: ['api-security'],
|
|
58
|
+
cryptograph: ['cryptography'],
|
|
59
|
+
encrypt: ['cryptography'],
|
|
60
|
+
phishing: ['social-engineering'],
|
|
61
|
+
exploit: ['exploit-development', 'binary-exploitation'],
|
|
62
|
+
'buffer overflow': ['binary-exploitation'],
|
|
63
|
+
rop: ['binary-exploitation'],
|
|
64
|
+
c2: ['c2-frameworks'],
|
|
65
|
+
'command and control': ['c2-frameworks'],
|
|
66
|
+
siem: ['log-analysis'],
|
|
67
|
+
detection: ['log-analysis', 'threat-intelligence'],
|
|
68
|
+
sigma: ['log-analysis'],
|
|
69
|
+
'threat intel': ['threat-intelligence'],
|
|
70
|
+
'zero trust': ['zero-trust'],
|
|
71
|
+
compliance: ['compliance-auditing'],
|
|
72
|
+
gdpr: ['privacy-engineering'],
|
|
73
|
+
privacy: ['privacy-engineering'],
|
|
74
|
+
network: ['network-security'],
|
|
75
|
+
firewall: ['network-security'],
|
|
76
|
+
wireless: ['wireless-security'],
|
|
77
|
+
wifi: ['wireless-security'],
|
|
78
|
+
mobile: ['mobile-security'],
|
|
79
|
+
android: ['mobile-security'],
|
|
80
|
+
ios: ['mobile-security'],
|
|
81
|
+
'supply chain': ['supply-chain-security'],
|
|
82
|
+
password: ['password-cracking'],
|
|
83
|
+
'brute force': ['password-cracking'],
|
|
84
|
+
'bug bounty': ['bug-bounty'],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find matching CIPHER skills for the conversation.
|
|
89
|
+
* @param {object[]} messages
|
|
90
|
+
* @param {number} [maxSkills=3]
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function findMatchingSkills(messages, maxSkills = 3) {
|
|
94
|
+
const skillsRoot = resolve(__dirname_workaround(), '..', '..', 'skills');
|
|
95
|
+
if (!existsSync(skillsRoot)) return '';
|
|
96
|
+
|
|
97
|
+
const userText = messages
|
|
98
|
+
.filter((m) => m.role === 'user' && typeof m.content === 'string')
|
|
99
|
+
.map((m) => m.content)
|
|
100
|
+
.join(' ')
|
|
101
|
+
.toLowerCase();
|
|
102
|
+
|
|
103
|
+
if (!userText) return '';
|
|
104
|
+
|
|
105
|
+
const matchedDomains = [];
|
|
106
|
+
for (const [keyword, domains] of Object.entries(KEYWORD_DOMAINS)) {
|
|
107
|
+
if (userText.includes(keyword)) {
|
|
108
|
+
for (const d of domains) {
|
|
109
|
+
if (!matchedDomains.includes(d)) matchedDomains.push(d);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!matchedDomains.length) return '';
|
|
115
|
+
|
|
116
|
+
const injections = [];
|
|
117
|
+
for (const domain of matchedDomains.slice(0, maxSkills)) {
|
|
118
|
+
const domainDir = join(skillsRoot, domain);
|
|
119
|
+
if (!existsSync(domainDir)) continue;
|
|
120
|
+
|
|
121
|
+
const skillMd = join(domainDir, 'SKILL.md');
|
|
122
|
+
if (existsSync(skillMd)) {
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync(skillMd, 'utf8');
|
|
125
|
+
const lines = content.split('\n');
|
|
126
|
+
const descLines = [];
|
|
127
|
+
let inBody = false;
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (inBody) {
|
|
130
|
+
if (line.trim() === '' && descLines.length) break;
|
|
131
|
+
if (line.trim().startsWith('#')) {
|
|
132
|
+
if (descLines.length) break;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
descLines.push(line.trim());
|
|
136
|
+
} else if (line.trim().startsWith('# ') || line.trim() === '---') {
|
|
137
|
+
inBody = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (descLines.length) {
|
|
141
|
+
injections.push(`[CIPHER/${domain}] ${descLines.slice(0, 3).join(' ')}`);
|
|
142
|
+
}
|
|
143
|
+
} catch { /* skip */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const techDir = join(domainDir, 'techniques');
|
|
147
|
+
if (existsSync(techDir)) {
|
|
148
|
+
try {
|
|
149
|
+
const techniques = readdirSync(techDir)
|
|
150
|
+
.filter((t) => { try { return statSync(join(techDir, t)).isDirectory(); } catch { return false; } })
|
|
151
|
+
.sort()
|
|
152
|
+
.slice(0, 8);
|
|
153
|
+
if (techniques.length) {
|
|
154
|
+
injections.push(` Available techniques: ${techniques.map((t) => t.replace(/-/g, ' ')).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
} catch { /* skip */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!injections.length) return '';
|
|
161
|
+
|
|
162
|
+
return `\n\n--- CIPHER Security Context ---\n${injections.join('\n')}\n--- End CIPHER Context ---\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Resolve dirname equivalent for ESM. */
|
|
166
|
+
function __dirname_workaround() {
|
|
167
|
+
try {
|
|
168
|
+
return new URL('.', import.meta.url).pathname;
|
|
169
|
+
} catch {
|
|
170
|
+
return process.cwd();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Proxy server
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create an OpenAI-compatible proxy server.
|
|
180
|
+
* @param {ProxyConfig} [config]
|
|
181
|
+
* @returns {{ server: import('node:http').Server, config: ProxyConfig, start: () => Promise<number>, stop: () => Promise<void> }}
|
|
182
|
+
*/
|
|
183
|
+
export function createOpenAIProxy(config) {
|
|
184
|
+
config = config instanceof ProxyConfig ? config : new ProxyConfig(config);
|
|
185
|
+
|
|
186
|
+
function setCors(res) {
|
|
187
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
188
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
189
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function respond(res, status, data) {
|
|
193
|
+
const body = JSON.stringify(data);
|
|
194
|
+
setCors(res);
|
|
195
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
196
|
+
res.end(body);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readBody(req) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const chunks = [];
|
|
202
|
+
let size = 0;
|
|
203
|
+
req.on('data', (chunk) => {
|
|
204
|
+
size += chunk.length;
|
|
205
|
+
if (size > 10 * 1024 * 1024) { resolve(null); req.destroy(); return; }
|
|
206
|
+
chunks.push(chunk);
|
|
207
|
+
});
|
|
208
|
+
req.on('end', () => {
|
|
209
|
+
if (size === 0) return resolve(null);
|
|
210
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
211
|
+
catch { resolve(null); }
|
|
212
|
+
});
|
|
213
|
+
req.on('error', () => resolve(null));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function forwardPost(path, body, headers) {
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
const upstream = new URL(config.upstreamUrl.replace(/\/+$/, '') + path);
|
|
220
|
+
const data = JSON.stringify(body);
|
|
221
|
+
const reqHeaders = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) };
|
|
222
|
+
if (headers?.authorization) reqHeaders.Authorization = headers.authorization;
|
|
223
|
+
|
|
224
|
+
const req = httpRequest(
|
|
225
|
+
{ hostname: upstream.hostname, port: upstream.port, path: upstream.pathname, method: 'POST', headers: reqHeaders, timeout: 120000 },
|
|
226
|
+
(res) => {
|
|
227
|
+
const chunks = [];
|
|
228
|
+
res.on('data', (c) => chunks.push(c));
|
|
229
|
+
res.on('end', () => {
|
|
230
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString()) }); }
|
|
231
|
+
catch { resolve({ status: res.statusCode, body: { error: 'invalid upstream response' } }); }
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
req.on('error', reject);
|
|
236
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('upstream timeout')); });
|
|
237
|
+
req.write(data);
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function forwardStream(path, body, headers, res) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const upstream = new URL(config.upstreamUrl.replace(/\/+$/, '') + path);
|
|
245
|
+
const data = JSON.stringify(body);
|
|
246
|
+
const reqHeaders = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) };
|
|
247
|
+
if (headers?.authorization) reqHeaders.Authorization = headers.authorization;
|
|
248
|
+
|
|
249
|
+
const req = httpRequest(
|
|
250
|
+
{ hostname: upstream.hostname, port: upstream.port, path: upstream.pathname, method: 'POST', headers: reqHeaders, timeout: 120000 },
|
|
251
|
+
(upstreamRes) => {
|
|
252
|
+
setCors(res);
|
|
253
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
|
|
254
|
+
upstreamRes.on('data', (chunk) => { res.write(chunk); });
|
|
255
|
+
upstreamRes.on('end', () => { res.end(); resolve(); });
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
req.on('error', (err) => { respond(res, 502, { error: 'upstream stream failed' }); reject(err); });
|
|
259
|
+
req.write(data);
|
|
260
|
+
req.end();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function logInteraction(messages, result) {
|
|
265
|
+
try {
|
|
266
|
+
const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
|
|
267
|
+
const lb = new SkillLeaderboard();
|
|
268
|
+
const sysMsg = messages.find((m) => m.role === 'system')?.content || '';
|
|
269
|
+
const domains = [...sysMsg.matchAll(/\[CIPHER\/([^\]]+)\]/g)].map((m) => m[1]);
|
|
270
|
+
const usage = result?.usage || {};
|
|
271
|
+
const completionTokens = usage.completion_tokens || 0;
|
|
272
|
+
const score = Math.min(0.9, 0.3 + completionTokens / 700);
|
|
273
|
+
for (const domain of domains) {
|
|
274
|
+
lb.recordInvocation(`${domain}/proxy-invocation`, { success: true, score, context: { source: 'openai_proxy', model: result?.model || '' } });
|
|
275
|
+
}
|
|
276
|
+
lb.close();
|
|
277
|
+
} catch { /* logging is best-effort */ }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function handleRequest(req, res) {
|
|
281
|
+
setCors(res);
|
|
282
|
+
|
|
283
|
+
if (req.method === 'OPTIONS') {
|
|
284
|
+
res.writeHead(204);
|
|
285
|
+
res.end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
290
|
+
const path = url.pathname;
|
|
291
|
+
|
|
292
|
+
// GET routes
|
|
293
|
+
if (req.method === 'GET') {
|
|
294
|
+
if (path === '/health' || path === '/v1/health') {
|
|
295
|
+
return respond(res, 200, { status: 'ok', proxy: 'cipher' });
|
|
296
|
+
}
|
|
297
|
+
if (path === '/v1/models' || path === '/v1/models/') {
|
|
298
|
+
return respond(res, 200, {
|
|
299
|
+
object: 'list',
|
|
300
|
+
data: [{
|
|
301
|
+
id: config.modelOverride || 'cipher-proxy',
|
|
302
|
+
object: 'model',
|
|
303
|
+
created: Math.floor(Date.now() / 1000),
|
|
304
|
+
owned_by: 'cipher',
|
|
305
|
+
}],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return respond(res, 404, { error: 'not found' });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// POST routes
|
|
312
|
+
if (req.method === 'POST') {
|
|
313
|
+
const body = await readBody(req);
|
|
314
|
+
if (!body) return respond(res, 400, { error: 'invalid or empty body' });
|
|
315
|
+
|
|
316
|
+
if (path === '/v1/chat/completions') {
|
|
317
|
+
const messages = body.messages || [];
|
|
318
|
+
|
|
319
|
+
// Inject skills
|
|
320
|
+
if (config.injectSkills && messages.length) {
|
|
321
|
+
const skillContext = findMatchingSkills(messages, config.maxSkills);
|
|
322
|
+
if (skillContext) {
|
|
323
|
+
if (messages[0]?.role === 'system') {
|
|
324
|
+
messages[0].content = (messages[0].content || '') + skillContext;
|
|
325
|
+
} else {
|
|
326
|
+
messages.unshift({ role: 'system', content: skillContext });
|
|
327
|
+
}
|
|
328
|
+
body.messages = messages;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Model override
|
|
333
|
+
if (config.modelOverride) body.model = config.modelOverride;
|
|
334
|
+
|
|
335
|
+
// Forward
|
|
336
|
+
try {
|
|
337
|
+
if (body.stream) {
|
|
338
|
+
await forwardStream('/v1/chat/completions', body, req.headers, res);
|
|
339
|
+
} else {
|
|
340
|
+
const result = await forwardPost('/v1/chat/completions', body, req.headers);
|
|
341
|
+
respond(res, result.status, result.body);
|
|
342
|
+
if (config.logInteractions) logInteraction(messages, result.body);
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
respond(res, 502, { error: 'upstream unavailable' });
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (path === '/v1/completions') {
|
|
351
|
+
try {
|
|
352
|
+
const result = await forwardPost('/v1/completions', body, req.headers);
|
|
353
|
+
respond(res, result.status, result.body);
|
|
354
|
+
} catch {
|
|
355
|
+
respond(res, 502, { error: 'upstream unavailable' });
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return respond(res, 404, { error: 'not found' });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
respond(res, 405, { error: 'method not allowed' });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const server = httpCreateServer(handleRequest);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
server,
|
|
370
|
+
config,
|
|
371
|
+
start() {
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
server.listen(config.port, config.host, () => {
|
|
374
|
+
resolve(server.address().port);
|
|
375
|
+
});
|
|
376
|
+
server.once('error', reject);
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
stop() {
|
|
380
|
+
return new Promise((resolve) => { server.close(() => resolve()); });
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|