@mrciphersmith/keryx 0.4.5 → 0.4.7
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/dist/cli.js +9529 -5532
- package/dist/core.js +1741 -518
- package/dist/proxy-worker.js +187 -31
- package/package.json +1 -1
package/dist/proxy-worker.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parentPort, workerData } from "worker_threads";
|
|
|
6
6
|
import http from "http";
|
|
7
7
|
import https from "https";
|
|
8
8
|
import net from "net";
|
|
9
|
+
import { PassThrough } from "stream";
|
|
9
10
|
import dns from "dns";
|
|
10
11
|
import { unlinkSync } from "fs";
|
|
11
12
|
|
|
@@ -18,6 +19,21 @@ var HARD_DENY_RISKS = new Set([
|
|
|
18
19
|
]);
|
|
19
20
|
|
|
20
21
|
// src/harness/mutation/guard.ts
|
|
22
|
+
var ADDR_START = "(?<![\\w.-])";
|
|
23
|
+
var PRIVATE_EGRESS_PATTERNS = [
|
|
24
|
+
/localhost/i,
|
|
25
|
+
/::1(?![0-9a-f])/i,
|
|
26
|
+
new RegExp(`${ADDR_START}127\\.\\d`),
|
|
27
|
+
/(?<!\d)2130706433(?!\d)/,
|
|
28
|
+
/0x0*7f/i,
|
|
29
|
+
/(?<!\d)0177\./,
|
|
30
|
+
new RegExp(`${ADDR_START}169\\.254\\.`),
|
|
31
|
+
new RegExp(`${ADDR_START}10\\.\\d`),
|
|
32
|
+
new RegExp(`${ADDR_START}172\\.(1[6-9]|2\\d|3[01])\\.`),
|
|
33
|
+
new RegExp(`${ADDR_START}192\\.168\\.`),
|
|
34
|
+
new RegExp(`${ADDR_START}0\\.0\\.0\\.0(?!\\d)`),
|
|
35
|
+
new RegExp(`${ADDR_START}100\\.(6[4-9]|[7-9]\\d|1[01]\\d|12[0-7])\\.`)
|
|
36
|
+
];
|
|
21
37
|
var MAX_U32 = 4294967295;
|
|
22
38
|
function u32ToOctets(n) {
|
|
23
39
|
return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
|
|
@@ -147,6 +163,7 @@ function decodeEncodedIPv4(candidate) {
|
|
|
147
163
|
var ENV_DUMP_ARGV = new Set(["env", "printenv"]);
|
|
148
164
|
|
|
149
165
|
// src/harness/process/sandbox/proxy.ts
|
|
166
|
+
var MAX_TERMINATORS = 64;
|
|
150
167
|
function normalizeHost(host) {
|
|
151
168
|
return host.toLowerCase().replace(/\.$/, "");
|
|
152
169
|
}
|
|
@@ -226,6 +243,17 @@ function applyMasks(headers, masks, hostname) {
|
|
|
226
243
|
}
|
|
227
244
|
return out;
|
|
228
245
|
}
|
|
246
|
+
function carriesSentinel(headers, masks, hostname) {
|
|
247
|
+
const applicable = masks.filter((m) => m.sentinel.length > 0 && matchesAllowlist(hostname, m.injectHosts));
|
|
248
|
+
if (applicable.length === 0)
|
|
249
|
+
return false;
|
|
250
|
+
for (const value of Object.values(headers)) {
|
|
251
|
+
const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
252
|
+
if (values.some((v) => applicable.some((m) => v.includes(m.sentinel))))
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
229
257
|
function httpTarget(req) {
|
|
230
258
|
const raw = req.url ?? "";
|
|
231
259
|
try {
|
|
@@ -253,6 +281,17 @@ function httpTarget(req) {
|
|
|
253
281
|
}
|
|
254
282
|
return { kind: "no-target" };
|
|
255
283
|
}
|
|
284
|
+
function listenOrReject(server, listen) {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const onError = (error) => reject(error);
|
|
287
|
+
server.once("error", onError);
|
|
288
|
+
server.once("listening", () => {
|
|
289
|
+
server.off("error", onError);
|
|
290
|
+
resolve();
|
|
291
|
+
});
|
|
292
|
+
listen();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
256
295
|
async function createAllowlistProxy(opts) {
|
|
257
296
|
const host = opts.host ?? "127.0.0.1";
|
|
258
297
|
const allowed = opts.allowedDomains;
|
|
@@ -287,9 +326,13 @@ async function createAllowlistProxy(opts) {
|
|
|
287
326
|
refuse(`port ${target.port} is not allowed`);
|
|
288
327
|
return;
|
|
289
328
|
}
|
|
329
|
+
if (carriesSentinel(req.headers, opts.masks ?? [], target.hostname)) {
|
|
330
|
+
refuse("credential sentinel over plaintext HTTP");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
290
333
|
const proceed = (connectHost) => {
|
|
291
334
|
decide({ host: hostname, allowed: true, kind: "http", port: target.port });
|
|
292
|
-
const headers =
|
|
335
|
+
const headers = req.headers;
|
|
293
336
|
const upstream = trySync(() => http.request({ host: connectHost, port: target.port, method: req.method, path: pathFromUrl(req.url), headers: { ...headers, host: target.hostname } }, (up) => {
|
|
294
337
|
res.writeHead(up.statusCode ?? 502, up.headers);
|
|
295
338
|
up.pipe(res);
|
|
@@ -357,19 +400,46 @@ async function createAllowlistProxy(opts) {
|
|
|
357
400
|
req.pipe(upstreamReq);
|
|
358
401
|
};
|
|
359
402
|
const mitmServers = new Map;
|
|
403
|
+
const closeTerminator = (entry) => entry.then(({ server: s }) => new Promise((resolve) => s.close(() => resolve())), () => {
|
|
404
|
+
return;
|
|
405
|
+
});
|
|
360
406
|
const mitmPortFor = async (hostname, targetPort, ca) => {
|
|
361
407
|
const host = normalizeHost(hostname);
|
|
362
408
|
const key = `${host}:${targetPort}`;
|
|
363
409
|
const hit = mitmServers.get(key);
|
|
364
|
-
if (hit)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
410
|
+
if (hit) {
|
|
411
|
+
mitmServers.delete(key);
|
|
412
|
+
mitmServers.set(key, hit);
|
|
413
|
+
return (await hit).port;
|
|
414
|
+
}
|
|
415
|
+
const created = (async () => {
|
|
416
|
+
const leaf = await ca.issueLeaf(host);
|
|
417
|
+
const terminator = https.createServer({ key: leaf.keyPem, cert: leaf.certPem }, makeMitmHandler(host, targetPort));
|
|
418
|
+
await new Promise((resolve, reject) => {
|
|
419
|
+
terminator.once("error", reject);
|
|
420
|
+
terminator.listen(0, "127.0.0.1", () => {
|
|
421
|
+
terminator.off("error", reject);
|
|
422
|
+
resolve();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
const addr = terminator.address();
|
|
426
|
+
return { server: terminator, port: addr && typeof addr === "object" ? addr.port : 0 };
|
|
427
|
+
})();
|
|
428
|
+
mitmServers.set(key, created);
|
|
429
|
+
created.catch(() => {
|
|
430
|
+
if (mitmServers.get(key) === created)
|
|
431
|
+
mitmServers.delete(key);
|
|
432
|
+
});
|
|
433
|
+
while (mitmServers.size > MAX_TERMINATORS) {
|
|
434
|
+
const oldest = mitmServers.keys().next().value;
|
|
435
|
+
if (oldest === undefined || oldest === key)
|
|
436
|
+
break;
|
|
437
|
+
const evicted = mitmServers.get(oldest);
|
|
438
|
+
mitmServers.delete(oldest);
|
|
439
|
+
if (evicted)
|
|
440
|
+
closeTerminator(evicted);
|
|
441
|
+
}
|
|
442
|
+
return (await created).port;
|
|
373
443
|
};
|
|
374
444
|
server.on("connect", (req, clientSocket, head) => {
|
|
375
445
|
const url = req.url ?? "";
|
|
@@ -397,24 +467,41 @@ async function createAllowlistProxy(opts) {
|
|
|
397
467
|
refuse(`port ${port} is not allowed`);
|
|
398
468
|
return;
|
|
399
469
|
}
|
|
470
|
+
const pending = new PassThrough;
|
|
471
|
+
clientSocket.pipe(pending);
|
|
472
|
+
const onPendingEnd = () => {
|
|
473
|
+
clientSocket.destroy();
|
|
474
|
+
};
|
|
475
|
+
clientSocket.once("end", onPendingEnd);
|
|
476
|
+
clientSocket.once("close", () => pending.destroy());
|
|
400
477
|
const proceed = (connectHost) => {
|
|
478
|
+
if (clientSocket.destroyed)
|
|
479
|
+
return;
|
|
401
480
|
decide({ host: hostname, allowed: true, kind: "connect", port });
|
|
402
481
|
const ca = opts.tlsTerminate;
|
|
403
482
|
if (ca) {
|
|
404
483
|
(async () => {
|
|
405
484
|
try {
|
|
406
485
|
const terminatorPort = await mitmPortFor(hostname, port, ca);
|
|
486
|
+
if (clientSocket.destroyed)
|
|
487
|
+
return;
|
|
407
488
|
clientSocket.write(`HTTP/1.1 200 Connection Established\r
|
|
408
489
|
\r
|
|
409
490
|
`);
|
|
410
491
|
const internal = net.connect(terminatorPort, "127.0.0.1", () => {
|
|
492
|
+
if (clientSocket.destroyed) {
|
|
493
|
+
internal.destroy();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
clientSocket.off("end", onPendingEnd);
|
|
411
497
|
if (head && head.length > 0)
|
|
412
498
|
internal.write(head);
|
|
413
|
-
|
|
499
|
+
pending.pipe(internal);
|
|
414
500
|
internal.pipe(clientSocket);
|
|
415
501
|
});
|
|
416
502
|
internal.on("error", () => clientSocket.destroy());
|
|
417
503
|
clientSocket.on("error", () => internal.destroy());
|
|
504
|
+
clientSocket.on("close", () => internal.destroy());
|
|
418
505
|
} catch {
|
|
419
506
|
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r
|
|
420
507
|
\r
|
|
@@ -425,13 +512,18 @@ async function createAllowlistProxy(opts) {
|
|
|
425
512
|
return;
|
|
426
513
|
}
|
|
427
514
|
const onUpstreamConnect = (upstream) => {
|
|
515
|
+
if (clientSocket.destroyed) {
|
|
516
|
+
upstream.destroy();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
clientSocket.off("end", onPendingEnd);
|
|
428
520
|
clientSocket.write(`HTTP/1.1 200 Connection Established\r
|
|
429
521
|
\r
|
|
430
522
|
`);
|
|
431
523
|
if (head && head.length > 0)
|
|
432
524
|
upstream.write(head);
|
|
433
525
|
upstream.pipe(clientSocket);
|
|
434
|
-
|
|
526
|
+
pending.pipe(upstream);
|
|
435
527
|
};
|
|
436
528
|
const onUpstreamFailure = () => {
|
|
437
529
|
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r
|
|
@@ -445,6 +537,7 @@ async function createAllowlistProxy(opts) {
|
|
|
445
537
|
upstream.once("connect", () => onUpstreamConnect(upstream));
|
|
446
538
|
upstream.on("error", onUpstreamFailure);
|
|
447
539
|
clientSocket.on("error", () => upstream.destroy());
|
|
540
|
+
clientSocket.on("close", () => upstream.destroy());
|
|
448
541
|
};
|
|
449
542
|
if (!hardened) {
|
|
450
543
|
proceed(hostname);
|
|
@@ -461,9 +554,9 @@ async function createAllowlistProxy(opts) {
|
|
|
461
554
|
try {
|
|
462
555
|
unlinkSync(opts.unixSocketPath);
|
|
463
556
|
} catch {}
|
|
464
|
-
await
|
|
557
|
+
await listenOrReject(server, () => server.listen(opts.unixSocketPath));
|
|
465
558
|
} else {
|
|
466
|
-
await
|
|
559
|
+
await listenOrReject(server, () => server.listen(opts.port ?? 0, host));
|
|
467
560
|
}
|
|
468
561
|
const addr = server.address();
|
|
469
562
|
const port = addr && typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
@@ -473,8 +566,9 @@ async function createAllowlistProxy(opts) {
|
|
|
473
566
|
...opts.unixSocketPath !== undefined ? { unixSocketPath: opts.unixSocketPath } : {},
|
|
474
567
|
close: async () => {
|
|
475
568
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
476
|
-
|
|
569
|
+
const terminators = [...mitmServers.values()];
|
|
477
570
|
mitmServers.clear();
|
|
571
|
+
await Promise.all(terminators.map(closeTerminator));
|
|
478
572
|
}
|
|
479
573
|
};
|
|
480
574
|
}
|
|
@@ -491,9 +585,12 @@ function pathFromUrl(url) {
|
|
|
491
585
|
|
|
492
586
|
// src/harness/process/sandbox/tls-ca.ts
|
|
493
587
|
import { spawn } from "child_process";
|
|
588
|
+
import { randomBytes } from "crypto";
|
|
494
589
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
495
590
|
import { tmpdir } from "os";
|
|
496
591
|
import path from "path";
|
|
592
|
+
var MAX_CACHED_LEAVES = 64;
|
|
593
|
+
var MAX_CONCURRENT_ISSUANCES = 4;
|
|
497
594
|
function run(bin, args) {
|
|
498
595
|
return new Promise((resolve, reject) => {
|
|
499
596
|
const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -505,6 +602,12 @@ function run(bin, args) {
|
|
|
505
602
|
proc.on("close", (code) => code === 0 ? resolve(out) : reject(new Error(`${bin} ${args[0]} failed (exit ${code}): ${err.trim()}`)));
|
|
506
603
|
});
|
|
507
604
|
}
|
|
605
|
+
function randomSerial() {
|
|
606
|
+
const bytes = randomBytes(16);
|
|
607
|
+
bytes[0] = (bytes[0] ?? 0) & 127;
|
|
608
|
+
bytes[15] = (bytes[15] ?? 0) | 1;
|
|
609
|
+
return `0x${bytes.toString("hex")}`;
|
|
610
|
+
}
|
|
508
611
|
var CA_CONFIG = `[req]
|
|
509
612
|
distinguished_name = dn
|
|
510
613
|
x509_extensions = v3_ca
|
|
@@ -562,18 +665,38 @@ async function createRunCa(options = {}) {
|
|
|
562
665
|
]);
|
|
563
666
|
const caCertPem = await readFile(caCert, "utf8");
|
|
564
667
|
const cache = new Map;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
668
|
+
const inflight = new Map;
|
|
669
|
+
let running = 0;
|
|
670
|
+
const waiting = [];
|
|
671
|
+
const acquire = () => {
|
|
672
|
+
if (running < MAX_CONCURRENT_ISSUANCES) {
|
|
673
|
+
running += 1;
|
|
674
|
+
return Promise.resolve();
|
|
675
|
+
}
|
|
676
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
677
|
+
};
|
|
678
|
+
const release = () => {
|
|
679
|
+
const next = waiting.shift();
|
|
680
|
+
if (next)
|
|
681
|
+
next();
|
|
682
|
+
else
|
|
683
|
+
running -= 1;
|
|
684
|
+
};
|
|
685
|
+
const issueLimited = async (key) => {
|
|
686
|
+
await acquire();
|
|
687
|
+
try {
|
|
688
|
+
return await issue(key);
|
|
689
|
+
} finally {
|
|
690
|
+
release();
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
const issue = async (key) => {
|
|
694
|
+
const work = await mkdtemp(path.join(root, "leaf-"));
|
|
695
|
+
try {
|
|
696
|
+
const leafKey = path.join(work, "leaf.key");
|
|
697
|
+
const leafCsr = path.join(work, "leaf.csr");
|
|
698
|
+
const leafCrt = path.join(work, "leaf.crt");
|
|
699
|
+
const leafCfg = path.join(work, "leaf.cnf");
|
|
577
700
|
await writeFile(leafCfg, leafConfig(key), { mode: 384 });
|
|
578
701
|
await run(openssl, [
|
|
579
702
|
"req",
|
|
@@ -597,7 +720,8 @@ async function createRunCa(options = {}) {
|
|
|
597
720
|
caCert,
|
|
598
721
|
"-CAkey",
|
|
599
722
|
caKey,
|
|
600
|
-
"-
|
|
723
|
+
"-set_serial",
|
|
724
|
+
randomSerial(),
|
|
601
725
|
"-out",
|
|
602
726
|
leafCrt,
|
|
603
727
|
"-days",
|
|
@@ -607,15 +731,47 @@ async function createRunCa(options = {}) {
|
|
|
607
731
|
"-extensions",
|
|
608
732
|
"v3_req"
|
|
609
733
|
]);
|
|
610
|
-
|
|
734
|
+
return {
|
|
611
735
|
certPem: await readFile(leafCrt, "utf8"),
|
|
612
736
|
keyPem: await readFile(leafKey, "utf8")
|
|
613
737
|
};
|
|
614
|
-
|
|
615
|
-
|
|
738
|
+
} finally {
|
|
739
|
+
await rm(work, { recursive: true, force: true });
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
caCertPem,
|
|
744
|
+
issueLeaf(host) {
|
|
745
|
+
const key = host.toLowerCase();
|
|
746
|
+
const hit = cache.get(key);
|
|
747
|
+
if (hit) {
|
|
748
|
+
cache.delete(key);
|
|
749
|
+
cache.set(key, hit);
|
|
750
|
+
return Promise.resolve(hit);
|
|
751
|
+
}
|
|
752
|
+
const pending = inflight.get(key);
|
|
753
|
+
if (pending)
|
|
754
|
+
return pending;
|
|
755
|
+
const started = issueLimited(key).then((issued) => {
|
|
756
|
+
cache.set(key, issued);
|
|
757
|
+
while (cache.size > MAX_CACHED_LEAVES) {
|
|
758
|
+
const oldest = cache.keys().next().value;
|
|
759
|
+
if (oldest === undefined)
|
|
760
|
+
break;
|
|
761
|
+
cache.delete(oldest);
|
|
762
|
+
}
|
|
763
|
+
inflight.delete(key);
|
|
764
|
+
return issued;
|
|
765
|
+
}, (error) => {
|
|
766
|
+
inflight.delete(key);
|
|
767
|
+
throw error;
|
|
768
|
+
});
|
|
769
|
+
inflight.set(key, started);
|
|
770
|
+
return started;
|
|
616
771
|
},
|
|
617
772
|
async dispose() {
|
|
618
773
|
cache.clear();
|
|
774
|
+
inflight.clear();
|
|
619
775
|
await rm(root, { recursive: true, force: true });
|
|
620
776
|
}
|
|
621
777
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|