@mrciphersmith/keryx 0.4.4 → 0.4.6

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.
@@ -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 = applyMasks(req.headers, opts.masks ?? [], target.hostname);
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
- return hit.port;
366
- const leaf = await ca.issueLeaf(host);
367
- const server = https.createServer({ key: leaf.keyPem, cert: leaf.certPem }, makeMitmHandler(host, targetPort));
368
- await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
369
- const addr = server.address();
370
- const port = addr && typeof addr === "object" ? addr.port : 0;
371
- mitmServers.set(key, { server, port });
372
- return port;
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
- clientSocket.pipe(internal);
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
- clientSocket.pipe(upstream);
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 new Promise((resolve) => server.listen(opts.unixSocketPath, () => resolve()));
557
+ await listenOrReject(server, () => server.listen(opts.unixSocketPath));
465
558
  } else {
466
- await new Promise((resolve) => server.listen(opts.port ?? 0, host, () => resolve()));
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
- await Promise.all([...mitmServers.values()].map(({ server: s }) => new Promise((resolve) => s.close(() => resolve()))));
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
- return {
566
- caCertPem,
567
- async issueLeaf(host) {
568
- const key = host.toLowerCase();
569
- const hit = cache.get(key);
570
- if (hit)
571
- return hit;
572
- const safe = key.replace(/[^a-z0-9.-]/g, "_");
573
- const leafKey = path.join(root, `${safe}.key`);
574
- const leafCsr = path.join(root, `${safe}.csr`);
575
- const leafCrt = path.join(root, `${safe}.crt`);
576
- const leafCfg = path.join(root, `${safe}.cnf`);
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
- "-CAcreateserial",
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
- const issued = {
734
+ return {
611
735
  certPem: await readFile(leafCrt, "utf8"),
612
736
  keyPem: await readFile(leafKey, "utf8")
613
737
  };
614
- cache.set(key, issued);
615
- return issued;
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.4",
3
+ "version": "0.4.6",
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": {