@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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/hydra/client.js +61 -0
- package/dist/hydra/client.js.map +1 -0
- package/dist/hydra/ws.js +178 -0
- package/dist/hydra/ws.js.map +1 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/server/auth.js +104 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/http.js +128 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/routes-agents.js +14 -0
- package/dist/server/routes-agents.js.map +1 -0
- package/dist/server/routes-files.js +170 -0
- package/dist/server/routes-files.js.map +1 -0
- package/dist/server/routes-root.js +67 -0
- package/dist/server/routes-root.js.map +1 -0
- package/dist/server/routes-sessions.js +104 -0
- package/dist/server/routes-sessions.js.map +1 -0
- package/dist/server/title-cache.js +59 -0
- package/dist/server/title-cache.js.map +1 -0
- package/dist/server/ws-bridge.js +261 -0
- package/dist/server/ws-bridge.js.map +1 -0
- package/dist/ui/index.html +2151 -0
- package/dist/util/csrf.js +57 -0
- package/dist/util/csrf.js.map +1 -0
- package/dist/util/log.js +46 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/paths.js +24 -0
- package/dist/util/paths.js.map +1 -0
- package/package.json +63 -0
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":";AACA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAE3B,SAAS,mBAAmB,CAAC,IAAY,EAAE,MAAe;IACxD,MAAM,UAAU,GACd,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,KAAK;QACd,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,OAAO,CAAC;IACnB,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,yCAAyC,IAAI,iGAAiG,CAC/I,CAAC;IACJ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,SAAS,EAAE,CAAC;QACZ,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAEjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEvB,mBAAmB,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,MAAM;QACpB,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC;QACnC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAEtC,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAE9B,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC7B,qBAAqB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAChC,mBAAmB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC9B,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAE7B,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAEzE,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEhC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,GAAG,GAAG,GAAG,MAAM,MAAM,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,WAAW,aAAa,OAAO,EAAE,CAAC;IACvG,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACpC,GAAG,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;IACnD,GAAG,CAAC,IAAI,CAAC,gBAAgB,MAAM,MAAM,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACjF,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;IAEzB,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,GAAG,CAAC,IAAI,CAAC,YAAY,MAAM,iBAAiB,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,mBAAoB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,GAAW;IAC9C,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,6BAA6B,IAAI,KAAM,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;;;;;;;;;;;;CAYH,CACE,CAAC;AACJ,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACxC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -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":"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"}
|
|
@@ -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"}
|