@openape/proxy 0.2.13 → 0.2.15

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/index.js ADDED
@@ -0,0 +1,710 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createServer } from "http";
5
+ import { parseArgs } from "util";
6
+
7
+ // src/config.ts
8
+ import { readFileSync } from "fs";
9
+ import { parse as parseTOML } from "smol-toml";
10
+ function loadMultiAgentConfig(path, overrides) {
11
+ const raw = readFileSync(path, "utf-8");
12
+ let parsed;
13
+ if (path.endsWith(".json")) {
14
+ parsed = JSON.parse(raw);
15
+ } else {
16
+ parsed = parseTOML(raw);
17
+ }
18
+ const proxy = parsed.proxy;
19
+ if (!proxy?.listen) {
20
+ throw new Error("Config must have [proxy] with listen");
21
+ }
22
+ const baseProxy = {
23
+ listen: proxy.listen,
24
+ default_action: proxy.default_action ?? "block",
25
+ audit_log: proxy.audit_log,
26
+ mandatory_auth: overrides?.mandatoryAuth ?? proxy.mandatory_auth
27
+ };
28
+ if (Array.isArray(parsed.agents)) {
29
+ return {
30
+ proxy: baseProxy,
31
+ agents: parsed.agents
32
+ };
33
+ }
34
+ const idpUrl = proxy.idp_url;
35
+ const agentEmail = proxy.agent_email;
36
+ if (!idpUrl || !agentEmail) {
37
+ throw new Error("Single-agent config requires proxy.idp_url and proxy.agent_email");
38
+ }
39
+ return {
40
+ proxy: baseProxy,
41
+ agents: [{
42
+ email: agentEmail,
43
+ idp_url: idpUrl,
44
+ allow: parsed.allow ?? [],
45
+ deny: parsed.deny ?? [],
46
+ grant_required: parsed.grant_required ?? []
47
+ }]
48
+ };
49
+ }
50
+
51
+ // src/proxy.ts
52
+ import { createHash } from "crypto";
53
+
54
+ // src/matcher.ts
55
+ function globMatch(pattern, value) {
56
+ const regex = new RegExp(
57
+ `^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTAR>>>/g, ".*")}$`
58
+ );
59
+ return regex.test(value);
60
+ }
61
+ function matchesRule(rule, domain, method, path) {
62
+ if (!globMatch(rule.domain, domain)) return false;
63
+ if (rule.methods && rule.methods.length > 0) {
64
+ if (!rule.methods.includes(method.toUpperCase())) return false;
65
+ }
66
+ if (rule.path) {
67
+ if (!globMatch(rule.path, path)) return false;
68
+ }
69
+ return true;
70
+ }
71
+ function evaluateRules(config2, domain, method, path) {
72
+ for (const rule of config2.deny) {
73
+ if (matchesRule(rule, domain, method, path)) {
74
+ return { type: "deny", note: rule.note };
75
+ }
76
+ }
77
+ for (const rule of config2.allow) {
78
+ if (matchesRule(rule, domain, method, path)) {
79
+ return { type: "allow" };
80
+ }
81
+ }
82
+ for (const rule of config2.grant_required) {
83
+ if (matchesRule(rule, domain, method, path)) {
84
+ return { type: "grant_required", rule };
85
+ }
86
+ }
87
+ if (config2.proxy.default_action === "block") {
88
+ return { type: "deny", note: "No matching rule (default: block)" };
89
+ }
90
+ return {
91
+ type: "grant_required",
92
+ rule: {
93
+ domain: "*",
94
+ grant_type: "once"
95
+ }
96
+ };
97
+ }
98
+
99
+ // src/auth.ts
100
+ import { verifyJWT, createRemoteJWKS } from "@openape/core";
101
+ var AuthError = class extends Error {
102
+ constructor(message) {
103
+ super(message);
104
+ this.name = "AuthError";
105
+ }
106
+ };
107
+ async function verifyAgentAuth(authHeader, idpUrl, mandatory = false) {
108
+ if (!authHeader) {
109
+ if (mandatory) throw new AuthError("JWT required");
110
+ return null;
111
+ }
112
+ const match = authHeader.match(/^Bearer (.+)$/i);
113
+ if (!match) {
114
+ if (mandatory) throw new AuthError("Invalid authorization header");
115
+ return null;
116
+ }
117
+ const token = match[1];
118
+ try {
119
+ const jwks = createRemoteJWKS(`${idpUrl}/.well-known/jwks.json`);
120
+ const { payload } = await verifyJWT(token, jwks, { issuer: idpUrl });
121
+ if (payload.act !== "agent" || !payload.sub) {
122
+ if (mandatory) throw new AuthError("Invalid agent token");
123
+ return null;
124
+ }
125
+ return {
126
+ email: payload.sub,
127
+ act: "agent"
128
+ };
129
+ } catch (err) {
130
+ if (err instanceof AuthError) throw err;
131
+ if (mandatory) throw new AuthError("JWT verification failed");
132
+ return null;
133
+ }
134
+ }
135
+
136
+ // src/grants-client.ts
137
+ var GrantsClient = class {
138
+ idpUrl;
139
+ agentToken;
140
+ constructor(idpUrl) {
141
+ this.idpUrl = idpUrl.replace(/\/$/, "");
142
+ }
143
+ setAgentToken(token) {
144
+ this.agentToken = token;
145
+ }
146
+ headers() {
147
+ const h = { "Content-Type": "application/json" };
148
+ if (this.agentToken) {
149
+ h.Authorization = `Bearer ${this.agentToken}`;
150
+ }
151
+ return h;
152
+ }
153
+ /**
154
+ * Create a grant request on the IdP.
155
+ */
156
+ async requestGrant(opts) {
157
+ const res = await fetch(`${this.idpUrl}/api/grants`, {
158
+ method: "POST",
159
+ headers: this.headers(),
160
+ body: JSON.stringify({
161
+ requester: opts.requester,
162
+ target_host: opts.targetHost,
163
+ audience: opts.audience,
164
+ grant_type: opts.grantType,
165
+ permissions: opts.permissions,
166
+ reason: opts.reason,
167
+ request_hash: opts.requestHash,
168
+ duration: opts.duration
169
+ })
170
+ });
171
+ if (!res.ok) {
172
+ throw new Error(`Grant request failed: ${res.status} ${await res.text()}`);
173
+ }
174
+ return res.json();
175
+ }
176
+ /**
177
+ * Poll a grant until it's approved, denied, or timeout.
178
+ */
179
+ async waitForApproval(grantId, timeoutMs = 3e5, pollIntervalMs = 2e3) {
180
+ const deadline = Date.now() + timeoutMs;
181
+ while (Date.now() < deadline) {
182
+ const res = await fetch(`${this.idpUrl}/api/grants/${grantId}`, {
183
+ headers: this.headers()
184
+ });
185
+ if (!res.ok) {
186
+ throw new Error(`Grant poll failed: ${res.status}`);
187
+ }
188
+ const grant = await res.json();
189
+ if (grant.status !== "pending") {
190
+ return grant;
191
+ }
192
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
193
+ }
194
+ throw new Error(`Grant approval timed out after ${timeoutMs}ms`);
195
+ }
196
+ /**
197
+ * Check if there's an existing approved grant for a host+audience+permissions combo.
198
+ * Ignores `once` grants (they're single-use).
199
+ */
200
+ async findExistingGrant(requester, targetHost, audience, permissions) {
201
+ const params = new URLSearchParams({
202
+ requester,
203
+ status: "approved"
204
+ });
205
+ const res = await fetch(`${this.idpUrl}/api/grants?${params}`, {
206
+ headers: this.headers()
207
+ });
208
+ if (!res.ok) return null;
209
+ const grants = await res.json();
210
+ const now = Math.floor(Date.now() / 1e3);
211
+ return grants.find((g) => {
212
+ if (g.status !== "approved") return false;
213
+ if (g.expires_at && g.expires_at <= now) return false;
214
+ if (g.request?.grant_type === "once") return false;
215
+ if (g.request?.target_host !== targetHost) return false;
216
+ if (g.request?.audience !== audience) return false;
217
+ if (permissions?.length && g.request?.permissions?.length) {
218
+ const grantedPerms = new Set(g.request.permissions);
219
+ if (!permissions.every((p) => grantedPerms.has(p))) return false;
220
+ }
221
+ return true;
222
+ }) ?? null;
223
+ }
224
+ };
225
+
226
+ // src/audit.ts
227
+ import { appendFileSync } from "fs";
228
+ var auditPath;
229
+ function initAudit(path) {
230
+ auditPath = path;
231
+ }
232
+ function writeAudit(entry) {
233
+ const line = JSON.stringify(entry);
234
+ console.error(`[audit] ${entry.action} ${entry.method} ${entry.domain}${entry.path}${entry.grant_id ? ` grant=${entry.grant_id}` : ""}`);
235
+ if (auditPath) {
236
+ appendFileSync(auditPath, `${line}
237
+ `);
238
+ }
239
+ }
240
+
241
+ // src/ssrf.ts
242
+ import { resolve4, resolve6 } from "dns/promises";
243
+ import { isIP } from "net";
244
+ var PRIVATE_RANGES_V4 = [
245
+ { prefix: 2130706432, mask: 4278190080 },
246
+ // 127.0.0.0/8
247
+ { prefix: 167772160, mask: 4278190080 },
248
+ // 10.0.0.0/8
249
+ { prefix: 2886729728, mask: 4293918720 },
250
+ // 172.16.0.0/12
251
+ { prefix: 3232235520, mask: 4294901760 },
252
+ // 192.168.0.0/16
253
+ { prefix: 2851995648, mask: 4294901760 },
254
+ // 169.254.0.0/16
255
+ { prefix: 0, mask: 4278190080 }
256
+ // 0.0.0.0/8
257
+ ];
258
+ function ipv4ToNumber(ip) {
259
+ const parts = ip.split(".").map(Number);
260
+ return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
261
+ }
262
+ function isPrivateIPv4(ip) {
263
+ const num = ipv4ToNumber(ip);
264
+ return PRIVATE_RANGES_V4.some((r) => (num & r.mask) >>> 0 === r.prefix);
265
+ }
266
+ function isPrivateIPv6(ip) {
267
+ const normalized = ip.toLowerCase();
268
+ if (normalized === "::1") return true;
269
+ if (normalized === "::") return true;
270
+ if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) {
271
+ return true;
272
+ }
273
+ if (normalized.startsWith("fd")) return true;
274
+ const v4mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
275
+ if (v4mapped) return isPrivateIPv4(v4mapped[1]);
276
+ return false;
277
+ }
278
+ function isPrivateIP(ip) {
279
+ if (isIP(ip) === 4) return isPrivateIPv4(ip);
280
+ if (isIP(ip) === 6) return isPrivateIPv6(ip);
281
+ return false;
282
+ }
283
+ async function isPrivateOrLoopback(hostname2) {
284
+ if (isIP(hostname2)) {
285
+ return isPrivateIP(hostname2);
286
+ }
287
+ if (hostname2 === "localhost") return true;
288
+ try {
289
+ const [v4, v6] = await Promise.allSettled([
290
+ resolve4(hostname2),
291
+ resolve6(hostname2)
292
+ ]);
293
+ const addrs = [];
294
+ if (v4.status === "fulfilled") addrs.push(...v4.value);
295
+ if (v6.status === "fulfilled") addrs.push(...v6.value);
296
+ if (addrs.length === 0) return true;
297
+ return addrs.some((addr) => isPrivateIP(addr));
298
+ } catch {
299
+ return true;
300
+ }
301
+ }
302
+
303
+ // src/connect.ts
304
+ import { connect } from "net";
305
+ async function handleConnect(config2, req, clientSocket, _head) {
306
+ const target = req.url ?? "";
307
+ const [host, portStr] = target.split(":");
308
+ const port2 = Number.parseInt(portStr || "443");
309
+ if (!host || !port2) {
310
+ clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
311
+ clientSocket.destroy();
312
+ return;
313
+ }
314
+ const mandatoryAuth = config2.proxy.mandatory_auth ?? false;
315
+ let agentEmail;
316
+ try {
317
+ const authHeader = req.headers["proxy-authorization"];
318
+ let identity = null;
319
+ for (const agentConf of config2.agents) {
320
+ identity = await verifyAgentAuth(
321
+ authHeader ?? null,
322
+ agentConf.idp_url,
323
+ mandatoryAuth && config2.agents.length === 1
324
+ );
325
+ if (identity) break;
326
+ }
327
+ if (mandatoryAuth && !identity) {
328
+ throw new AuthError("JWT required");
329
+ }
330
+ agentEmail = identity?.email;
331
+ if (agentEmail) {
332
+ const known = config2.agents.find((a) => a.email === agentEmail);
333
+ if (!known) {
334
+ clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
335
+ clientSocket.destroy();
336
+ return;
337
+ }
338
+ } else if (config2.agents.length > 1) {
339
+ throw new AuthError("JWT required for multi-agent proxy");
340
+ }
341
+ } catch (err) {
342
+ if (err instanceof AuthError) {
343
+ clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
344
+ clientSocket.destroy();
345
+ return;
346
+ }
347
+ throw err;
348
+ }
349
+ if (await isPrivateOrLoopback(host)) {
350
+ writeAudit({
351
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
352
+ agent: agentEmail ?? config2.agents[0]?.email ?? "unknown",
353
+ action: "deny",
354
+ domain: host,
355
+ method: "CONNECT",
356
+ path: target,
357
+ rule: "ssrf-blocked"
358
+ });
359
+ clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
360
+ clientSocket.destroy();
361
+ return;
362
+ }
363
+ const targetSocket = connect(port2, host, () => {
364
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
365
+ targetSocket.pipe(clientSocket);
366
+ clientSocket.pipe(targetSocket);
367
+ });
368
+ targetSocket.on("error", () => {
369
+ clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
370
+ clientSocket.destroy();
371
+ });
372
+ clientSocket.on("error", () => {
373
+ targetSocket.destroy();
374
+ });
375
+ clientSocket.on("close", () => targetSocket.destroy());
376
+ targetSocket.on("close", () => clientSocket.destroy());
377
+ }
378
+
379
+ // src/proxy.ts
380
+ async function computeRequestHash(method, targetUrl, body) {
381
+ const hash = createHash("sha256");
382
+ hash.update(`${method} ${targetUrl}
383
+ `);
384
+ if (body && body.byteLength > 0) {
385
+ hash.update(new Uint8Array(body));
386
+ }
387
+ return hash.digest("hex");
388
+ }
389
+ function createMultiAgentProxy(config2) {
390
+ const grantsClients = /* @__PURE__ */ new Map();
391
+ for (const agent of config2.agents) {
392
+ grantsClients.set(agent.email, new GrantsClient(agent.idp_url));
393
+ }
394
+ const mandatoryAuth = config2.proxy.mandatory_auth ?? false;
395
+ return {
396
+ port: Number.parseInt(config2.proxy.listen.split(":")[1] || "9090"),
397
+ hostname: config2.proxy.listen.split(":")[0] || "127.0.0.1",
398
+ async fetch(req) {
399
+ const url = new URL(req.url);
400
+ const startTime = Date.now();
401
+ if (url.pathname === "/healthz") {
402
+ return Response.json({
403
+ status: "ok",
404
+ agents: config2.agents.map((a) => a.email)
405
+ });
406
+ }
407
+ const targetUrl = url.pathname.slice(1) + url.search;
408
+ let targetParsed;
409
+ try {
410
+ targetParsed = new URL(targetUrl);
411
+ } catch {
412
+ return new Response(
413
+ "Invalid target URL. Send requests as: http://proxy:port/https://target.com/path",
414
+ { status: 400 }
415
+ );
416
+ }
417
+ const domain = targetParsed.hostname;
418
+ const method = req.method;
419
+ const path = targetParsed.pathname;
420
+ if (await isPrivateOrLoopback(domain)) {
421
+ return new Response("Blocked: private/loopback IP", { status: 403 });
422
+ }
423
+ const bodyBuffer = req.body ? await req.arrayBuffer() : null;
424
+ let agentIdentity = null;
425
+ try {
426
+ for (const agentConf2 of config2.agents) {
427
+ agentIdentity = await verifyAgentAuth(
428
+ req.headers.get("proxy-authorization"),
429
+ agentConf2.idp_url,
430
+ mandatoryAuth && config2.agents.length === 1
431
+ );
432
+ if (agentIdentity) break;
433
+ }
434
+ if (mandatoryAuth && !agentIdentity) {
435
+ throw new AuthError("JWT required");
436
+ }
437
+ } catch (err) {
438
+ if (err instanceof AuthError) {
439
+ return new Response(`Unauthorized: ${err.message}`, { status: 401 });
440
+ }
441
+ throw err;
442
+ }
443
+ const agentEmail = agentIdentity?.email;
444
+ let agentConf;
445
+ if (agentEmail) {
446
+ agentConf = config2.agents.find((a) => a.email === agentEmail);
447
+ if (!agentConf) {
448
+ return new Response(`Forbidden: unknown agent ${agentEmail}`, { status: 403 });
449
+ }
450
+ } else if (config2.agents.length === 1) {
451
+ agentConf = config2.agents[0];
452
+ } else {
453
+ return new Response("Unauthorized: JWT required for multi-agent proxy", { status: 401 });
454
+ }
455
+ const effectiveEmail = agentEmail ?? agentConf.email;
456
+ const grantsClient = grantsClients.get(agentConf.email);
457
+ const rulesConfig = {
458
+ proxy: {
459
+ listen: config2.proxy.listen,
460
+ idp_url: agentConf.idp_url,
461
+ agent_email: agentConf.email,
462
+ default_action: config2.proxy.default_action
463
+ },
464
+ allow: agentConf.allow ?? [],
465
+ deny: agentConf.deny ?? [],
466
+ grant_required: agentConf.grant_required ?? []
467
+ };
468
+ const action = evaluateRules(rulesConfig, domain, method, path);
469
+ const baseAudit = {
470
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
471
+ agent: effectiveEmail,
472
+ domain,
473
+ method,
474
+ path
475
+ };
476
+ if (action.type === "deny") {
477
+ writeAudit({ ...baseAudit, action: "deny", rule: "deny-list", grant_id: null });
478
+ return new Response(`Blocked: ${action.note || "deny rule"}`, { status: 403 });
479
+ }
480
+ if (action.type === "allow") {
481
+ writeAudit({ ...baseAudit, action: "allow", rule: "allow-list", grant_id: null });
482
+ return forwardRequest(req, targetUrl, bodyBuffer);
483
+ }
484
+ const rule = action.rule;
485
+ const permissions = rule.permissions ?? [`${method.toLowerCase()}:${domain}`];
486
+ const requestHash = await computeRequestHash(method, targetUrl, bodyBuffer);
487
+ const existing = await grantsClient.findExistingGrant(
488
+ effectiveEmail,
489
+ domain,
490
+ "proxy",
491
+ permissions
492
+ ).catch(() => null);
493
+ if (existing) {
494
+ writeAudit({
495
+ ...baseAudit,
496
+ action: "grant_approved",
497
+ rule: "standing-grant",
498
+ grant_id: existing.id,
499
+ request_hash: requestHash
500
+ });
501
+ return forwardRequest(req, targetUrl, bodyBuffer);
502
+ }
503
+ if (config2.proxy.default_action === "block") {
504
+ writeAudit({ ...baseAudit, action: "deny", rule: "no-grant (block mode)", grant_id: null });
505
+ return new Response("No grant \u2014 blocked", { status: 403 });
506
+ }
507
+ if (config2.proxy.default_action === "request-async") {
508
+ const grant = await grantsClient.requestGrant({
509
+ requester: effectiveEmail,
510
+ targetHost: domain,
511
+ audience: "proxy",
512
+ grantType: rule.grant_type,
513
+ permissions,
514
+ reason: `${method} ${targetUrl}`,
515
+ requestHash,
516
+ duration: rule.duration
517
+ }).catch(() => null);
518
+ writeAudit({
519
+ ...baseAudit,
520
+ action: "grant_denied",
521
+ rule: "grant_required (async)",
522
+ grant_id: grant?.id ?? null
523
+ });
524
+ return new Response(
525
+ JSON.stringify({
526
+ error: "Grant required",
527
+ grant_id: grant?.id,
528
+ message: "Grant request created. Retry after approval."
529
+ }),
530
+ { status: 407, headers: { "Content-Type": "application/json" } }
531
+ );
532
+ }
533
+ console.error(`[proxy] Requesting grant for ${method} ${domain}${path} \u2014 waiting for approval...`);
534
+ try {
535
+ const grant = await grantsClient.requestGrant({
536
+ requester: effectiveEmail,
537
+ targetHost: domain,
538
+ audience: "proxy",
539
+ grantType: rule.grant_type,
540
+ permissions,
541
+ reason: `${method} ${targetUrl}`,
542
+ requestHash,
543
+ duration: rule.duration
544
+ });
545
+ const approved = await grantsClient.waitForApproval(grant.id);
546
+ const waitedMs = Date.now() - startTime;
547
+ if (approved.status === "approved") {
548
+ writeAudit({
549
+ ...baseAudit,
550
+ action: "grant_approved",
551
+ rule: "grant_required",
552
+ grant_id: approved.id,
553
+ request_hash: requestHash,
554
+ waited_ms: waitedMs
555
+ });
556
+ return forwardRequest(req, targetUrl, bodyBuffer);
557
+ }
558
+ writeAudit({
559
+ ...baseAudit,
560
+ action: "grant_denied",
561
+ rule: "grant_required",
562
+ grant_id: approved.id,
563
+ waited_ms: waitedMs
564
+ });
565
+ return new Response(`Grant denied by ${approved.decided_by}`, { status: 403 });
566
+ } catch (err) {
567
+ const msg = err instanceof Error ? err.message : "Unknown error";
568
+ writeAudit({
569
+ ...baseAudit,
570
+ action: "grant_timeout",
571
+ rule: "grant_required",
572
+ error: msg
573
+ });
574
+ return new Response(`Grant request failed: ${msg}`, { status: 504 });
575
+ }
576
+ }
577
+ };
578
+ }
579
+ function createNodeHandler(config2) {
580
+ const proxy = createMultiAgentProxy(config2);
581
+ return {
582
+ handleRequest(req, res) {
583
+ const url = `http://${req.headers.host || "localhost"}${req.url || "/"}`;
584
+ const chunks = [];
585
+ req.on("data", (chunk) => chunks.push(chunk));
586
+ req.on("end", async () => {
587
+ try {
588
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
589
+ const headers = new Headers();
590
+ for (const [key, value] of Object.entries(req.headers)) {
591
+ if (value) {
592
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
593
+ }
594
+ }
595
+ const request = new Request(url, {
596
+ method: req.method,
597
+ headers,
598
+ body: body && req.method !== "GET" && req.method !== "HEAD" ? body : void 0,
599
+ duplex: "half"
600
+ });
601
+ const response = await proxy.fetch(request);
602
+ res.writeHead(response.status, response.statusText, Object.fromEntries(response.headers));
603
+ if (response.body) {
604
+ const reader = response.body.getReader();
605
+ const pump = async () => {
606
+ const { done, value } = await reader.read();
607
+ if (done) {
608
+ res.end();
609
+ return;
610
+ }
611
+ res.write(value);
612
+ return pump();
613
+ };
614
+ await pump();
615
+ } else {
616
+ res.end();
617
+ }
618
+ } catch {
619
+ res.writeHead(502);
620
+ res.end("Proxy error");
621
+ }
622
+ });
623
+ },
624
+ handleConnect(req, socket, head) {
625
+ handleConnect(config2, req, socket, head);
626
+ }
627
+ };
628
+ }
629
+ async function forwardRequest(originalReq, targetUrl, cachedBody) {
630
+ const headers = new Headers(originalReq.headers);
631
+ headers.delete("proxy-authorization");
632
+ headers.delete("proxy-connection");
633
+ headers.delete("host");
634
+ const body = cachedBody && cachedBody.byteLength > 0 ? cachedBody : null;
635
+ try {
636
+ const res = await fetch(targetUrl, {
637
+ method: originalReq.method,
638
+ headers,
639
+ body,
640
+ duplex: "half",
641
+ redirect: "manual"
642
+ });
643
+ const responseHeaders = new Headers(res.headers);
644
+ responseHeaders.delete("transfer-encoding");
645
+ responseHeaders.delete("connection");
646
+ return new Response(res.body, {
647
+ status: res.status,
648
+ statusText: res.statusText,
649
+ headers: responseHeaders
650
+ });
651
+ } catch (err) {
652
+ const msg = err instanceof Error ? err.message : "Upstream error";
653
+ return new Response(`Proxy error: ${msg}`, { status: 502 });
654
+ }
655
+ }
656
+
657
+ // src/index.ts
658
+ var { values } = parseArgs({
659
+ options: {
660
+ config: { type: "string", short: "c", default: "config.toml" },
661
+ "dry-run": { type: "boolean", default: false },
662
+ "mandatory-auth": { type: "boolean", default: false }
663
+ }
664
+ });
665
+ var configPath = values.config;
666
+ console.log(`[openape-proxy] Loading config from ${configPath}`);
667
+ var config = loadMultiAgentConfig(configPath, {
668
+ mandatoryAuth: values["mandatory-auth"] || void 0
669
+ });
670
+ initAudit(config.proxy.audit_log);
671
+ if (values["dry-run"]) {
672
+ console.log("[openape-proxy] DRY RUN mode \u2014 logging only, not blocking");
673
+ console.log("[openape-proxy] Config loaded:");
674
+ console.log(` Listen: ${config.proxy.listen}`);
675
+ console.log(` Default action: ${config.proxy.default_action}`);
676
+ console.log(` Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
677
+ console.log(` Agents: ${config.agents.length}`);
678
+ for (const agent of config.agents) {
679
+ const allowCount = agent.allow?.length ?? 0;
680
+ const denyCount = agent.deny?.length ?? 0;
681
+ const grantCount = agent.grant_required?.length ?? 0;
682
+ console.log(` ${agent.email} (${agent.idp_url}) \u2014 ${allowCount} allow, ${denyCount} deny, ${grantCount} grant`);
683
+ }
684
+ process.exit(0);
685
+ }
686
+ var handler = createNodeHandler(config);
687
+ var port = Number.parseInt(config.proxy.listen.split(":")[1] || "9090");
688
+ var hostname = config.proxy.listen.split(":")[0] || "127.0.0.1";
689
+ var server = createServer(handler.handleRequest);
690
+ server.on("connect", handler.handleConnect);
691
+ server.listen(port, hostname, () => {
692
+ const addr = server.address();
693
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
694
+ console.log(`[openape-proxy] Listening on http://${hostname}:${actualPort}`);
695
+ console.log(`[openape-proxy] CONNECT tunneling enabled`);
696
+ console.log(`[openape-proxy] Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
697
+ console.log(`[openape-proxy] Agents: ${config.agents.map((a) => a.email).join(", ")}`);
698
+ console.log(`[openape-proxy] Default action: ${config.proxy.default_action}`);
699
+ });
700
+ process.on("SIGINT", () => {
701
+ console.log("\n[openape-proxy] Shutting down...");
702
+ server.close();
703
+ process.exit(0);
704
+ });
705
+ process.on("SIGTERM", () => {
706
+ console.log("[openape-proxy] Shutting down...");
707
+ server.close();
708
+ process.exit(0);
709
+ });
710
+ //# sourceMappingURL=index.js.map