@hydra-acp/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync, chmodSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { loadConfig } from "./config.js";
5
+ import { HydraRestClient } from "./hydra/client.js";
6
+ import { ensureAuthkey, rotateAuthkey } from "./server/auth.js";
7
+ import { buildContext, createServer } from "./server/http.js";
8
+ import { registerSessionRoutes } from "./server/routes-sessions.js";
9
+ import { registerAgentRoutes } from "./server/routes-agents.js";
10
+ import { registerFileRoutes } from "./server/routes-files.js";
11
+ import { registerRootRoutes } from "./server/routes-root.js";
12
+ import { attachWsBridge } from "./server/ws-bridge.js";
13
+ import { logger, setDebug } from "./util/log.js";
14
+ const log = logger("main");
15
+ function ensureLoopbackOrTls(host, hasTls) {
16
+ const isLoopback = host === "127.0.0.1" ||
17
+ host === "::1" ||
18
+ host === "localhost" ||
19
+ host === "[::1]";
20
+ if (!isLoopback && !hasTls) {
21
+ throw new Error(`Refusing to bind to non-loopback host ${host} without TLS configured. Set BROWSER_TLS_CERT and BROWSER_TLS_KEY in ~/.hydra-acp-browser.conf.`);
22
+ }
23
+ }
24
+ async function main(argv) {
25
+ if (argv.includes("--help") || argv.includes("-h")) {
26
+ printHelp();
27
+ return;
28
+ }
29
+ const rotate = argv.includes("--rotate-authkey");
30
+ const config = loadConfig();
31
+ setDebug(config.debug);
32
+ ensureLoopbackOrTls(config.browserHost, !!config.tls);
33
+ const authkey = rotate
34
+ ? rotateAuthkey(config.authkeyFile)
35
+ : ensureAuthkey(config.authkeyFile);
36
+ const rest = new HydraRestClient(config.hydraDaemonUrl, config.hydraToken);
37
+ const ctx = buildContext(config, rest, authkey);
38
+ const app = createServer(ctx);
39
+ registerRootRoutes(app, ctx);
40
+ registerSessionRoutes(app, ctx);
41
+ registerAgentRoutes(app, ctx);
42
+ registerFileRoutes(app, ctx);
43
+ await app.listen({ host: config.browserHost, port: config.browserPort });
44
+ attachWsBridge(app.server, ctx);
45
+ const scheme = config.tls ? "https" : "http";
46
+ const url = `${scheme}://${displayHost(config.browserHost)}:${config.browserPort}/?authkey=${authkey}`;
47
+ writeLinkFile(config.linkFile, url);
48
+ log.info(`hydra daemon: ${config.hydraDaemonUrl}`);
49
+ log.info(`listening on ${scheme}://${config.browserHost}:${config.browserPort}`);
50
+ log.info(`Open: ${url}`);
51
+ const shutdown = async (signal) => {
52
+ log.info(`received ${signal}, shutting down`);
53
+ try {
54
+ await app.close();
55
+ }
56
+ catch (err) {
57
+ log.warn(`shutdown error: ${err.message}`);
58
+ }
59
+ process.exit(0);
60
+ };
61
+ process.on("SIGINT", () => void shutdown("SIGINT"));
62
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
63
+ }
64
+ function displayHost(host) {
65
+ if (host === "0.0.0.0") {
66
+ return "127.0.0.1";
67
+ }
68
+ return host;
69
+ }
70
+ function writeLinkFile(path, url) {
71
+ try {
72
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
73
+ writeFileSync(path, url + "\n", { mode: 0o600 });
74
+ chmodSync(path, 0o600);
75
+ }
76
+ catch (err) {
77
+ log.warn(`unable to write link file ${path}: ${err.message}`);
78
+ }
79
+ }
80
+ function printHelp() {
81
+ process.stdout.write(`hydra-acp-browser — web UI extension for hydra-acp
82
+
83
+ Usage:
84
+ hydra-acp-browser Start the server.
85
+ hydra-acp-browser --rotate-authkey
86
+ Generate a fresh authkey, kicking existing
87
+ browsers, and exit after starting.
88
+ hydra-acp-browser --help Show this message.
89
+
90
+ Config: ~/.hydra-acp-browser.conf (KEY=VALUE).
91
+ When run as an hydra-acp extension, HYDRA_ACP_DAEMON_URL / HYDRA_ACP_TOKEN /
92
+ HYDRA_ACP_WS_URL are injected automatically.
93
+ `);
94
+ }
95
+ main(process.argv.slice(2)).catch((err) => {
96
+ log.error(err);
97
+ process.exit(1);
98
+ });
99
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { randomBytes, timingSafeEqual } from "node:crypto";
4
+ import { logger } from "../util/log.js";
5
+ const log = logger("auth");
6
+ export const COOKIE_NAME = "hb_authkey";
7
+ export const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
8
+ export function ensureAuthkey(path) {
9
+ try {
10
+ const existing = readFileSync(path, "utf8").trim();
11
+ if (existing.length >= 32) {
12
+ return existing;
13
+ }
14
+ log.warn(`existing authkey at ${path} too short; rotating`);
15
+ }
16
+ catch {
17
+ // Doesn't exist yet or unreadable; we'll create it.
18
+ }
19
+ return rotateAuthkey(path);
20
+ }
21
+ export function rotateAuthkey(path) {
22
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
23
+ const key = randomBytes(32).toString("hex");
24
+ writeFileSync(path, key + "\n", { mode: 0o600 });
25
+ chmodSync(path, 0o600);
26
+ log.info(`authkey written to ${path}`);
27
+ return key;
28
+ }
29
+ export function constantTimeKeyMatch(a, b) {
30
+ if (typeof a !== "string" || typeof b !== "string") {
31
+ return false;
32
+ }
33
+ const aBuf = Buffer.from(a);
34
+ const bBuf = Buffer.from(b);
35
+ if (aBuf.length !== bBuf.length) {
36
+ return false;
37
+ }
38
+ return timingSafeEqual(aBuf, bBuf);
39
+ }
40
+ // Simple per-IP rate limiter for failed auth attempts. 10 failures in 15 min
41
+ // triggers a temporary block.
42
+ export class AuthRateLimiter {
43
+ entries = new Map();
44
+ maxFails = 10;
45
+ windowMs = 15 * 60 * 1000;
46
+ isBlocked(ip) {
47
+ const e = this.entries.get(ip);
48
+ if (!e) {
49
+ return false;
50
+ }
51
+ if (Date.now() - e.windowStart > this.windowMs) {
52
+ this.entries.delete(ip);
53
+ return false;
54
+ }
55
+ return e.fails >= this.maxFails;
56
+ }
57
+ recordFailure(ip) {
58
+ const now = Date.now();
59
+ const e = this.entries.get(ip);
60
+ if (!e || now - e.windowStart > this.windowMs) {
61
+ this.entries.set(ip, { fails: 1, windowStart: now });
62
+ return;
63
+ }
64
+ e.fails += 1;
65
+ }
66
+ recordSuccess(ip) {
67
+ this.entries.delete(ip);
68
+ }
69
+ }
70
+ export function buildSetCookie(value, opts) {
71
+ const parts = [
72
+ `${COOKIE_NAME}=${value}`,
73
+ "HttpOnly",
74
+ "SameSite=Strict",
75
+ "Path=/",
76
+ `Max-Age=${opts.maxAgeSeconds}`,
77
+ ];
78
+ if (opts.secure) {
79
+ parts.push("Secure");
80
+ }
81
+ return parts.join("; ");
82
+ }
83
+ export function buildClearCookie() {
84
+ return `${COOKIE_NAME}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`;
85
+ }
86
+ export function parseCookies(header) {
87
+ const out = new Map();
88
+ if (!header) {
89
+ return out;
90
+ }
91
+ for (const part of header.split(";")) {
92
+ const eq = part.indexOf("=");
93
+ if (eq === -1) {
94
+ continue;
95
+ }
96
+ const k = part.slice(0, eq).trim();
97
+ const v = part.slice(eq + 1).trim();
98
+ if (k.length > 0) {
99
+ out.set(k, v);
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import Fastify from "fastify";
4
+ import { logger } from "../util/log.js";
5
+ import { buildSecurityContext, checkStateChanging } from "../util/csrf.js";
6
+ import { AuthRateLimiter, COOKIE_NAME, buildSetCookie, constantTimeKeyMatch, parseCookies, } from "./auth.js";
7
+ const log = logger("http");
8
+ export function createServer(ctx) {
9
+ const httpsOptions = ctx.config.tls
10
+ ? {
11
+ cert: readFileSync(ctx.config.tls.cert),
12
+ key: readFileSync(ctx.config.tls.key),
13
+ }
14
+ : undefined;
15
+ const app = Fastify({
16
+ logger: false,
17
+ https: httpsOptions ?? null,
18
+ });
19
+ app.addHook("onRequest", async (request, reply) => {
20
+ const nonce = randomBytes(16).toString("base64");
21
+ request.cspNonce = nonce;
22
+ setSecurityHeaders(reply, nonce, ctx.scheme === "https");
23
+ });
24
+ app.addHook("onRequest", async (request, reply) => {
25
+ if (request.routeOptions.config?.skipCsrf) {
26
+ return;
27
+ }
28
+ const headers = request.headers;
29
+ const result = checkStateChanging(ctx.security, headers);
30
+ if (!result.ok) {
31
+ log.warn(`csrf reject ${request.method} ${request.url} ${result.reason}`);
32
+ reply.code(result.status).send({ error: result.reason });
33
+ }
34
+ });
35
+ app.addHook("onRequest", async (request, reply) => {
36
+ if (request.routeOptions.config?.skipAuth) {
37
+ return;
38
+ }
39
+ if (!authenticate(request, reply, ctx)) {
40
+ return reply;
41
+ }
42
+ });
43
+ return app;
44
+ }
45
+ function setSecurityHeaders(reply, nonce, secure) {
46
+ const csp = [
47
+ "default-src 'self'",
48
+ `script-src 'self' 'nonce-${nonce}'`,
49
+ `style-src 'self' 'nonce-${nonce}'`,
50
+ "img-src 'self' data:",
51
+ "connect-src 'self'",
52
+ "frame-ancestors 'none'",
53
+ "base-uri 'self'",
54
+ "form-action 'self'",
55
+ ].join("; ");
56
+ reply.header("Content-Security-Policy", csp);
57
+ reply.header("X-Frame-Options", "DENY");
58
+ reply.header("X-Content-Type-Options", "nosniff");
59
+ reply.header("Referrer-Policy", "no-referrer");
60
+ reply.header("Cache-Control", "no-store");
61
+ if (secure) {
62
+ reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
63
+ }
64
+ }
65
+ // Returns true if the request is authenticated; otherwise it has already
66
+ // written a response and the caller should stop.
67
+ function authenticate(request, reply, ctx) {
68
+ const ip = request.ip ?? "unknown";
69
+ if (ctx.rateLimiter.isBlocked(ip)) {
70
+ reply.code(429).send({ error: "rate limited" });
71
+ return false;
72
+ }
73
+ const cookies = parseCookies(request.headers.cookie);
74
+ const provided = cookies.get(COOKIE_NAME);
75
+ if (!provided) {
76
+ ctx.rateLimiter.recordFailure(ip);
77
+ reply.code(401).send({ error: "unauthorized" });
78
+ return false;
79
+ }
80
+ if (!constantTimeKeyMatch(provided, ctx.authkey)) {
81
+ ctx.rateLimiter.recordFailure(ip);
82
+ reply.code(401).send({ error: "unauthorized" });
83
+ return false;
84
+ }
85
+ ctx.rateLimiter.recordSuccess(ip);
86
+ return true;
87
+ }
88
+ // Check + set cookie for a `?authkey=…` query against the root path. Returns
89
+ // "redirect" (cookie set, caller should 302), "ok" (already authenticated,
90
+ // caller serves the SPA), or "deny" (caller serves the login instructions).
91
+ export function processRootAuth(request, reply, ctx) {
92
+ const ip = request.ip ?? "unknown";
93
+ if (ctx.rateLimiter.isBlocked(ip)) {
94
+ reply.code(429).send({ error: "rate limited" });
95
+ return "deny";
96
+ }
97
+ const query = (request.query ?? {});
98
+ if (typeof query.authkey === "string") {
99
+ if (constantTimeKeyMatch(query.authkey, ctx.authkey)) {
100
+ ctx.rateLimiter.recordSuccess(ip);
101
+ reply.header("Set-Cookie", buildSetCookie(ctx.authkey, {
102
+ secure: ctx.scheme === "https",
103
+ maxAgeSeconds: 60 * 60 * 24 * 30,
104
+ }));
105
+ return "redirect";
106
+ }
107
+ ctx.rateLimiter.recordFailure(ip);
108
+ return "deny";
109
+ }
110
+ const cookies = parseCookies(request.headers.cookie);
111
+ const provided = cookies.get(COOKIE_NAME);
112
+ if (provided && constantTimeKeyMatch(provided, ctx.authkey)) {
113
+ return "ok";
114
+ }
115
+ return "deny";
116
+ }
117
+ export function buildContext(config, rest, authkey) {
118
+ const scheme = config.tls ? "https" : "http";
119
+ return {
120
+ config,
121
+ rest,
122
+ authkey,
123
+ security: buildSecurityContext(config.browserHost, config.browserPort, scheme, config.allowedHosts),
124
+ rateLimiter: new AuthRateLimiter(),
125
+ scheme,
126
+ };
127
+ }
128
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,OAAyE,MAAM,SAAS,CAAC;AAGhG,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAwB,MAAM,iBAAiB,CAAC;AACjG,OAAO,EACL,eAAe,EACf,WAAW,EACX,cAAc,EACd,oBAAoB,EACpB,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAwB3B,MAAM,UAAU,YAAY,CAAC,GAAkB;IAC7C,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG;QACjC,CAAC,CAAC;YACE,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YACvC,GAAG,EAAE,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;SACtC;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,GAAG,GAAG,OAAO,CAAC;QAClB,MAAM,EAAE,KAAK;QACb,KAAK,EAAE,YAAY,IAAI,IAAI;KAC5B,CAAC,CAAC;IAEH,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChD,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACjD,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC;QACzB,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChD,IAAI,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,GAAG,CAAC,IAAI,CAAC,eAAe,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChD,IAAI,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACvC,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,kBAAkB,CACzB,KAAmB,EACnB,KAAa,EACb,MAAe;IAEf,MAAM,GAAG,GAAG;QACV,oBAAoB;QACpB,4BAA4B,KAAK,GAAG;QACpC,2BAA2B,KAAK,GAAG;QACnC,sBAAsB;QACtB,oBAAoB;QACpB,wBAAwB;QACxB,iBAAiB;QACjB,oBAAoB;KACrB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,KAAK,CAAC,MAAM,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IAC7C,KAAK,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IAClD,KAAK,CAAC,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;IAC/C,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAC1C,IAAI,MAAM,EAAE,CAAC;QACX,KAAK,CAAC,MAAM,CACV,2BAA2B,EAC3B,qCAAqC,CACtC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,iDAAiD;AACjD,SAAS,YAAY,CACnB,OAAuB,EACvB,KAAmB,EACnB,GAAkB;IAElB,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,SAAS,CAAC;IACnC,IAAI,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,oBAAoB,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAC7B,OAAuB,EACvB,KAAmB,EACnB,GAAkB;IAElB,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,SAAS,CAAC;IACnC,IAAI,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAyB,CAAC;IAC5D,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACtC,IAAI,oBAAoB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;YAClC,KAAK,CAAC,MAAM,CACV,YAAY,EACZ,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE;gBAC1B,MAAM,EAAE,GAAG,CAAC,MAAM,KAAK,OAAO;gBAC9B,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;aACjC,CAAC,CACH,CAAC;YACF,OAAO,UAAU,CAAC;QACpB,CAAC;QACD,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAClC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,QAAQ,IAAI,oBAAoB,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,IAAqB,EACrB,OAAe;IAEf,MAAM,MAAM,GAAqB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/D,OAAO;QACL,MAAM;QACN,IAAI;QACJ,OAAO;QACP,QAAQ,EAAE,oBAAoB,CAC5B,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,WAAW,EAClB,MAAM,EACN,MAAM,CAAC,YAAY,CACpB;QACD,WAAW,EAAE,IAAI,eAAe,EAAE;QAClC,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { HydraRestError } from "../hydra/client.js";
2
+ export function registerAgentRoutes(app, ctx) {
3
+ app.get("/api/agents", async (_request, reply) => {
4
+ try {
5
+ const result = await ctx.rest.listAgents();
6
+ reply.send(result);
7
+ }
8
+ catch (err) {
9
+ const status = err instanceof HydraRestError ? err.status : 502;
10
+ reply.code(status).send({ error: err.message });
11
+ }
12
+ });
13
+ }
14
+ //# sourceMappingURL=routes-agents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-agents.js","sourceRoot":"","sources":["../../src/server/routes-agents.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGpD,MAAM,UAAU,mBAAmB,CACjC,GAAoB,EACpB,GAAkB;IAElB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QAC/C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;YAChE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,170 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ // Resolve a request `path` (relative to the session's cwd) and verify it is
4
+ // inside the cwd after symlink resolution. Returns the realpath if safe,
5
+ // otherwise throws PathScopeError.
6
+ export async function resolveScopedPath(cwd, requested) {
7
+ const cwdReal = await fsp.realpath(cwd);
8
+ const cwdReq = requested.length === 0 ? "." : requested;
9
+ const target = resolve(cwdReal, cwdReq);
10
+ let real;
11
+ try {
12
+ real = await fsp.realpath(target);
13
+ }
14
+ catch {
15
+ real = target;
16
+ }
17
+ const cwdWithSep = cwdReal.endsWith(sep) ? cwdReal : cwdReal + sep;
18
+ if (real !== cwdReal && !real.startsWith(cwdWithSep)) {
19
+ throw new PathScopeError(`path escapes cwd: requested=${requested} resolved=${real} cwd=${cwdReal}`);
20
+ }
21
+ return real;
22
+ }
23
+ export class PathScopeError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "PathScopeError";
27
+ }
28
+ }
29
+ async function lookupSessionCwd(ctx, sessionId) {
30
+ const result = await ctx.rest.listSessions({ all: true });
31
+ const match = result.sessions.find((s) => s.sessionId === sessionId);
32
+ return match?.cwd;
33
+ }
34
+ export function registerFileRoutes(app, ctx) {
35
+ app.post("/api/files/list", async (request, reply) => {
36
+ const body = (request.body ?? {});
37
+ if (!body.sessionId) {
38
+ reply.code(400).send({ error: "sessionId required" });
39
+ return;
40
+ }
41
+ const cwd = await lookupSessionCwd(ctx, body.sessionId);
42
+ if (!cwd) {
43
+ reply.code(404).send({ error: "session not found" });
44
+ return;
45
+ }
46
+ let target;
47
+ try {
48
+ target = await resolveScopedPath(cwd, body.path ?? "");
49
+ }
50
+ catch (err) {
51
+ if (err instanceof PathScopeError) {
52
+ reply.code(400).send({ error: "path out of scope" });
53
+ return;
54
+ }
55
+ reply.code(500).send({ error: err.message });
56
+ return;
57
+ }
58
+ let stat;
59
+ try {
60
+ stat = await fsp.stat(target);
61
+ }
62
+ catch (err) {
63
+ reply.code(404).send({ error: err.message });
64
+ return;
65
+ }
66
+ if (!stat.isDirectory()) {
67
+ reply.code(400).send({ error: "not a directory" });
68
+ return;
69
+ }
70
+ const dirents = await fsp.readdir(target, { withFileTypes: true });
71
+ const entries = [];
72
+ for (const d of dirents) {
73
+ const childPath = resolve(target, d.name);
74
+ let s;
75
+ try {
76
+ s = await fsp.stat(childPath);
77
+ }
78
+ catch {
79
+ continue;
80
+ }
81
+ let kind = "other";
82
+ if (s.isDirectory()) {
83
+ kind = "dir";
84
+ }
85
+ else if (s.isFile()) {
86
+ kind = "file";
87
+ }
88
+ entries.push({
89
+ name: d.name,
90
+ kind,
91
+ size: s.size,
92
+ mtimeMs: s.mtimeMs,
93
+ });
94
+ }
95
+ entries.sort((a, b) => {
96
+ if (a.kind !== b.kind) {
97
+ return a.kind === "dir" ? -1 : 1;
98
+ }
99
+ return a.name.localeCompare(b.name);
100
+ });
101
+ reply.send({ cwd, path: body.path ?? "", entries });
102
+ });
103
+ app.post("/api/files/read", async (request, reply) => {
104
+ const body = (request.body ?? {});
105
+ if (!body.sessionId) {
106
+ reply.code(400).send({ error: "sessionId required" });
107
+ return;
108
+ }
109
+ if (!body.path) {
110
+ reply.code(400).send({ error: "path required" });
111
+ return;
112
+ }
113
+ const cwd = await lookupSessionCwd(ctx, body.sessionId);
114
+ if (!cwd) {
115
+ reply.code(404).send({ error: "session not found" });
116
+ return;
117
+ }
118
+ let target;
119
+ try {
120
+ target = await resolveScopedPath(cwd, body.path);
121
+ }
122
+ catch (err) {
123
+ if (err instanceof PathScopeError) {
124
+ reply.code(400).send({ error: "path out of scope" });
125
+ return;
126
+ }
127
+ reply.code(500).send({ error: err.message });
128
+ return;
129
+ }
130
+ let stat;
131
+ try {
132
+ stat = await fsp.stat(target);
133
+ }
134
+ catch (err) {
135
+ reply.code(404).send({ error: err.message });
136
+ return;
137
+ }
138
+ if (!stat.isFile()) {
139
+ reply.code(400).send({ error: "not a file" });
140
+ return;
141
+ }
142
+ const max = Math.min(body.maxBytes ?? ctx.config.fileMaxBytes, ctx.config.fileMaxBytes);
143
+ if (stat.size > max) {
144
+ reply.code(413).send({ error: `file larger than ${max} bytes` });
145
+ return;
146
+ }
147
+ const buf = await fsp.readFile(target);
148
+ if (containsBinary(buf)) {
149
+ reply.code(415).send({ error: "binary file" });
150
+ return;
151
+ }
152
+ reply.send({
153
+ path: body.path,
154
+ size: stat.size,
155
+ mtimeMs: stat.mtimeMs,
156
+ content: buf.toString("utf8"),
157
+ });
158
+ });
159
+ }
160
+ // Heuristic: presence of a NUL byte in the first 8 KiB indicates binary.
161
+ function containsBinary(buf) {
162
+ const limit = Math.min(buf.length, 8192);
163
+ for (let i = 0; i < limit; i++) {
164
+ if (buf[i] === 0) {
165
+ return true;
166
+ }
167
+ }
168
+ return false;
169
+ }
170
+ //# sourceMappingURL=routes-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-files.js","sourceRoot":"","sources":["../../src/server/routes-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { processRootAuth } from "./http.js";
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ // In dist layout: dist/server/routes-root.js → ../ui/index.html
7
+ const UI_DIR = resolve(here, "..", "ui");
8
+ let cachedHtml;
9
+ async function loadHtml() {
10
+ if (cachedHtml) {
11
+ return cachedHtml;
12
+ }
13
+ const path = resolve(UI_DIR, "index.html");
14
+ cachedHtml = await fsp.readFile(path, "utf8");
15
+ return cachedHtml;
16
+ }
17
+ export function registerRootRoutes(app, ctx) {
18
+ app.get("/", {
19
+ config: { skipAuth: true, skipCsrf: true },
20
+ }, async (request, reply) => {
21
+ const result = processRootAuth(request, reply, ctx);
22
+ if (result === "redirect") {
23
+ reply.code(302).header("Location", "/").send();
24
+ return;
25
+ }
26
+ reply.header("Content-Type", "text/html; charset=utf-8");
27
+ if (result === "deny") {
28
+ reply
29
+ .code(401)
30
+ .send(loginPage(request.cspNonce ?? ""));
31
+ return;
32
+ }
33
+ const html = await loadHtml();
34
+ reply.send(injectNonce(html, request.cspNonce ?? ""));
35
+ });
36
+ app.get("/favicon.ico", { config: { skipAuth: true, skipCsrf: true } }, async (_request, reply) => {
37
+ reply.code(204).send();
38
+ });
39
+ }
40
+ // Replace the literal placeholder __CSP_NONCE__ in the HTML template with
41
+ // the per-request nonce.
42
+ function injectNonce(html, nonce) {
43
+ return html.replaceAll("__CSP_NONCE__", nonce);
44
+ }
45
+ function loginPage(nonce) {
46
+ return `<!doctype html>
47
+ <html lang="en">
48
+ <head>
49
+ <meta charset="utf-8">
50
+ <meta name="viewport" content="width=device-width,initial-scale=1">
51
+ <title>hydra-acp-browser</title>
52
+ <style nonce="${nonce}">
53
+ :root { color-scheme: dark; font-family: system-ui, sans-serif; }
54
+ body { background: #0e1116; color: #d6deeb; margin: 0; padding: 4rem 1.5rem; max-width: 38rem; margin-inline: auto; line-height: 1.5; }
55
+ h1 { font-size: 1.25rem; margin: 0 0 1rem; }
56
+ code { background: #1c2230; padding: 0.1rem 0.4rem; border-radius: 4px; }
57
+ .note { color: #7c8aa8; font-size: 0.95rem; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <h1>hydra-acp-browser</h1>
62
+ <p>Authentication required.</p>
63
+ <p class="note">Open the URL printed by the server (it contains <code>?authkey=…</code>). The link is also written to <code>~/.hydra-acp-browser/link</code>.</p>
64
+ </body>
65
+ </html>`;
66
+ }
67
+ //# sourceMappingURL=routes-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-root.js","sourceRoot":"","sources":["../../src/server/routes-root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAsB,MAAM,WAAW,CAAC;AAEhE,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErD,gEAAgE;AAChE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEzC,IAAI,UAA8B,CAAC;AAEnC,KAAK,UAAU,QAAQ;IACrB,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,UAAU,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9C,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,GAAoB,EACpB,GAAkB;IAElB,GAAG,CAAC,GAAG,CACL,GAAG,EACH;QACE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;KAC3C,EACD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACpD,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAC;QACzD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,KAAK;iBACF,IAAI,CAAC,GAAG,CAAC;iBACT,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC;IACxD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,cAAc,EACd,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAC9C,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC,CACF,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,yBAAyB;AACzB,SAAS,WAAW,CAAC,IAAY,EAAE,KAAa;IAC9C,OAAO,IAAI,CAAC,UAAU,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO;;;;;;gBAMO,KAAK;;;;;;;;;;;;;QAab,CAAC;AACT,CAAC"}