@openape/proxy 0.4.3 → 0.4.5
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/README.md +10 -0
- package/dist/index.cjs +560 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +538 -138
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.cjs
CHANGED
|
@@ -1,15 +1,130 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
|
|
4
26
|
// src/index.ts
|
|
27
|
+
var import_node_fs3 = require("fs");
|
|
5
28
|
var import_node_http = require("http");
|
|
29
|
+
var import_node_os = require("os");
|
|
30
|
+
var import_node_path2 = require("path");
|
|
6
31
|
var import_node_util = require("util");
|
|
7
32
|
|
|
8
|
-
// src/
|
|
33
|
+
// src/ca-store.ts
|
|
9
34
|
var import_node_fs = require("fs");
|
|
35
|
+
var import_node_path = require("path");
|
|
36
|
+
var import_node_forge = __toESM(require("node-forge"), 1);
|
|
37
|
+
function loadOrCreateCa(opts) {
|
|
38
|
+
if ((0, import_node_fs.existsSync)(opts.certPath) && (0, import_node_fs.existsSync)(opts.keyPath)) {
|
|
39
|
+
return {
|
|
40
|
+
certPem: (0, import_node_fs.readFileSync)(opts.certPath, "utf-8"),
|
|
41
|
+
keyPem: (0, import_node_fs.readFileSync)(opts.keyPath, "utf-8"),
|
|
42
|
+
created: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const keys = import_node_forge.default.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
46
|
+
const cert = import_node_forge.default.pki.createCertificate();
|
|
47
|
+
cert.publicKey = keys.publicKey;
|
|
48
|
+
cert.serialNumber = String(Date.now());
|
|
49
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
50
|
+
cert.validity.notAfter = new Date(Date.now() + 10 * 365 * 864e5);
|
|
51
|
+
const attrs = [
|
|
52
|
+
// @types/node-forge types valueTagClass as asn1.Class, but node-forge's
|
|
53
|
+
// x509.js reads it as an asn1.Type at runtime (see comment above). Cast to
|
|
54
|
+
// satisfy the too-narrow typing without changing runtime behaviour.
|
|
55
|
+
{ name: "commonName", value: opts.subjectCN, valueTagClass: import_node_forge.default.asn1.Type.UTF8 }
|
|
56
|
+
];
|
|
57
|
+
cert.setSubject(attrs);
|
|
58
|
+
cert.setIssuer(attrs);
|
|
59
|
+
cert.setExtensions([
|
|
60
|
+
{ name: "basicConstraints", cA: true },
|
|
61
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true },
|
|
62
|
+
{ name: "subjectKeyIdentifier" }
|
|
63
|
+
]);
|
|
64
|
+
cert.sign(keys.privateKey, import_node_forge.default.md.sha256.create());
|
|
65
|
+
const certPem = import_node_forge.default.pki.certificateToPem(cert);
|
|
66
|
+
const keyPem = import_node_forge.default.pki.privateKeyToPem(keys.privateKey);
|
|
67
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(opts.certPath), { recursive: true, mode: 448 });
|
|
68
|
+
(0, import_node_fs.writeFileSync)(opts.certPath, certPem, { mode: 384 });
|
|
69
|
+
(0, import_node_fs.writeFileSync)(opts.keyPath, keyPem, { mode: 384 });
|
|
70
|
+
return { certPem, keyPem, created: true };
|
|
71
|
+
}
|
|
72
|
+
function mintLeafCert(ca, hostname) {
|
|
73
|
+
const caCert = import_node_forge.default.pki.certificateFromPem(ca.certPem);
|
|
74
|
+
const caKey = import_node_forge.default.pki.privateKeyFromPem(ca.keyPem);
|
|
75
|
+
const keys = import_node_forge.default.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
76
|
+
const cert = import_node_forge.default.pki.createCertificate();
|
|
77
|
+
cert.publicKey = keys.publicKey;
|
|
78
|
+
cert.serialNumber = `${Date.now()}${Math.floor(Math.random() * 1e6)}`;
|
|
79
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
80
|
+
const expiresAt = Date.now() + 24 * 36e5;
|
|
81
|
+
cert.validity.notAfter = new Date(expiresAt);
|
|
82
|
+
cert.setSubject([
|
|
83
|
+
// See note in createCaCert: valueTagClass is an asn1.Type at runtime;
|
|
84
|
+
// cast around the too-narrow @types/node-forge annotation.
|
|
85
|
+
{ name: "commonName", value: hostname, valueTagClass: import_node_forge.default.asn1.Type.UTF8 }
|
|
86
|
+
]);
|
|
87
|
+
cert.setIssuer(caCert.subject.attributes);
|
|
88
|
+
cert.setExtensions([
|
|
89
|
+
{ name: "basicConstraints", cA: false },
|
|
90
|
+
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
|
91
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
92
|
+
{ name: "subjectAltName", altNames: [{ type: 2, value: hostname }] }
|
|
93
|
+
]);
|
|
94
|
+
cert.sign(caKey, import_node_forge.default.md.sha256.create());
|
|
95
|
+
return {
|
|
96
|
+
certPem: import_node_forge.default.pki.certificateToPem(cert),
|
|
97
|
+
keyPem: import_node_forge.default.pki.privateKeyToPem(keys.privateKey),
|
|
98
|
+
expiresAt
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function createLeafCertCache(ca, opts) {
|
|
102
|
+
const cache = /* @__PURE__ */ new Map();
|
|
103
|
+
return {
|
|
104
|
+
get(hostname) {
|
|
105
|
+
const existing = cache.get(hostname);
|
|
106
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
107
|
+
cache.delete(hostname);
|
|
108
|
+
cache.set(hostname, existing);
|
|
109
|
+
return existing;
|
|
110
|
+
}
|
|
111
|
+
const fresh = mintLeafCert(ca, hostname);
|
|
112
|
+
cache.set(hostname, fresh);
|
|
113
|
+
while (cache.size > opts.capacity) {
|
|
114
|
+
const oldestKey = cache.keys().next().value;
|
|
115
|
+
if (oldestKey === void 0) break;
|
|
116
|
+
cache.delete(oldestKey);
|
|
117
|
+
}
|
|
118
|
+
return fresh;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/config.ts
|
|
124
|
+
var import_node_fs2 = require("fs");
|
|
10
125
|
var import_smol_toml = require("smol-toml");
|
|
11
126
|
function loadMultiAgentConfig(path, overrides) {
|
|
12
|
-
const raw = (0,
|
|
127
|
+
const raw = (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
13
128
|
let parsed;
|
|
14
129
|
if (path.endsWith(".json")) {
|
|
15
130
|
parsed = JSON.parse(raw);
|
|
@@ -68,26 +183,26 @@ function matchesRule(rule, domain, method, path) {
|
|
|
68
183
|
}
|
|
69
184
|
return true;
|
|
70
185
|
}
|
|
71
|
-
function evaluateRules(
|
|
72
|
-
for (const rule of
|
|
186
|
+
function evaluateRules(config, domain, method, path) {
|
|
187
|
+
for (const rule of config.deny) {
|
|
73
188
|
if (matchesRule(rule, domain, method, path)) {
|
|
74
189
|
return { type: "deny", note: rule.note };
|
|
75
190
|
}
|
|
76
191
|
}
|
|
77
|
-
for (const rule of
|
|
192
|
+
for (const rule of config.allow) {
|
|
78
193
|
if (matchesRule(rule, domain, method, path)) {
|
|
79
194
|
return { type: "allow" };
|
|
80
195
|
}
|
|
81
196
|
}
|
|
82
|
-
for (const rule of
|
|
197
|
+
for (const rule of config.grant_required) {
|
|
83
198
|
if (matchesRule(rule, domain, method, path)) {
|
|
84
199
|
return { type: "grant_required", rule };
|
|
85
200
|
}
|
|
86
201
|
}
|
|
87
|
-
if (
|
|
202
|
+
if (config.proxy.default_action === "block") {
|
|
88
203
|
return { type: "deny", note: "No matching rule (default: block)" };
|
|
89
204
|
}
|
|
90
|
-
if (
|
|
205
|
+
if (config.proxy.default_action === "allow") {
|
|
91
206
|
return { type: "allow" };
|
|
92
207
|
}
|
|
93
208
|
return {
|
|
@@ -277,14 +392,14 @@ function isPrivateIP(ip) {
|
|
|
277
392
|
if ((0, import_node_net.isIP)(ip) === 6) return isPrivateIPv6(ip);
|
|
278
393
|
return false;
|
|
279
394
|
}
|
|
280
|
-
async function checkEgress(
|
|
281
|
-
if ((0, import_node_net.isIP)(
|
|
282
|
-
return isPrivateIP(
|
|
395
|
+
async function checkEgress(hostname) {
|
|
396
|
+
if ((0, import_node_net.isIP)(hostname)) {
|
|
397
|
+
return isPrivateIP(hostname) ? { kind: "private" } : { kind: "ok" };
|
|
283
398
|
}
|
|
284
|
-
if (
|
|
399
|
+
if (hostname === "localhost") return { kind: "private" };
|
|
285
400
|
let settled;
|
|
286
401
|
try {
|
|
287
|
-
settled = await Promise.allSettled([(0, import_promises.resolve4)(
|
|
402
|
+
settled = await Promise.allSettled([(0, import_promises.resolve4)(hostname), (0, import_promises.resolve6)(hostname)]);
|
|
288
403
|
} catch {
|
|
289
404
|
return { kind: "unresolvable", reason: "dns-error" };
|
|
290
405
|
}
|
|
@@ -300,17 +415,100 @@ async function checkEgress(hostname2) {
|
|
|
300
415
|
|
|
301
416
|
// src/connect.ts
|
|
302
417
|
var import_node_net2 = require("net");
|
|
303
|
-
|
|
418
|
+
|
|
419
|
+
// src/mitm-connect.ts
|
|
420
|
+
var import_node_tls = require("tls");
|
|
421
|
+
function handleMitmConnect(opts) {
|
|
422
|
+
const leaf = opts.leafCache.get(opts.host);
|
|
423
|
+
const ctx = (0, import_node_tls.createSecureContext)({ cert: leaf.certPem, key: leaf.keyPem });
|
|
424
|
+
const tls = new import_node_tls.TLSSocket(opts.clientSocket, { isServer: true, secureContext: ctx });
|
|
425
|
+
opts.clientSocket.resume();
|
|
426
|
+
let buffer = Buffer.alloc(0);
|
|
427
|
+
let dispatched = false;
|
|
428
|
+
const onData = (chunk) => {
|
|
429
|
+
if (dispatched) return;
|
|
430
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
431
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
432
|
+
if (headerEnd < 0) return;
|
|
433
|
+
const headerBlock = buffer.subarray(0, headerEnd).toString("utf-8");
|
|
434
|
+
const lines = headerBlock.split("\r\n");
|
|
435
|
+
const requestLine = lines[0] ?? "";
|
|
436
|
+
const [method, path] = requestLine.split(" ");
|
|
437
|
+
const headers = /* @__PURE__ */ new Map();
|
|
438
|
+
for (const line of lines.slice(1)) {
|
|
439
|
+
const i = line.indexOf(":");
|
|
440
|
+
if (i > 0) headers.set(line.slice(0, i).trim().toLowerCase(), line.slice(i + 1).trim());
|
|
441
|
+
}
|
|
442
|
+
const decision = opts.onRequest({
|
|
443
|
+
method: method ?? "",
|
|
444
|
+
host: opts.host,
|
|
445
|
+
path: path ?? "/",
|
|
446
|
+
headers,
|
|
447
|
+
// inject decisions use only target+headers; raw body bytes still flow through to upstream below
|
|
448
|
+
body: null
|
|
449
|
+
});
|
|
450
|
+
if (decision.type === "short-circuit") {
|
|
451
|
+
dispatched = true;
|
|
452
|
+
const body = Buffer.from(decision.body, "utf-8");
|
|
453
|
+
tls.write(`HTTP/1.1 ${decision.status} OK\r
|
|
454
|
+
Content-Length: ${body.length}\r
|
|
455
|
+
\r
|
|
456
|
+
`);
|
|
457
|
+
tls.write(body);
|
|
458
|
+
tls.end();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
dispatched = true;
|
|
462
|
+
tls.pause();
|
|
463
|
+
const upstream = (0, import_node_tls.connect)({
|
|
464
|
+
host: opts.host,
|
|
465
|
+
port: opts.port,
|
|
466
|
+
servername: opts.host,
|
|
467
|
+
rejectUnauthorized: opts.upstreamRejectUnauthorized ?? true
|
|
468
|
+
});
|
|
469
|
+
upstream.once("secureConnect", () => {
|
|
470
|
+
const mutated = decision.mutatedHeaders;
|
|
471
|
+
const newHeaderLines = [];
|
|
472
|
+
for (const line of lines.slice(1)) {
|
|
473
|
+
const i = line.indexOf(":");
|
|
474
|
+
if (i <= 0) continue;
|
|
475
|
+
const lower = line.slice(0, i).trim().toLowerCase();
|
|
476
|
+
if (mutated.has(lower)) continue;
|
|
477
|
+
newHeaderLines.push(line);
|
|
478
|
+
}
|
|
479
|
+
for (const [name, value] of mutated.entries()) {
|
|
480
|
+
newHeaderLines.push(`${capitalizeHeader(name)}: ${value}`);
|
|
481
|
+
}
|
|
482
|
+
const fullRequest = [requestLine, ...newHeaderLines, "", ""].join("\r\n");
|
|
483
|
+
upstream.write(fullRequest);
|
|
484
|
+
const remainder = buffer.subarray(headerEnd + 4);
|
|
485
|
+
if (remainder.length > 0) upstream.write(remainder);
|
|
486
|
+
upstream.pipe(tls);
|
|
487
|
+
tls.removeListener("data", onData);
|
|
488
|
+
tls.pipe(upstream);
|
|
489
|
+
tls.resume();
|
|
490
|
+
});
|
|
491
|
+
upstream.on("error", () => tls.end());
|
|
492
|
+
};
|
|
493
|
+
tls.on("data", onData);
|
|
494
|
+
tls.on("error", () => opts.clientSocket.destroy());
|
|
495
|
+
}
|
|
496
|
+
function capitalizeHeader(name) {
|
|
497
|
+
return name.split("-").map((p) => (p[0]?.toUpperCase() ?? "") + p.slice(1)).join("-");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/connect.ts
|
|
501
|
+
async function handleConnect(config, grantsClients, req, clientSocket, head, deps) {
|
|
304
502
|
const target = req.url ?? "";
|
|
305
503
|
const [host, portStr] = target.split(":");
|
|
306
|
-
const
|
|
307
|
-
if (!host || !
|
|
504
|
+
const port = Number.parseInt(portStr || "443");
|
|
505
|
+
if (!host || !port) {
|
|
308
506
|
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
309
507
|
clientSocket.destroy();
|
|
310
508
|
return;
|
|
311
509
|
}
|
|
312
510
|
const idpHosts = new Set(
|
|
313
|
-
|
|
511
|
+
config.agents.map((a) => {
|
|
314
512
|
try {
|
|
315
513
|
return new URL(a.idp_url).hostname.toLowerCase();
|
|
316
514
|
} catch {
|
|
@@ -328,46 +526,51 @@ async function handleConnect(config2, grantsClients, req, clientSocket, _head) {
|
|
|
328
526
|
path: target,
|
|
329
527
|
rule: "idp-system-bypass"
|
|
330
528
|
});
|
|
331
|
-
tunnel(host,
|
|
529
|
+
tunnel(host, port, clientSocket);
|
|
332
530
|
return;
|
|
333
531
|
}
|
|
334
|
-
const mandatoryAuth =
|
|
532
|
+
const mandatoryAuth = config.proxy.mandatory_auth ?? false;
|
|
533
|
+
const daemonMode = deps?.daemonMode ?? false;
|
|
335
534
|
let agentEmail;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
535
|
+
if (daemonMode) {
|
|
536
|
+
agentEmail = config.agents[0]?.email;
|
|
537
|
+
} else {
|
|
538
|
+
try {
|
|
539
|
+
const authHeader = req.headers["proxy-authorization"];
|
|
540
|
+
let identity = null;
|
|
541
|
+
for (const agentConf2 of config.agents) {
|
|
542
|
+
identity = await verifyAgentAuth(
|
|
543
|
+
authHeader ?? null,
|
|
544
|
+
agentConf2.idp_url,
|
|
545
|
+
mandatoryAuth && config.agents.length === 1
|
|
546
|
+
);
|
|
547
|
+
if (identity) break;
|
|
548
|
+
}
|
|
549
|
+
if (mandatoryAuth && !identity) {
|
|
550
|
+
throw new AuthError("JWT required");
|
|
551
|
+
}
|
|
552
|
+
agentEmail = identity?.email;
|
|
553
|
+
if (agentEmail) {
|
|
554
|
+
const known = config.agents.find((a) => a.email === agentEmail);
|
|
555
|
+
if (!known) {
|
|
556
|
+
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
557
|
+
clientSocket.destroy();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
} else if (config.agents.length > 1) {
|
|
561
|
+
throw new AuthError("JWT required for multi-agent proxy");
|
|
562
|
+
}
|
|
563
|
+
} catch (err) {
|
|
564
|
+
if (err instanceof AuthError) {
|
|
565
|
+
clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
355
566
|
clientSocket.destroy();
|
|
356
567
|
return;
|
|
357
568
|
}
|
|
358
|
-
|
|
359
|
-
throw new AuthError("JWT required for multi-agent proxy");
|
|
360
|
-
}
|
|
361
|
-
} catch (err) {
|
|
362
|
-
if (err instanceof AuthError) {
|
|
363
|
-
clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
364
|
-
clientSocket.destroy();
|
|
365
|
-
return;
|
|
569
|
+
throw err;
|
|
366
570
|
}
|
|
367
|
-
throw err;
|
|
368
571
|
}
|
|
369
572
|
const egress = await checkEgress(host);
|
|
370
|
-
const auditAgent = agentEmail ??
|
|
573
|
+
const auditAgent = agentEmail ?? config.agents[0]?.email ?? "unknown";
|
|
371
574
|
if (egress.kind === "private") {
|
|
372
575
|
writeAudit({
|
|
373
576
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -402,7 +605,7 @@ DNS lookup failed for ${host} (${egress.reason}).\r
|
|
|
402
605
|
clientSocket.destroy();
|
|
403
606
|
return;
|
|
404
607
|
}
|
|
405
|
-
const agentConf = agentEmail ?
|
|
608
|
+
const agentConf = agentEmail ? config.agents.find((a) => a.email === agentEmail) : config.agents[0];
|
|
406
609
|
if (!agentConf) {
|
|
407
610
|
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
408
611
|
clientSocket.destroy();
|
|
@@ -411,10 +614,10 @@ DNS lookup failed for ${host} (${egress.reason}).\r
|
|
|
411
614
|
const effectiveEmail = agentEmail ?? agentConf.email;
|
|
412
615
|
const rulesConfig = {
|
|
413
616
|
proxy: {
|
|
414
|
-
listen:
|
|
617
|
+
listen: config.proxy.listen,
|
|
415
618
|
idp_url: agentConf.idp_url,
|
|
416
619
|
agent_email: agentConf.email,
|
|
417
|
-
default_action:
|
|
620
|
+
default_action: config.proxy.default_action
|
|
418
621
|
},
|
|
419
622
|
allow: agentConf.allow ?? [],
|
|
420
623
|
deny: agentConf.deny ?? [],
|
|
@@ -455,7 +658,7 @@ Blocked:${note}\r
|
|
|
455
658
|
audience: "ape-proxy",
|
|
456
659
|
grantType: action.rule.grant_type,
|
|
457
660
|
permissions: action.rule.permissions,
|
|
458
|
-
reason: `CONNECT ${host}:${
|
|
661
|
+
reason: `CONNECT ${host}:${port}`,
|
|
459
662
|
duration: action.rule.duration
|
|
460
663
|
});
|
|
461
664
|
const decided = await grantsClient.waitForApproval(grant.id);
|
|
@@ -484,10 +687,40 @@ Grant request failed: ${msg}\r
|
|
|
484
687
|
} else {
|
|
485
688
|
writeAudit({ ...baseAudit, action: "allow", rule: "allow-list" });
|
|
486
689
|
}
|
|
487
|
-
|
|
690
|
+
if (deps?.secretsStore && deps?.leafCache) {
|
|
691
|
+
clientSocket.pause();
|
|
692
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
693
|
+
if (head.length > 0) {
|
|
694
|
+
clientSocket.unshift(head);
|
|
695
|
+
}
|
|
696
|
+
const store = deps.secretsStore;
|
|
697
|
+
handleMitmConnect({
|
|
698
|
+
clientSocket,
|
|
699
|
+
host,
|
|
700
|
+
port,
|
|
701
|
+
leafCache: deps.leafCache,
|
|
702
|
+
upstreamRejectUnauthorized: deps.upstreamRejectUnauthorized,
|
|
703
|
+
onRequest: (mreq) => {
|
|
704
|
+
const portSuffix = port === 443 ? "" : `:${port}`;
|
|
705
|
+
const targetUrl = new URL(`https://${mreq.host}${portSuffix}${mreq.path}`);
|
|
706
|
+
const match = store.findFor(targetUrl);
|
|
707
|
+
if (!match) return { type: "forward", mutatedHeaders: /* @__PURE__ */ new Map() };
|
|
708
|
+
const rendered = match.template.replace(/\$\{value\}/g, match.value);
|
|
709
|
+
console.error(
|
|
710
|
+
`[openape-proxy] injected secret '${match.name}' into ${match.header} for ${mreq.method} ${targetUrl.href}`
|
|
711
|
+
);
|
|
712
|
+
return {
|
|
713
|
+
type: "forward",
|
|
714
|
+
mutatedHeaders: /* @__PURE__ */ new Map([[match.header.toLowerCase(), rendered]])
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
tunnel(host, port, clientSocket);
|
|
488
721
|
}
|
|
489
|
-
function tunnel(host,
|
|
490
|
-
const targetSocket = (0, import_node_net2.connect)(
|
|
722
|
+
function tunnel(host, port, clientSocket) {
|
|
723
|
+
const targetSocket = (0, import_node_net2.connect)(port, host, () => {
|
|
491
724
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
492
725
|
targetSocket.pipe(clientSocket);
|
|
493
726
|
clientSocket.pipe(targetSocket);
|
|
@@ -513,30 +746,33 @@ async function computeRequestHash(method, targetUrl, body) {
|
|
|
513
746
|
}
|
|
514
747
|
return hash.digest("hex");
|
|
515
748
|
}
|
|
516
|
-
function buildGrantsClients(
|
|
749
|
+
function buildGrantsClients(config) {
|
|
517
750
|
const grantsClients = /* @__PURE__ */ new Map();
|
|
518
|
-
for (const agent of
|
|
751
|
+
for (const agent of config.agents) {
|
|
519
752
|
grantsClients.set(agent.email, new GrantsClient(agent.idp_url));
|
|
520
753
|
}
|
|
521
754
|
return grantsClients;
|
|
522
755
|
}
|
|
523
|
-
function createMultiAgentProxy(
|
|
524
|
-
for (const agent of
|
|
756
|
+
function createMultiAgentProxy(config, grantsClients = buildGrantsClients(config), deps = {}) {
|
|
757
|
+
for (const agent of config.agents) {
|
|
525
758
|
if (!grantsClients.has(agent.email)) {
|
|
526
759
|
grantsClients.set(agent.email, new GrantsClient(agent.idp_url));
|
|
527
760
|
}
|
|
528
761
|
}
|
|
529
|
-
const mandatoryAuth =
|
|
762
|
+
const mandatoryAuth = config.proxy.mandatory_auth ?? false;
|
|
763
|
+
const secretsStore = deps.secretsStore;
|
|
764
|
+
const daemonMode = deps.daemonMode ?? false;
|
|
765
|
+
const fetchImpl = deps.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
530
766
|
return {
|
|
531
|
-
port: Number.parseInt(
|
|
532
|
-
hostname:
|
|
767
|
+
port: Number.parseInt(config.proxy.listen.split(":")[1] || "9090"),
|
|
768
|
+
hostname: config.proxy.listen.split(":")[0] || "127.0.0.1",
|
|
533
769
|
async fetch(req) {
|
|
534
770
|
const url = new URL(req.url);
|
|
535
771
|
const startTime = Date.now();
|
|
536
772
|
if (url.pathname === "/healthz") {
|
|
537
773
|
return Response.json({
|
|
538
774
|
status: "ok",
|
|
539
|
-
agents:
|
|
775
|
+
agents: config.agents.map((a) => a.email)
|
|
540
776
|
});
|
|
541
777
|
}
|
|
542
778
|
const targetUrl = url.pathname.slice(1) + url.search;
|
|
@@ -564,33 +800,37 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
564
800
|
}
|
|
565
801
|
const bodyBuffer = req.body ? await req.arrayBuffer() : null;
|
|
566
802
|
let agentIdentity = null;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
803
|
+
if (daemonMode) {
|
|
804
|
+
agentIdentity = { email: config.agents[0].email, act: "agent" };
|
|
805
|
+
} else {
|
|
806
|
+
try {
|
|
807
|
+
for (const agentConf2 of config.agents) {
|
|
808
|
+
agentIdentity = await verifyAgentAuth(
|
|
809
|
+
req.headers.get("proxy-authorization"),
|
|
810
|
+
agentConf2.idp_url,
|
|
811
|
+
mandatoryAuth && config.agents.length === 1
|
|
812
|
+
);
|
|
813
|
+
if (agentIdentity) break;
|
|
814
|
+
}
|
|
815
|
+
if (mandatoryAuth && !agentIdentity) {
|
|
816
|
+
throw new AuthError("JWT required");
|
|
817
|
+
}
|
|
818
|
+
} catch (err) {
|
|
819
|
+
if (err instanceof AuthError) {
|
|
820
|
+
return new Response(`Unauthorized: ${err.message}`, { status: 401 });
|
|
821
|
+
}
|
|
822
|
+
throw err;
|
|
582
823
|
}
|
|
583
|
-
throw err;
|
|
584
824
|
}
|
|
585
825
|
const agentEmail = agentIdentity?.email;
|
|
586
826
|
let agentConf;
|
|
587
827
|
if (agentEmail) {
|
|
588
|
-
agentConf =
|
|
828
|
+
agentConf = config.agents.find((a) => a.email === agentEmail);
|
|
589
829
|
if (!agentConf) {
|
|
590
830
|
return new Response(`Forbidden: unknown agent ${agentEmail}`, { status: 403 });
|
|
591
831
|
}
|
|
592
|
-
} else if (
|
|
593
|
-
agentConf =
|
|
832
|
+
} else if (config.agents.length === 1) {
|
|
833
|
+
agentConf = config.agents[0];
|
|
594
834
|
} else {
|
|
595
835
|
return new Response("Unauthorized: JWT required for multi-agent proxy", { status: 401 });
|
|
596
836
|
}
|
|
@@ -598,10 +838,10 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
598
838
|
const grantsClient = grantsClients.get(agentConf.email);
|
|
599
839
|
const rulesConfig = {
|
|
600
840
|
proxy: {
|
|
601
|
-
listen:
|
|
841
|
+
listen: config.proxy.listen,
|
|
602
842
|
idp_url: agentConf.idp_url,
|
|
603
843
|
agent_email: agentConf.email,
|
|
604
|
-
default_action:
|
|
844
|
+
default_action: config.proxy.default_action
|
|
605
845
|
},
|
|
606
846
|
allow: agentConf.allow ?? [],
|
|
607
847
|
deny: agentConf.deny ?? [],
|
|
@@ -621,7 +861,7 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
621
861
|
}
|
|
622
862
|
if (action.type === "allow") {
|
|
623
863
|
writeAudit({ ...baseAudit, action: "allow", rule: "allow-list", grant_id: null });
|
|
624
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
864
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
625
865
|
}
|
|
626
866
|
const rule = action.rule;
|
|
627
867
|
const permissions = rule.permissions ?? [`${method.toLowerCase()}:${domain}`];
|
|
@@ -640,13 +880,13 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
640
880
|
grant_id: existing.id,
|
|
641
881
|
request_hash: requestHash
|
|
642
882
|
});
|
|
643
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
883
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
644
884
|
}
|
|
645
|
-
if (
|
|
885
|
+
if (config.proxy.default_action === "block") {
|
|
646
886
|
writeAudit({ ...baseAudit, action: "deny", rule: "no-grant (block mode)", grant_id: null });
|
|
647
887
|
return new Response("No grant \u2014 blocked", { status: 403 });
|
|
648
888
|
}
|
|
649
|
-
if (
|
|
889
|
+
if (config.proxy.default_action === "request-async") {
|
|
650
890
|
const grant = await grantsClient.requestGrant({
|
|
651
891
|
requester: effectiveEmail,
|
|
652
892
|
targetHost: domain,
|
|
@@ -695,7 +935,7 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
695
935
|
request_hash: requestHash,
|
|
696
936
|
waited_ms: waitedMs
|
|
697
937
|
});
|
|
698
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
938
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
699
939
|
}
|
|
700
940
|
writeAudit({
|
|
701
941
|
...baseAudit,
|
|
@@ -718,9 +958,12 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
718
958
|
}
|
|
719
959
|
};
|
|
720
960
|
}
|
|
721
|
-
function createNodeHandler(
|
|
722
|
-
const grantsClients = buildGrantsClients(
|
|
723
|
-
const proxy = createMultiAgentProxy(
|
|
961
|
+
function createNodeHandler(config, deps = {}) {
|
|
962
|
+
const grantsClients = buildGrantsClients(config);
|
|
963
|
+
const proxy = createMultiAgentProxy(config, grantsClients, {
|
|
964
|
+
secretsStore: deps.secretsStore,
|
|
965
|
+
daemonMode: deps.daemonMode
|
|
966
|
+
});
|
|
724
967
|
return {
|
|
725
968
|
handleRequest(req, res) {
|
|
726
969
|
const reqUrl = req.url || "/";
|
|
@@ -768,24 +1011,39 @@ function createNodeHandler(config2) {
|
|
|
768
1011
|
});
|
|
769
1012
|
},
|
|
770
1013
|
handleConnect(req, socket, head) {
|
|
771
|
-
handleConnect(
|
|
1014
|
+
handleConnect(config, grantsClients, req, socket, head, {
|
|
1015
|
+
secretsStore: deps.secretsStore,
|
|
1016
|
+
leafCache: deps.leafCache,
|
|
1017
|
+
daemonMode: deps.daemonMode
|
|
1018
|
+
});
|
|
772
1019
|
}
|
|
773
1020
|
};
|
|
774
1021
|
}
|
|
775
|
-
async function forwardRequest(originalReq, targetUrl, cachedBody) {
|
|
1022
|
+
async function forwardRequest(originalReq, targetUrl, targetParsed, cachedBody, secretsStore, fetchImpl) {
|
|
776
1023
|
const headers = new Headers(originalReq.headers);
|
|
777
1024
|
headers.delete("proxy-authorization");
|
|
778
1025
|
headers.delete("proxy-connection");
|
|
779
1026
|
headers.delete("host");
|
|
1027
|
+
if (secretsStore) {
|
|
1028
|
+
const match = secretsStore.findFor(targetParsed);
|
|
1029
|
+
if (match) {
|
|
1030
|
+
const rendered = match.template.replace(/\$\{value\}/g, match.value);
|
|
1031
|
+
headers.set(match.header, rendered);
|
|
1032
|
+
console.error(
|
|
1033
|
+
`[openape-proxy] injected secret '${match.name}' into ${match.header} for ${originalReq.method} ${targetUrl}`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
780
1037
|
const body = cachedBody && cachedBody.byteLength > 0 ? cachedBody : null;
|
|
781
1038
|
try {
|
|
782
|
-
const
|
|
1039
|
+
const upstreamReq = new Request(targetUrl, {
|
|
783
1040
|
method: originalReq.method,
|
|
784
1041
|
headers,
|
|
785
1042
|
body,
|
|
786
1043
|
duplex: "half",
|
|
787
1044
|
redirect: "manual"
|
|
788
1045
|
});
|
|
1046
|
+
const res = await fetchImpl(upstreamReq);
|
|
789
1047
|
const responseHeaders = new Headers(res.headers);
|
|
790
1048
|
responseHeaders.delete("transfer-encoding");
|
|
791
1049
|
responseHeaders.delete("connection");
|
|
@@ -800,56 +1058,220 @@ async function forwardRequest(originalReq, targetUrl, cachedBody) {
|
|
|
800
1058
|
}
|
|
801
1059
|
}
|
|
802
1060
|
|
|
1061
|
+
// src/secrets-store.ts
|
|
1062
|
+
var import_smol_toml2 = require("smol-toml");
|
|
1063
|
+
|
|
1064
|
+
// src/secrets-match.ts
|
|
1065
|
+
function compileGlob(glob) {
|
|
1066
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1067
|
+
return new RegExp(`^${escaped}$`);
|
|
1068
|
+
}
|
|
1069
|
+
function literalPrefixLen(glob) {
|
|
1070
|
+
const i = glob.indexOf("*");
|
|
1071
|
+
return i < 0 ? glob.length : i;
|
|
1072
|
+
}
|
|
1073
|
+
function targetString(url) {
|
|
1074
|
+
const port = url.port ? `:${url.port}` : "";
|
|
1075
|
+
const path = url.pathname === "/" ? "" : url.pathname;
|
|
1076
|
+
return `${url.hostname}${port}${path}`;
|
|
1077
|
+
}
|
|
1078
|
+
function matchSecret(target, entries) {
|
|
1079
|
+
const targetStr = targetString(target);
|
|
1080
|
+
let best = null;
|
|
1081
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1082
|
+
const entry = entries[i];
|
|
1083
|
+
const re = compileGlob(entry.target);
|
|
1084
|
+
if (!re.test(targetStr) && !re.test(`${targetStr}/`)) continue;
|
|
1085
|
+
const prefix = literalPrefixLen(entry.target);
|
|
1086
|
+
if (!best || prefix > best.prefix) {
|
|
1087
|
+
best = { entry, prefix, idx: i };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return best?.entry ?? null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/secrets-store.ts
|
|
1094
|
+
var MAX_BLOB_BYTES = 4 * 1024;
|
|
1095
|
+
var SUPPORTED_VERSION = "1";
|
|
1096
|
+
function parseSecretsBlob(toml) {
|
|
1097
|
+
if (Buffer.byteLength(toml, "utf-8") > MAX_BLOB_BYTES) {
|
|
1098
|
+
throw new Error(`Secrets blob too large (max ${MAX_BLOB_BYTES} bytes / 4 KiB)`);
|
|
1099
|
+
}
|
|
1100
|
+
const parsed = (0, import_smol_toml2.parse)(toml);
|
|
1101
|
+
const version = parsed.version;
|
|
1102
|
+
if (version !== SUPPORTED_VERSION) {
|
|
1103
|
+
throw new Error(`Unsupported or missing version (expected "${SUPPORTED_VERSION}", got ${JSON.stringify(version)})`);
|
|
1104
|
+
}
|
|
1105
|
+
const secretsBlock = parsed.secrets;
|
|
1106
|
+
const entries = [];
|
|
1107
|
+
if (secretsBlock) {
|
|
1108
|
+
for (const [name, raw] of Object.entries(secretsBlock)) {
|
|
1109
|
+
const required = ["target", "header", "template", "value"];
|
|
1110
|
+
for (const field of required) {
|
|
1111
|
+
if (!raw[field] || typeof raw[field] !== "string") {
|
|
1112
|
+
throw new Error(`Secret '${name}' is missing required field '${field}'`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
entries.push({
|
|
1116
|
+
name,
|
|
1117
|
+
target: raw.target,
|
|
1118
|
+
header: raw.header,
|
|
1119
|
+
template: raw.template,
|
|
1120
|
+
value: raw.value
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const literalPrefix = (glob) => {
|
|
1125
|
+
const i = glob.indexOf("*");
|
|
1126
|
+
return i < 0 ? glob : glob.slice(0, i);
|
|
1127
|
+
};
|
|
1128
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const e of entries) {
|
|
1130
|
+
const key = `${literalPrefix(e.target)}|${e.target}`;
|
|
1131
|
+
if (seen.has(key)) {
|
|
1132
|
+
throw new Error(`Duplicate target with identical specificity: '${e.target}' (entries '${seen.get(key)}' and '${e.name}')`);
|
|
1133
|
+
}
|
|
1134
|
+
seen.set(key, e.name);
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
entries,
|
|
1138
|
+
findFor: (target) => matchSecret(target, entries)
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
803
1142
|
// src/index.ts
|
|
1143
|
+
function loadIdentity() {
|
|
1144
|
+
const path = (0, import_node_path2.join)((0, import_node_os.homedir)(), ".config", "apes", "auth.json");
|
|
1145
|
+
if (!(0, import_node_fs3.existsSync)(path)) {
|
|
1146
|
+
console.error(`[openape-proxy] missing ${path} \u2014 run \`apes login\` first`);
|
|
1147
|
+
process.exit(2);
|
|
1148
|
+
}
|
|
1149
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
|
|
1150
|
+
const bearer = raw.access_token ?? raw.bearer;
|
|
1151
|
+
if (!raw.email || !raw.idp || !bearer) {
|
|
1152
|
+
console.error(`[openape-proxy] malformed ${path} \u2014 re-run \`apes login\``);
|
|
1153
|
+
process.exit(2);
|
|
1154
|
+
}
|
|
1155
|
+
return { email: raw.email, idpUrl: raw.idp, bearer };
|
|
1156
|
+
}
|
|
804
1157
|
var { values } = (0, import_node_util.parseArgs)({
|
|
805
1158
|
options: {
|
|
806
1159
|
config: { type: "string", short: "c", default: "config.toml" },
|
|
807
1160
|
"dry-run": { type: "boolean", default: false },
|
|
808
|
-
"mandatory-auth": { type: "boolean", default: false }
|
|
1161
|
+
"mandatory-auth": { type: "boolean", default: false },
|
|
1162
|
+
global: { type: "boolean", default: false },
|
|
1163
|
+
port: { type: "string" }
|
|
809
1164
|
}
|
|
810
1165
|
});
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1166
|
+
if (values.global) {
|
|
1167
|
+
if (process.versions.bun) {
|
|
1168
|
+
console.error(
|
|
1169
|
+
"[openape-proxy] --global mode requires Node (Bun is not supported).\n Bun's node:tls compat layer does not handle TLSSocket-on-existing-socket\n for the CONNECT-MITM pipeline. Re-run with `node` directly, or use the\n built `openape-proxy` binary (its shebang invokes Node)."
|
|
1170
|
+
);
|
|
1171
|
+
process.exit(2);
|
|
1172
|
+
}
|
|
1173
|
+
const stdinBuf = [];
|
|
1174
|
+
let total = 0;
|
|
1175
|
+
let aborted = false;
|
|
1176
|
+
process.stdin.on("data", (chunk) => {
|
|
1177
|
+
if (aborted) return;
|
|
1178
|
+
total += chunk.length;
|
|
1179
|
+
if (total > 8 * 1024) {
|
|
1180
|
+
console.error("[openape-proxy] stdin > 8 KiB, refusing");
|
|
1181
|
+
aborted = true;
|
|
1182
|
+
process.exit(2);
|
|
1183
|
+
}
|
|
1184
|
+
stdinBuf.push(chunk);
|
|
1185
|
+
});
|
|
1186
|
+
process.stdin.on("end", () => {
|
|
1187
|
+
if (aborted) return;
|
|
1188
|
+
const blob = Buffer.concat(stdinBuf).toString("utf-8");
|
|
1189
|
+
if (!blob.trim()) {
|
|
1190
|
+
console.error("[openape-proxy] --global requires secrets TOML on stdin");
|
|
1191
|
+
process.exit(2);
|
|
1192
|
+
}
|
|
1193
|
+
let store;
|
|
1194
|
+
try {
|
|
1195
|
+
store = parseSecretsBlob(blob);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
console.error(`[openape-proxy] secrets parse error: ${err.message}`);
|
|
1198
|
+
process.exit(2);
|
|
1199
|
+
}
|
|
1200
|
+
const identity = loadIdentity();
|
|
1201
|
+
const port = Number.parseInt(values.port ?? "18789");
|
|
1202
|
+
const ca = loadOrCreateCa({
|
|
1203
|
+
certPath: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".openape", "proxy", "ca.crt"),
|
|
1204
|
+
keyPath: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".openape", "proxy", "ca.key"),
|
|
1205
|
+
subjectCN: `OpenApe Proxy CA (${identity.email})`
|
|
1206
|
+
});
|
|
1207
|
+
if (ca.created) {
|
|
1208
|
+
console.log(`[openape-proxy] generated new CA at ~/.openape/proxy/ca.crt`);
|
|
1209
|
+
}
|
|
1210
|
+
const leafCache = createLeafCertCache(ca, { capacity: 256 });
|
|
1211
|
+
const config = {
|
|
1212
|
+
proxy: { listen: `127.0.0.1:${port}`, default_action: "allow" },
|
|
1213
|
+
agents: [{ email: identity.email, idp_url: identity.idpUrl }]
|
|
1214
|
+
};
|
|
1215
|
+
const handler = createNodeHandler(config, { secretsStore: store, leafCache, daemonMode: true });
|
|
1216
|
+
const server = (0, import_node_http.createServer)(handler.handleRequest);
|
|
1217
|
+
server.on("connect", handler.handleConnect);
|
|
1218
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1219
|
+
const addr = server.address();
|
|
1220
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
1221
|
+
console.log(`[openape-proxy] identity: ${identity.email} (${identity.idpUrl})`);
|
|
1222
|
+
const names = store.entries.map((e) => e.name).join(", ");
|
|
1223
|
+
console.log(`[openape-proxy] secrets: ${names}`);
|
|
1224
|
+
console.log(`[openape-proxy] export OPENAPE_PROXY=127.0.0.1:${actualPort}`);
|
|
1225
|
+
console.log(`[openape-proxy] listening on 127.0.0.1:${actualPort}`);
|
|
1226
|
+
});
|
|
1227
|
+
process.on("SIGINT", () => server.close(() => process.exit(0)));
|
|
1228
|
+
process.on("SIGTERM", () => server.close(() => process.exit(0)));
|
|
1229
|
+
});
|
|
1230
|
+
process.stdin.resume();
|
|
1231
|
+
} else {
|
|
1232
|
+
const configPath = values.config;
|
|
1233
|
+
console.log(`[openape-proxy] Loading config from ${configPath}`);
|
|
1234
|
+
const config = loadMultiAgentConfig(configPath, {
|
|
1235
|
+
mandatoryAuth: values["mandatory-auth"] || void 0
|
|
1236
|
+
});
|
|
1237
|
+
if (values["dry-run"]) {
|
|
1238
|
+
console.log("[openape-proxy] DRY RUN mode \u2014 logging only, not blocking");
|
|
1239
|
+
console.log("[openape-proxy] Config loaded:");
|
|
1240
|
+
console.log(` Listen: ${config.proxy.listen}`);
|
|
1241
|
+
console.log(` Default action: ${config.proxy.default_action}`);
|
|
1242
|
+
console.log(` Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
1243
|
+
console.log(` Agents: ${config.agents.length}`);
|
|
1244
|
+
for (const agent of config.agents) {
|
|
1245
|
+
const allowCount = agent.allow?.length ?? 0;
|
|
1246
|
+
const denyCount = agent.deny?.length ?? 0;
|
|
1247
|
+
const grantCount = agent.grant_required?.length ?? 0;
|
|
1248
|
+
console.log(` ${agent.email} (${agent.idp_url}) \u2014 ${allowCount} allow, ${denyCount} deny, ${grantCount} grant`);
|
|
1249
|
+
}
|
|
1250
|
+
process.exit(0);
|
|
828
1251
|
}
|
|
829
|
-
|
|
1252
|
+
const handler = createNodeHandler(config);
|
|
1253
|
+
const port = Number.parseInt(config.proxy.listen.split(":")[1] || "9090");
|
|
1254
|
+
const hostname = config.proxy.listen.split(":")[0] || "127.0.0.1";
|
|
1255
|
+
const server = (0, import_node_http.createServer)(handler.handleRequest);
|
|
1256
|
+
server.on("connect", handler.handleConnect);
|
|
1257
|
+
server.listen(port, hostname, () => {
|
|
1258
|
+
const addr = server.address();
|
|
1259
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
1260
|
+
console.log(`[openape-proxy] Listening on http://${hostname}:${actualPort}`);
|
|
1261
|
+
console.log(`[openape-proxy] CONNECT tunneling enabled`);
|
|
1262
|
+
console.log(`[openape-proxy] Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
1263
|
+
console.log(`[openape-proxy] Agents: ${config.agents.map((a) => a.email).join(", ")}`);
|
|
1264
|
+
console.log(`[openape-proxy] Default action: ${config.proxy.default_action}`);
|
|
1265
|
+
});
|
|
1266
|
+
process.on("SIGINT", () => {
|
|
1267
|
+
console.log("\n[openape-proxy] Shutting down...");
|
|
1268
|
+
server.close();
|
|
1269
|
+
process.exit(0);
|
|
1270
|
+
});
|
|
1271
|
+
process.on("SIGTERM", () => {
|
|
1272
|
+
console.log("[openape-proxy] Shutting down...");
|
|
1273
|
+
server.close();
|
|
1274
|
+
process.exit(0);
|
|
1275
|
+
});
|
|
830
1276
|
}
|
|
831
|
-
var handler = createNodeHandler(config);
|
|
832
|
-
var port = Number.parseInt(config.proxy.listen.split(":")[1] || "9090");
|
|
833
|
-
var hostname = config.proxy.listen.split(":")[0] || "127.0.0.1";
|
|
834
|
-
var server = (0, import_node_http.createServer)(handler.handleRequest);
|
|
835
|
-
server.on("connect", handler.handleConnect);
|
|
836
|
-
server.listen(port, hostname, () => {
|
|
837
|
-
const addr = server.address();
|
|
838
|
-
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
839
|
-
console.log(`[openape-proxy] Listening on http://${hostname}:${actualPort}`);
|
|
840
|
-
console.log(`[openape-proxy] CONNECT tunneling enabled`);
|
|
841
|
-
console.log(`[openape-proxy] Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
842
|
-
console.log(`[openape-proxy] Agents: ${config.agents.map((a) => a.email).join(", ")}`);
|
|
843
|
-
console.log(`[openape-proxy] Default action: ${config.proxy.default_action}`);
|
|
844
|
-
});
|
|
845
|
-
process.on("SIGINT", () => {
|
|
846
|
-
console.log("\n[openape-proxy] Shutting down...");
|
|
847
|
-
server.close();
|
|
848
|
-
process.exit(0);
|
|
849
|
-
});
|
|
850
|
-
process.on("SIGTERM", () => {
|
|
851
|
-
console.log("[openape-proxy] Shutting down...");
|
|
852
|
-
server.close();
|
|
853
|
-
process.exit(0);
|
|
854
|
-
});
|
|
855
1277
|
//# sourceMappingURL=index.cjs.map
|