hanoman 0.1.41 → 0.1.43
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/build-info.json +3 -3
- package/dist/cli.js +3 -0
- package/dist/server.js +526 -441
- package/package.json +1 -1
- package/web/assets/index-TRbs6vrh.css +41 -0
- package/web/assets/{index-BLt4wnhB.js → index-e01zHm_c.js} +1531 -1529
- package/web/index.html +2 -2
- package/web/assets/index-BeFLNV7l.css +0 -41
package/dist/server.js
CHANGED
|
@@ -6119,6 +6119,9 @@ var init_api = __esm({
|
|
|
6119
6119
|
sessionReviewFile: (id, path2) => `${API}/terminal/sessions/${id}/review/${path2}`,
|
|
6120
6120
|
sessionIntegrate: (id) => `${API}/terminal/sessions/${id}/integrate`,
|
|
6121
6121
|
terminalWs: (id) => `${API}/terminal/sessions/${id}/ws`,
|
|
6122
|
+
// SPEC-816 · lampiran gambar sesi (multipart). Di bawah prefix /terminal supaya ikut capability
|
|
6123
|
+
// `sessions` yang sudah ada — POST menurunkan cabang tulisnya tanpa perubahan peta.
|
|
6124
|
+
terminalAttachments: (id) => `${API}/terminal/sessions/${id}/attachments`,
|
|
6122
6125
|
// SPEC-362 · ADR-0079 · riwayat sesi. Di bawah prefix /terminal supaya ikut capability
|
|
6123
6126
|
// `sessions` yang sudah ada (services/agent-capabilities.ts) tanpa menambah domain baru.
|
|
6124
6127
|
sessionHistory: (qs = "") => `${API}/terminal/history${qs}`,
|
|
@@ -9362,6 +9365,400 @@ var init_session_id = __esm({
|
|
|
9362
9365
|
}
|
|
9363
9366
|
});
|
|
9364
9367
|
|
|
9368
|
+
// src/services/secret-box.ts
|
|
9369
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
9370
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9371
|
+
import { dirname as dirname6, join as join4 } from "node:path";
|
|
9372
|
+
function isEncrypted(value) {
|
|
9373
|
+
return value.startsWith(ENC_PREFIX);
|
|
9374
|
+
}
|
|
9375
|
+
function encryptWithKey(plain, key) {
|
|
9376
|
+
const iv = randomBytes(IV_BYTES);
|
|
9377
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
9378
|
+
const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
9379
|
+
const tag2 = cipher.getAuthTag();
|
|
9380
|
+
return `${ENC_PREFIX}${iv.toString("base64url")}:${tag2.toString("base64url")}:${body.toString("base64url")}`;
|
|
9381
|
+
}
|
|
9382
|
+
function decryptWithKey(value, key) {
|
|
9383
|
+
if (!isEncrypted(value)) return value;
|
|
9384
|
+
const parts = value.slice(ENC_PREFIX.length).split(":");
|
|
9385
|
+
if (parts.length !== 3) return null;
|
|
9386
|
+
const [ivRaw, tagRaw, bodyRaw] = parts;
|
|
9387
|
+
try {
|
|
9388
|
+
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivRaw, "base64url"));
|
|
9389
|
+
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
9390
|
+
return Buffer.concat([decipher.update(Buffer.from(bodyRaw, "base64url")), decipher.final()]).toString("utf8");
|
|
9391
|
+
} catch {
|
|
9392
|
+
return null;
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
function fromEnv(raw2) {
|
|
9396
|
+
for (const enc2 of ["base64url", "base64", "hex"]) {
|
|
9397
|
+
const buf = Buffer.from(raw2, enc2);
|
|
9398
|
+
if (buf.length === KEY_BYTES) return buf;
|
|
9399
|
+
}
|
|
9400
|
+
return null;
|
|
9401
|
+
}
|
|
9402
|
+
function secretKeyPath() {
|
|
9403
|
+
return join4(resolveHome(), "secret.key");
|
|
9404
|
+
}
|
|
9405
|
+
function secretKey() {
|
|
9406
|
+
if (cached) return cached;
|
|
9407
|
+
const override = process.env.HANOMAN_SECRET_KEY?.trim();
|
|
9408
|
+
if (override) {
|
|
9409
|
+
const key2 = fromEnv(override);
|
|
9410
|
+
if (!key2) throw new Error(`HANOMAN_SECRET_KEY harus 32 byte (hex/base64), dapat ${override.length} karakter`);
|
|
9411
|
+
cached = key2;
|
|
9412
|
+
return key2;
|
|
9413
|
+
}
|
|
9414
|
+
const path2 = secretKeyPath();
|
|
9415
|
+
if (existsSync5(path2)) {
|
|
9416
|
+
const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
|
|
9417
|
+
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9418
|
+
cached = key2;
|
|
9419
|
+
return key2;
|
|
9420
|
+
}
|
|
9421
|
+
const key = randomBytes(KEY_BYTES);
|
|
9422
|
+
mkdirSync3(dirname6(path2), { recursive: true });
|
|
9423
|
+
writeFileSync(path2, key.toString("base64url"), { mode: 384 });
|
|
9424
|
+
chmodSync2(path2, 384);
|
|
9425
|
+
cached = key;
|
|
9426
|
+
return key;
|
|
9427
|
+
}
|
|
9428
|
+
function encryptSecret(plain) {
|
|
9429
|
+
return encryptWithKey(plain, secretKey());
|
|
9430
|
+
}
|
|
9431
|
+
function decryptSecret(value) {
|
|
9432
|
+
return decryptWithKey(value, secretKey());
|
|
9433
|
+
}
|
|
9434
|
+
var ENC_PREFIX, ALGO, IV_BYTES, KEY_BYTES, cached;
|
|
9435
|
+
var init_secret_box = __esm({
|
|
9436
|
+
"src/services/secret-box.ts"() {
|
|
9437
|
+
"use strict";
|
|
9438
|
+
init_src2();
|
|
9439
|
+
ENC_PREFIX = "enc:v1:";
|
|
9440
|
+
ALGO = "aes-256-gcm";
|
|
9441
|
+
IV_BYTES = 12;
|
|
9442
|
+
KEY_BYTES = 32;
|
|
9443
|
+
cached = null;
|
|
9444
|
+
}
|
|
9445
|
+
});
|
|
9446
|
+
|
|
9447
|
+
// src/config.ts
|
|
9448
|
+
var config_exports = {};
|
|
9449
|
+
__export(config_exports, {
|
|
9450
|
+
clearConfig: () => clearConfig,
|
|
9451
|
+
effectiveBool: () => effectiveBool,
|
|
9452
|
+
effectiveInt: () => effectiveInt,
|
|
9453
|
+
effectiveStr: () => effectiveStr,
|
|
9454
|
+
loadConfig: () => loadConfig,
|
|
9455
|
+
rawDbValue: () => rawDbValue,
|
|
9456
|
+
setConfig: () => setConfig,
|
|
9457
|
+
setConfigsAtomic: () => setConfigsAtomic,
|
|
9458
|
+
sourceOf: () => sourceOf
|
|
9459
|
+
});
|
|
9460
|
+
async function loadConfig() {
|
|
9461
|
+
const rows = await prisma.runtimeConfig.findMany();
|
|
9462
|
+
const next = /* @__PURE__ */ new Map();
|
|
9463
|
+
for (const r of rows) {
|
|
9464
|
+
if (!isSecret(r.key)) {
|
|
9465
|
+
next.set(r.key, r.value);
|
|
9466
|
+
continue;
|
|
9467
|
+
}
|
|
9468
|
+
const plain = decryptSecret(r.value);
|
|
9469
|
+
if (plain === null) {
|
|
9470
|
+
console.error(`config: nilai '${r.key}' tak bisa didekripsi \u2014 diabaikan`);
|
|
9471
|
+
continue;
|
|
9472
|
+
}
|
|
9473
|
+
next.set(r.key, plain);
|
|
9474
|
+
}
|
|
9475
|
+
cache = next;
|
|
9476
|
+
}
|
|
9477
|
+
function rawDbValue(key) {
|
|
9478
|
+
return cache.get(key);
|
|
9479
|
+
}
|
|
9480
|
+
function effectiveStr(key) {
|
|
9481
|
+
return cache.get(key) ?? process.env[key] ?? configEntry(key)?.default;
|
|
9482
|
+
}
|
|
9483
|
+
function effectiveInt(key) {
|
|
9484
|
+
const v = effectiveStr(key);
|
|
9485
|
+
return v === void 0 ? void 0 : Number(v);
|
|
9486
|
+
}
|
|
9487
|
+
function effectiveBool(key) {
|
|
9488
|
+
const v = effectiveStr(key);
|
|
9489
|
+
return v === "1" || v === "true";
|
|
9490
|
+
}
|
|
9491
|
+
function sourceOf(key) {
|
|
9492
|
+
if (cache.has(key)) return "db";
|
|
9493
|
+
if (process.env[key] !== void 0) return "env";
|
|
9494
|
+
return "default";
|
|
9495
|
+
}
|
|
9496
|
+
async function setConfig(key, value) {
|
|
9497
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9498
|
+
await prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9499
|
+
cache.set(key, value);
|
|
9500
|
+
}
|
|
9501
|
+
async function setConfigsAtomic(values) {
|
|
9502
|
+
await prisma.$transaction(Object.entries(values).map(([key, value]) => {
|
|
9503
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9504
|
+
return prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9505
|
+
}));
|
|
9506
|
+
for (const [key, value] of Object.entries(values)) cache.set(key, value);
|
|
9507
|
+
}
|
|
9508
|
+
async function clearConfig(key) {
|
|
9509
|
+
await prisma.runtimeConfig.deleteMany({ where: { key } });
|
|
9510
|
+
cache.delete(key);
|
|
9511
|
+
}
|
|
9512
|
+
var cache, isSecret;
|
|
9513
|
+
var init_config2 = __esm({
|
|
9514
|
+
"src/config.ts"() {
|
|
9515
|
+
"use strict";
|
|
9516
|
+
init_db();
|
|
9517
|
+
init_src();
|
|
9518
|
+
init_secret_box();
|
|
9519
|
+
cache = /* @__PURE__ */ new Map();
|
|
9520
|
+
isSecret = (key) => configEntry(key)?.kind === "secret";
|
|
9521
|
+
}
|
|
9522
|
+
});
|
|
9523
|
+
|
|
9524
|
+
// src/services/webhooks/ssrf.ts
|
|
9525
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9526
|
+
import { isIPv4 } from "node:net";
|
|
9527
|
+
function validateWebhookUrl(raw2) {
|
|
9528
|
+
let url2;
|
|
9529
|
+
try {
|
|
9530
|
+
url2 = new URL(raw2.trim());
|
|
9531
|
+
} catch {
|
|
9532
|
+
return { ok: false, error: "URL tak valid" };
|
|
9533
|
+
}
|
|
9534
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:")
|
|
9535
|
+
return { ok: false, error: "hanya http atau https" };
|
|
9536
|
+
if (url2.username || url2.password)
|
|
9537
|
+
return { ok: false, error: "kredensial di URL tak diizinkan" };
|
|
9538
|
+
if (!url2.hostname) return { ok: false, error: "hostname kosong" };
|
|
9539
|
+
return { ok: true, url: url2 };
|
|
9540
|
+
}
|
|
9541
|
+
function blockedV4(ip) {
|
|
9542
|
+
const p = ip.split(".").map(Number);
|
|
9543
|
+
if (p.length !== 4 || p.some((n2) => !Number.isInteger(n2) || n2 < 0 || n2 > 255)) return true;
|
|
9544
|
+
const [a, b] = p;
|
|
9545
|
+
if (a === 0 || a === 127) return true;
|
|
9546
|
+
if (a === 10) return true;
|
|
9547
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
9548
|
+
if (a === 192 && b === 168) return true;
|
|
9549
|
+
if (a === 169 && b === 254) return true;
|
|
9550
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
9551
|
+
if (a === 192 && b === 0) return true;
|
|
9552
|
+
if (a >= 224) return true;
|
|
9553
|
+
return false;
|
|
9554
|
+
}
|
|
9555
|
+
function isBlockedAddress(ip) {
|
|
9556
|
+
const v = ip.trim().toLowerCase();
|
|
9557
|
+
if (!v) return true;
|
|
9558
|
+
const mapped = v.startsWith("::ffff:") ? v.slice(7) : v;
|
|
9559
|
+
if (isIPv4(mapped)) return blockedV4(mapped);
|
|
9560
|
+
if (!v.includes(":")) return true;
|
|
9561
|
+
if (v === "::" || v === "::1") return true;
|
|
9562
|
+
if (v.startsWith("fe80") || v.startsWith("fec0")) return true;
|
|
9563
|
+
if (/^f[cd]/.test(v)) return true;
|
|
9564
|
+
if (v.startsWith("ff")) return true;
|
|
9565
|
+
return false;
|
|
9566
|
+
}
|
|
9567
|
+
function checkUrlShape(raw2, allowPrivate) {
|
|
9568
|
+
const parsed = validateWebhookUrl(raw2);
|
|
9569
|
+
if (!parsed.ok) return parsed;
|
|
9570
|
+
if (allowPrivate) return parsed;
|
|
9571
|
+
const host2 = parsed.url.hostname.replace(/^\[|\]$/g, "");
|
|
9572
|
+
const literal = isIPv4(host2) || host2.includes(":");
|
|
9573
|
+
if (literal && isBlockedAddress(host2))
|
|
9574
|
+
return {
|
|
9575
|
+
ok: false,
|
|
9576
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9577
|
+
};
|
|
9578
|
+
if (host2 === "localhost" || host2.endsWith(".localhost"))
|
|
9579
|
+
return {
|
|
9580
|
+
ok: false,
|
|
9581
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9582
|
+
};
|
|
9583
|
+
return parsed;
|
|
9584
|
+
}
|
|
9585
|
+
async function checkDestination(url2, allowPrivate, lookup = defaultLookup) {
|
|
9586
|
+
if (allowPrivate) return { ok: true };
|
|
9587
|
+
const host2 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
9588
|
+
let addrs;
|
|
9589
|
+
try {
|
|
9590
|
+
addrs = await lookup(host2);
|
|
9591
|
+
} catch (e) {
|
|
9592
|
+
return { ok: false, error: `DNS gagal: ${e.message}` };
|
|
9593
|
+
}
|
|
9594
|
+
if (!addrs.length) return { ok: false, error: "DNS tak mengembalikan alamat" };
|
|
9595
|
+
for (const a of addrs)
|
|
9596
|
+
if (isBlockedAddress(a.address))
|
|
9597
|
+
return {
|
|
9598
|
+
ok: false,
|
|
9599
|
+
error: `alamat internal ditolak (${a.address}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9600
|
+
};
|
|
9601
|
+
return { ok: true };
|
|
9602
|
+
}
|
|
9603
|
+
var defaultLookup;
|
|
9604
|
+
var init_ssrf = __esm({
|
|
9605
|
+
"src/services/webhooks/ssrf.ts"() {
|
|
9606
|
+
"use strict";
|
|
9607
|
+
defaultLookup = (host2) => dnsLookup(host2, { all: true });
|
|
9608
|
+
}
|
|
9609
|
+
});
|
|
9610
|
+
|
|
9611
|
+
// src/services/safe-outbound-request.ts
|
|
9612
|
+
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
9613
|
+
import { request as httpRequest } from "node:http";
|
|
9614
|
+
import { request as httpsRequest } from "node:https";
|
|
9615
|
+
async function pinnedRequest(input) {
|
|
9616
|
+
return new Promise((resolve19, reject2) => {
|
|
9617
|
+
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
9618
|
+
const req = transport({
|
|
9619
|
+
protocol: input.url.protocol,
|
|
9620
|
+
hostname: input.url.hostname,
|
|
9621
|
+
port: input.url.port || void 0,
|
|
9622
|
+
method: input.method,
|
|
9623
|
+
path: `${input.url.pathname}${input.url.search}`,
|
|
9624
|
+
headers: input.headers,
|
|
9625
|
+
servername: input.url.hostname,
|
|
9626
|
+
// Node >= 20 menyalakan `autoSelectFamily` secara default: socket memanggil `lookup` dengan
|
|
9627
|
+
// `all: true` dan membaca `addresses[0].address` dari hasilnya. Menjawab dalam bentuk skalar
|
|
9628
|
+
// di situ memberi `undefined` → ERR_INVALID_IP_ADDRESS sebelum satu paket pun keluar, dan
|
|
9629
|
+
// pemanggil (sync tick, webhook) menelannya sebagai "offline". Kedua bentuk dijawab supaya
|
|
9630
|
+
// pinning tetap benar apa pun setelan family-nya.
|
|
9631
|
+
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
9632
|
+
timeout: input.connectMs
|
|
9633
|
+
}, (response) => {
|
|
9634
|
+
const chunks = [];
|
|
9635
|
+
let bytes = 0;
|
|
9636
|
+
response.on("data", (chunk) => {
|
|
9637
|
+
bytes += chunk.length;
|
|
9638
|
+
if (bytes > input.maxResponseBytes) {
|
|
9639
|
+
response.destroy(new Error("outbound response terlalu besar"));
|
|
9640
|
+
return;
|
|
9641
|
+
}
|
|
9642
|
+
chunks.push(chunk);
|
|
9643
|
+
});
|
|
9644
|
+
response.on("end", () => resolve19({
|
|
9645
|
+
status: response.statusCode ?? 0,
|
|
9646
|
+
headers: response.headers,
|
|
9647
|
+
body: Buffer.concat(chunks)
|
|
9648
|
+
}));
|
|
9649
|
+
});
|
|
9650
|
+
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
9651
|
+
timer9.unref?.();
|
|
9652
|
+
req.once("close", () => clearTimeout(timer9));
|
|
9653
|
+
req.once("error", reject2);
|
|
9654
|
+
if (input.body) req.write(input.body);
|
|
9655
|
+
req.end();
|
|
9656
|
+
});
|
|
9657
|
+
}
|
|
9658
|
+
async function safeRequest(options2, deps = {}) {
|
|
9659
|
+
if (options2.url.protocol !== "http:" && options2.url.protocol !== "https:") throw new Error("outbound scheme ditolak");
|
|
9660
|
+
if (options2.url.username || options2.url.password) throw new Error("outbound credential URL ditolak");
|
|
9661
|
+
const addresses = await (deps.lookupAll ?? defaultLookup2)(options2.url.hostname);
|
|
9662
|
+
if (!addresses.length) throw new Error("DNS tak mengembalikan alamat");
|
|
9663
|
+
if (!options2.allowPrivate) {
|
|
9664
|
+
const blocked = addresses.find((row) => isBlockedAddress(row.address));
|
|
9665
|
+
if (blocked) throw new Error(`alamat internal ditolak (${blocked.address})`);
|
|
9666
|
+
}
|
|
9667
|
+
const selected = addresses[0];
|
|
9668
|
+
return (deps.request ?? pinnedRequest)({ ...options2, address: selected.address, family: selected.family });
|
|
9669
|
+
}
|
|
9670
|
+
var defaultLookup2;
|
|
9671
|
+
var init_safe_outbound_request = __esm({
|
|
9672
|
+
"src/services/safe-outbound-request.ts"() {
|
|
9673
|
+
"use strict";
|
|
9674
|
+
init_ssrf();
|
|
9675
|
+
defaultLookup2 = async (host2) => (await dnsLookup2(host2, { all: true })).map((row) => ({ address: row.address, family: row.family }));
|
|
9676
|
+
}
|
|
9677
|
+
});
|
|
9678
|
+
|
|
9679
|
+
// src/services/uploads.ts
|
|
9680
|
+
import { randomUUID } from "node:crypto";
|
|
9681
|
+
import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
|
|
9682
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
9683
|
+
function extFor(mimeType) {
|
|
9684
|
+
return EXT[mimeType] ?? ".bin";
|
|
9685
|
+
}
|
|
9686
|
+
function uploadDir() {
|
|
9687
|
+
return resolve8(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join5(resolveHome(), "uploads"));
|
|
9688
|
+
}
|
|
9689
|
+
function sessionUploadDir(sessionId2) {
|
|
9690
|
+
if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
|
|
9691
|
+
return join5(uploadDir(), "terminal", sessionId2);
|
|
9692
|
+
}
|
|
9693
|
+
async function saveSessionUpload(sessionId2, buf, mimeType) {
|
|
9694
|
+
const dir2 = sessionUploadDir(sessionId2);
|
|
9695
|
+
await mkdir(dir2, { recursive: true, mode: 448 });
|
|
9696
|
+
const path2 = join5(dir2, `${randomUUID()}${extFor(mimeType)}`);
|
|
9697
|
+
await writeFile(path2, buf, { mode: 384 });
|
|
9698
|
+
return { path: path2, size: buf.length };
|
|
9699
|
+
}
|
|
9700
|
+
async function dropSessionUploads(sessionId2) {
|
|
9701
|
+
let dir2;
|
|
9702
|
+
try {
|
|
9703
|
+
dir2 = sessionUploadDir(sessionId2);
|
|
9704
|
+
} catch {
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
await rm(dir2, { recursive: true, force: true }).catch(() => {
|
|
9708
|
+
});
|
|
9709
|
+
}
|
|
9710
|
+
async function readUpload(storageKey) {
|
|
9711
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9712
|
+
return readFile2(join5(uploadDir(), safe));
|
|
9713
|
+
}
|
|
9714
|
+
async function readUploadOrFetch(storageKey) {
|
|
9715
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9716
|
+
const target2 = join5(uploadDir(), safe);
|
|
9717
|
+
try {
|
|
9718
|
+
return await readFile2(target2);
|
|
9719
|
+
} catch {
|
|
9720
|
+
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
9721
|
+
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
9722
|
+
if (!base2 || !token) throw new Error(`lampiran ${safe} tak ada lokal & bukan client sync`);
|
|
9723
|
+
const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe)}`, `${base2.replace(/\/$/, "")}/`);
|
|
9724
|
+
const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
|
|
9725
|
+
const res = await safeRequest({
|
|
9726
|
+
url: url2,
|
|
9727
|
+
method: "GET",
|
|
9728
|
+
headers: { authorization: `Bearer ${token}` },
|
|
9729
|
+
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
9730
|
+
connectMs: 5e3,
|
|
9731
|
+
totalMs: 15e3,
|
|
9732
|
+
maxResponseBytes: 5 * 1024 * 1024
|
|
9733
|
+
});
|
|
9734
|
+
if (res.status < 200 || res.status >= 300) throw new Error(`fetch lampiran hub gagal: ${res.status}`);
|
|
9735
|
+
const buf = res.body;
|
|
9736
|
+
await mkdir(uploadDir(), { recursive: true, mode: 448 });
|
|
9737
|
+
await writeFile(target2, buf, { mode: 384 });
|
|
9738
|
+
return buf;
|
|
9739
|
+
}
|
|
9740
|
+
}
|
|
9741
|
+
async function deleteUpload(storageKey) {
|
|
9742
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9743
|
+
await unlink(join5(uploadDir(), safe)).catch(() => {
|
|
9744
|
+
});
|
|
9745
|
+
}
|
|
9746
|
+
var EXT, SESSION_ID;
|
|
9747
|
+
var init_uploads = __esm({
|
|
9748
|
+
"src/services/uploads.ts"() {
|
|
9749
|
+
"use strict";
|
|
9750
|
+
init_config2();
|
|
9751
|
+
init_safe_outbound_request();
|
|
9752
|
+
init_src2();
|
|
9753
|
+
EXT = {
|
|
9754
|
+
"image/png": ".png",
|
|
9755
|
+
"image/jpeg": ".jpg",
|
|
9756
|
+
"image/webp": ".webp"
|
|
9757
|
+
};
|
|
9758
|
+
SESSION_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9759
|
+
}
|
|
9760
|
+
});
|
|
9761
|
+
|
|
9365
9762
|
// src/services/tui-dialog.ts
|
|
9366
9763
|
function readChoiceDialog(paneText2) {
|
|
9367
9764
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
@@ -9594,162 +9991,6 @@ var init_tui_dialog = __esm({
|
|
|
9594
9991
|
}
|
|
9595
9992
|
});
|
|
9596
9993
|
|
|
9597
|
-
// src/services/secret-box.ts
|
|
9598
|
-
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
9599
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9600
|
-
import { dirname as dirname6, join as join4 } from "node:path";
|
|
9601
|
-
function isEncrypted(value) {
|
|
9602
|
-
return value.startsWith(ENC_PREFIX);
|
|
9603
|
-
}
|
|
9604
|
-
function encryptWithKey(plain, key) {
|
|
9605
|
-
const iv = randomBytes(IV_BYTES);
|
|
9606
|
-
const cipher = createCipheriv(ALGO, key, iv);
|
|
9607
|
-
const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
9608
|
-
const tag2 = cipher.getAuthTag();
|
|
9609
|
-
return `${ENC_PREFIX}${iv.toString("base64url")}:${tag2.toString("base64url")}:${body.toString("base64url")}`;
|
|
9610
|
-
}
|
|
9611
|
-
function decryptWithKey(value, key) {
|
|
9612
|
-
if (!isEncrypted(value)) return value;
|
|
9613
|
-
const parts = value.slice(ENC_PREFIX.length).split(":");
|
|
9614
|
-
if (parts.length !== 3) return null;
|
|
9615
|
-
const [ivRaw, tagRaw, bodyRaw] = parts;
|
|
9616
|
-
try {
|
|
9617
|
-
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivRaw, "base64url"));
|
|
9618
|
-
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
9619
|
-
return Buffer.concat([decipher.update(Buffer.from(bodyRaw, "base64url")), decipher.final()]).toString("utf8");
|
|
9620
|
-
} catch {
|
|
9621
|
-
return null;
|
|
9622
|
-
}
|
|
9623
|
-
}
|
|
9624
|
-
function fromEnv(raw2) {
|
|
9625
|
-
for (const enc2 of ["base64url", "base64", "hex"]) {
|
|
9626
|
-
const buf = Buffer.from(raw2, enc2);
|
|
9627
|
-
if (buf.length === KEY_BYTES) return buf;
|
|
9628
|
-
}
|
|
9629
|
-
return null;
|
|
9630
|
-
}
|
|
9631
|
-
function secretKeyPath() {
|
|
9632
|
-
return join4(resolveHome(), "secret.key");
|
|
9633
|
-
}
|
|
9634
|
-
function secretKey() {
|
|
9635
|
-
if (cached) return cached;
|
|
9636
|
-
const override = process.env.HANOMAN_SECRET_KEY?.trim();
|
|
9637
|
-
if (override) {
|
|
9638
|
-
const key2 = fromEnv(override);
|
|
9639
|
-
if (!key2) throw new Error(`HANOMAN_SECRET_KEY harus 32 byte (hex/base64), dapat ${override.length} karakter`);
|
|
9640
|
-
cached = key2;
|
|
9641
|
-
return key2;
|
|
9642
|
-
}
|
|
9643
|
-
const path2 = secretKeyPath();
|
|
9644
|
-
if (existsSync5(path2)) {
|
|
9645
|
-
const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
|
|
9646
|
-
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9647
|
-
cached = key2;
|
|
9648
|
-
return key2;
|
|
9649
|
-
}
|
|
9650
|
-
const key = randomBytes(KEY_BYTES);
|
|
9651
|
-
mkdirSync3(dirname6(path2), { recursive: true });
|
|
9652
|
-
writeFileSync(path2, key.toString("base64url"), { mode: 384 });
|
|
9653
|
-
chmodSync2(path2, 384);
|
|
9654
|
-
cached = key;
|
|
9655
|
-
return key;
|
|
9656
|
-
}
|
|
9657
|
-
function encryptSecret(plain) {
|
|
9658
|
-
return encryptWithKey(plain, secretKey());
|
|
9659
|
-
}
|
|
9660
|
-
function decryptSecret(value) {
|
|
9661
|
-
return decryptWithKey(value, secretKey());
|
|
9662
|
-
}
|
|
9663
|
-
var ENC_PREFIX, ALGO, IV_BYTES, KEY_BYTES, cached;
|
|
9664
|
-
var init_secret_box = __esm({
|
|
9665
|
-
"src/services/secret-box.ts"() {
|
|
9666
|
-
"use strict";
|
|
9667
|
-
init_src2();
|
|
9668
|
-
ENC_PREFIX = "enc:v1:";
|
|
9669
|
-
ALGO = "aes-256-gcm";
|
|
9670
|
-
IV_BYTES = 12;
|
|
9671
|
-
KEY_BYTES = 32;
|
|
9672
|
-
cached = null;
|
|
9673
|
-
}
|
|
9674
|
-
});
|
|
9675
|
-
|
|
9676
|
-
// src/config.ts
|
|
9677
|
-
var config_exports = {};
|
|
9678
|
-
__export(config_exports, {
|
|
9679
|
-
clearConfig: () => clearConfig,
|
|
9680
|
-
effectiveBool: () => effectiveBool,
|
|
9681
|
-
effectiveInt: () => effectiveInt,
|
|
9682
|
-
effectiveStr: () => effectiveStr,
|
|
9683
|
-
loadConfig: () => loadConfig,
|
|
9684
|
-
rawDbValue: () => rawDbValue,
|
|
9685
|
-
setConfig: () => setConfig,
|
|
9686
|
-
setConfigsAtomic: () => setConfigsAtomic,
|
|
9687
|
-
sourceOf: () => sourceOf
|
|
9688
|
-
});
|
|
9689
|
-
async function loadConfig() {
|
|
9690
|
-
const rows = await prisma.runtimeConfig.findMany();
|
|
9691
|
-
const next = /* @__PURE__ */ new Map();
|
|
9692
|
-
for (const r of rows) {
|
|
9693
|
-
if (!isSecret(r.key)) {
|
|
9694
|
-
next.set(r.key, r.value);
|
|
9695
|
-
continue;
|
|
9696
|
-
}
|
|
9697
|
-
const plain = decryptSecret(r.value);
|
|
9698
|
-
if (plain === null) {
|
|
9699
|
-
console.error(`config: nilai '${r.key}' tak bisa didekripsi \u2014 diabaikan`);
|
|
9700
|
-
continue;
|
|
9701
|
-
}
|
|
9702
|
-
next.set(r.key, plain);
|
|
9703
|
-
}
|
|
9704
|
-
cache = next;
|
|
9705
|
-
}
|
|
9706
|
-
function rawDbValue(key) {
|
|
9707
|
-
return cache.get(key);
|
|
9708
|
-
}
|
|
9709
|
-
function effectiveStr(key) {
|
|
9710
|
-
return cache.get(key) ?? process.env[key] ?? configEntry(key)?.default;
|
|
9711
|
-
}
|
|
9712
|
-
function effectiveInt(key) {
|
|
9713
|
-
const v = effectiveStr(key);
|
|
9714
|
-
return v === void 0 ? void 0 : Number(v);
|
|
9715
|
-
}
|
|
9716
|
-
function effectiveBool(key) {
|
|
9717
|
-
const v = effectiveStr(key);
|
|
9718
|
-
return v === "1" || v === "true";
|
|
9719
|
-
}
|
|
9720
|
-
function sourceOf(key) {
|
|
9721
|
-
if (cache.has(key)) return "db";
|
|
9722
|
-
if (process.env[key] !== void 0) return "env";
|
|
9723
|
-
return "default";
|
|
9724
|
-
}
|
|
9725
|
-
async function setConfig(key, value) {
|
|
9726
|
-
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9727
|
-
await prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9728
|
-
cache.set(key, value);
|
|
9729
|
-
}
|
|
9730
|
-
async function setConfigsAtomic(values) {
|
|
9731
|
-
await prisma.$transaction(Object.entries(values).map(([key, value]) => {
|
|
9732
|
-
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9733
|
-
return prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9734
|
-
}));
|
|
9735
|
-
for (const [key, value] of Object.entries(values)) cache.set(key, value);
|
|
9736
|
-
}
|
|
9737
|
-
async function clearConfig(key) {
|
|
9738
|
-
await prisma.runtimeConfig.deleteMany({ where: { key } });
|
|
9739
|
-
cache.delete(key);
|
|
9740
|
-
}
|
|
9741
|
-
var cache, isSecret;
|
|
9742
|
-
var init_config2 = __esm({
|
|
9743
|
-
"src/config.ts"() {
|
|
9744
|
-
"use strict";
|
|
9745
|
-
init_db();
|
|
9746
|
-
init_src();
|
|
9747
|
-
init_secret_box();
|
|
9748
|
-
cache = /* @__PURE__ */ new Map();
|
|
9749
|
-
isSecret = (key) => configEntry(key)?.kind === "secret";
|
|
9750
|
-
}
|
|
9751
|
-
});
|
|
9752
|
-
|
|
9753
9994
|
// src/services/session-sandbox.ts
|
|
9754
9995
|
function assertRuntimeBoundary(env, runtime) {
|
|
9755
9996
|
if (env.NODE_ENV !== "production") return;
|
|
@@ -9836,7 +10077,7 @@ var init_session_sandbox = __esm({
|
|
|
9836
10077
|
import { spawn } from "node-pty";
|
|
9837
10078
|
import { execFileSync } from "node:child_process";
|
|
9838
10079
|
import { createRequire } from "node:module";
|
|
9839
|
-
import { randomUUID } from "node:crypto";
|
|
10080
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9840
10081
|
import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9841
10082
|
import { dirname as dirname7 } from "node:path";
|
|
9842
10083
|
import { tmpdir } from "node:os";
|
|
@@ -10173,12 +10414,24 @@ function broadcast(a, f) {
|
|
|
10173
10414
|
const msg = frame(f);
|
|
10174
10415
|
for (const c of a.clients) c.send(msg);
|
|
10175
10416
|
}
|
|
10417
|
+
function flushOutput(a) {
|
|
10418
|
+
if (a.flushTimer) {
|
|
10419
|
+
clearTimeout(a.flushTimer);
|
|
10420
|
+
a.flushTimer = void 0;
|
|
10421
|
+
}
|
|
10422
|
+
if (!a.pending) return;
|
|
10423
|
+
const d = a.pending;
|
|
10424
|
+
a.pending = "";
|
|
10425
|
+
a.scrollback = trimScrollback(a.scrollback + d);
|
|
10426
|
+
broadcast(a, { t: "data", d });
|
|
10427
|
+
}
|
|
10176
10428
|
function open(id) {
|
|
10177
10429
|
const pty = spawnPty("attach-session", "-d", "-t", name(id));
|
|
10178
|
-
const a = { pty, scrollback: "", clients: /* @__PURE__ */ new Set(), lastPhases: "" };
|
|
10430
|
+
const a = { pty, scrollback: "", clients: /* @__PURE__ */ new Set(), lastPhases: "", pending: "" };
|
|
10179
10431
|
pty.onData((d) => {
|
|
10180
|
-
a.
|
|
10181
|
-
|
|
10432
|
+
a.pending += d;
|
|
10433
|
+
if (a.pending.length >= COALESCE_MAX_BYTES) flushOutput(a);
|
|
10434
|
+
else if (!a.flushTimer) a.flushTimer = setTimeout(() => flushOutput(a), COALESCE_MS);
|
|
10182
10435
|
});
|
|
10183
10436
|
pty.onExit(() => {
|
|
10184
10437
|
if (attached.get(id) === a) drop(id);
|
|
@@ -10190,6 +10443,7 @@ function open(id) {
|
|
|
10190
10443
|
function drop(id) {
|
|
10191
10444
|
const a = attached.get(id);
|
|
10192
10445
|
if (!a) return;
|
|
10446
|
+
flushOutput(a);
|
|
10193
10447
|
attached.delete(id);
|
|
10194
10448
|
a.pty.kill();
|
|
10195
10449
|
for (const c of a.clients) c.close();
|
|
@@ -10198,6 +10452,7 @@ function drop(id) {
|
|
|
10198
10452
|
function end(id, code) {
|
|
10199
10453
|
const a = attached.get(id);
|
|
10200
10454
|
if (!a) return;
|
|
10455
|
+
flushOutput(a);
|
|
10201
10456
|
broadcast(a, { t: "exit", code });
|
|
10202
10457
|
drop(id);
|
|
10203
10458
|
}
|
|
@@ -10269,6 +10524,8 @@ function killSession(id) {
|
|
|
10269
10524
|
drop(id);
|
|
10270
10525
|
tmux("kill-session", "-t", name(id));
|
|
10271
10526
|
emitDeath({ sessionId: id, exitCode: p.exited ? p.code : null, transcript });
|
|
10527
|
+
void dropSessionUploads(id).catch(() => {
|
|
10528
|
+
});
|
|
10272
10529
|
return true;
|
|
10273
10530
|
}
|
|
10274
10531
|
function detachAll() {
|
|
@@ -10291,7 +10548,7 @@ function spawnPty(...args) {
|
|
|
10291
10548
|
);
|
|
10292
10549
|
}
|
|
10293
10550
|
}
|
|
10294
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
10551
|
+
var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
10295
10552
|
var init_pty = __esm({
|
|
10296
10553
|
"src/services/pty.ts"() {
|
|
10297
10554
|
"use strict";
|
|
@@ -10299,12 +10556,14 @@ var init_pty = __esm({
|
|
|
10299
10556
|
init_src();
|
|
10300
10557
|
init_session_phases();
|
|
10301
10558
|
init_session_id();
|
|
10559
|
+
init_uploads();
|
|
10302
10560
|
init_tui_dialog();
|
|
10303
10561
|
init_config2();
|
|
10304
10562
|
init_session_sandbox();
|
|
10305
10563
|
socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
|
|
10306
10564
|
PREFIX = "hanoman-";
|
|
10307
10565
|
MAX_SCROLLBACK = 256 * 1024;
|
|
10566
|
+
SCROLLBACK_SLACK = 64 * 1024;
|
|
10308
10567
|
POLL_MS = 500;
|
|
10309
10568
|
markerFilled = (f) => {
|
|
10310
10569
|
try {
|
|
@@ -10333,7 +10592,7 @@ var init_pty = __esm({
|
|
|
10333
10592
|
}
|
|
10334
10593
|
};
|
|
10335
10594
|
sq = (s2) => `'${s2.split("'").join("'\\''")}'`;
|
|
10336
|
-
idFor = (specId) => specId ? sessionIdForSpec(specId) :
|
|
10595
|
+
idFor = (specId) => specId ? sessionIdForSpec(specId) : randomUUID2().slice(0, 8);
|
|
10337
10596
|
FMT = [
|
|
10338
10597
|
"#{session_name}",
|
|
10339
10598
|
"#{@hanoman_project}",
|
|
@@ -10418,6 +10677,9 @@ var init_pty = __esm({
|
|
|
10418
10677
|
const text = paneText(id);
|
|
10419
10678
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
10420
10679
|
};
|
|
10680
|
+
COALESCE_MS = 16;
|
|
10681
|
+
COALESCE_MAX_BYTES = 64 * 1024;
|
|
10682
|
+
trimScrollback = (s2) => s2.length > MAX_SCROLLBACK + SCROLLBACK_SLACK ? s2.slice(-MAX_SCROLLBACK) : s2;
|
|
10421
10683
|
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
10422
10684
|
paneComplete = (p) => !!p.flow && !!p.phaseFile && sessionComplete(readPhases(p.phaseFile, p.flow), p.cwd, p.specId);
|
|
10423
10685
|
sessionFinished = (id) => {
|
|
@@ -12566,161 +12828,6 @@ var init_conflicts = __esm({
|
|
|
12566
12828
|
}
|
|
12567
12829
|
});
|
|
12568
12830
|
|
|
12569
|
-
// src/services/webhooks/ssrf.ts
|
|
12570
|
-
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12571
|
-
import { isIPv4 } from "node:net";
|
|
12572
|
-
function validateWebhookUrl(raw2) {
|
|
12573
|
-
let url2;
|
|
12574
|
-
try {
|
|
12575
|
-
url2 = new URL(raw2.trim());
|
|
12576
|
-
} catch {
|
|
12577
|
-
return { ok: false, error: "URL tak valid" };
|
|
12578
|
-
}
|
|
12579
|
-
if (url2.protocol !== "http:" && url2.protocol !== "https:")
|
|
12580
|
-
return { ok: false, error: "hanya http atau https" };
|
|
12581
|
-
if (url2.username || url2.password)
|
|
12582
|
-
return { ok: false, error: "kredensial di URL tak diizinkan" };
|
|
12583
|
-
if (!url2.hostname) return { ok: false, error: "hostname kosong" };
|
|
12584
|
-
return { ok: true, url: url2 };
|
|
12585
|
-
}
|
|
12586
|
-
function blockedV4(ip) {
|
|
12587
|
-
const p = ip.split(".").map(Number);
|
|
12588
|
-
if (p.length !== 4 || p.some((n2) => !Number.isInteger(n2) || n2 < 0 || n2 > 255)) return true;
|
|
12589
|
-
const [a, b] = p;
|
|
12590
|
-
if (a === 0 || a === 127) return true;
|
|
12591
|
-
if (a === 10) return true;
|
|
12592
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12593
|
-
if (a === 192 && b === 168) return true;
|
|
12594
|
-
if (a === 169 && b === 254) return true;
|
|
12595
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12596
|
-
if (a === 192 && b === 0) return true;
|
|
12597
|
-
if (a >= 224) return true;
|
|
12598
|
-
return false;
|
|
12599
|
-
}
|
|
12600
|
-
function isBlockedAddress(ip) {
|
|
12601
|
-
const v = ip.trim().toLowerCase();
|
|
12602
|
-
if (!v) return true;
|
|
12603
|
-
const mapped = v.startsWith("::ffff:") ? v.slice(7) : v;
|
|
12604
|
-
if (isIPv4(mapped)) return blockedV4(mapped);
|
|
12605
|
-
if (!v.includes(":")) return true;
|
|
12606
|
-
if (v === "::" || v === "::1") return true;
|
|
12607
|
-
if (v.startsWith("fe80") || v.startsWith("fec0")) return true;
|
|
12608
|
-
if (/^f[cd]/.test(v)) return true;
|
|
12609
|
-
if (v.startsWith("ff")) return true;
|
|
12610
|
-
return false;
|
|
12611
|
-
}
|
|
12612
|
-
function checkUrlShape(raw2, allowPrivate) {
|
|
12613
|
-
const parsed = validateWebhookUrl(raw2);
|
|
12614
|
-
if (!parsed.ok) return parsed;
|
|
12615
|
-
if (allowPrivate) return parsed;
|
|
12616
|
-
const host2 = parsed.url.hostname.replace(/^\[|\]$/g, "");
|
|
12617
|
-
const literal = isIPv4(host2) || host2.includes(":");
|
|
12618
|
-
if (literal && isBlockedAddress(host2))
|
|
12619
|
-
return {
|
|
12620
|
-
ok: false,
|
|
12621
|
-
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12622
|
-
};
|
|
12623
|
-
if (host2 === "localhost" || host2.endsWith(".localhost"))
|
|
12624
|
-
return {
|
|
12625
|
-
ok: false,
|
|
12626
|
-
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12627
|
-
};
|
|
12628
|
-
return parsed;
|
|
12629
|
-
}
|
|
12630
|
-
async function checkDestination(url2, allowPrivate, lookup = defaultLookup) {
|
|
12631
|
-
if (allowPrivate) return { ok: true };
|
|
12632
|
-
const host2 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
12633
|
-
let addrs;
|
|
12634
|
-
try {
|
|
12635
|
-
addrs = await lookup(host2);
|
|
12636
|
-
} catch (e) {
|
|
12637
|
-
return { ok: false, error: `DNS gagal: ${e.message}` };
|
|
12638
|
-
}
|
|
12639
|
-
if (!addrs.length) return { ok: false, error: "DNS tak mengembalikan alamat" };
|
|
12640
|
-
for (const a of addrs)
|
|
12641
|
-
if (isBlockedAddress(a.address))
|
|
12642
|
-
return {
|
|
12643
|
-
ok: false,
|
|
12644
|
-
error: `alamat internal ditolak (${a.address}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12645
|
-
};
|
|
12646
|
-
return { ok: true };
|
|
12647
|
-
}
|
|
12648
|
-
var defaultLookup;
|
|
12649
|
-
var init_ssrf = __esm({
|
|
12650
|
-
"src/services/webhooks/ssrf.ts"() {
|
|
12651
|
-
"use strict";
|
|
12652
|
-
defaultLookup = (host2) => dnsLookup(host2, { all: true });
|
|
12653
|
-
}
|
|
12654
|
-
});
|
|
12655
|
-
|
|
12656
|
-
// src/services/safe-outbound-request.ts
|
|
12657
|
-
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
12658
|
-
import { request as httpRequest } from "node:http";
|
|
12659
|
-
import { request as httpsRequest } from "node:https";
|
|
12660
|
-
async function pinnedRequest(input) {
|
|
12661
|
-
return new Promise((resolve19, reject2) => {
|
|
12662
|
-
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
12663
|
-
const req = transport({
|
|
12664
|
-
protocol: input.url.protocol,
|
|
12665
|
-
hostname: input.url.hostname,
|
|
12666
|
-
port: input.url.port || void 0,
|
|
12667
|
-
method: input.method,
|
|
12668
|
-
path: `${input.url.pathname}${input.url.search}`,
|
|
12669
|
-
headers: input.headers,
|
|
12670
|
-
servername: input.url.hostname,
|
|
12671
|
-
// Node >= 20 menyalakan `autoSelectFamily` secara default: socket memanggil `lookup` dengan
|
|
12672
|
-
// `all: true` dan membaca `addresses[0].address` dari hasilnya. Menjawab dalam bentuk skalar
|
|
12673
|
-
// di situ memberi `undefined` → ERR_INVALID_IP_ADDRESS sebelum satu paket pun keluar, dan
|
|
12674
|
-
// pemanggil (sync tick, webhook) menelannya sebagai "offline". Kedua bentuk dijawab supaya
|
|
12675
|
-
// pinning tetap benar apa pun setelan family-nya.
|
|
12676
|
-
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
12677
|
-
timeout: input.connectMs
|
|
12678
|
-
}, (response) => {
|
|
12679
|
-
const chunks = [];
|
|
12680
|
-
let bytes = 0;
|
|
12681
|
-
response.on("data", (chunk) => {
|
|
12682
|
-
bytes += chunk.length;
|
|
12683
|
-
if (bytes > input.maxResponseBytes) {
|
|
12684
|
-
response.destroy(new Error("outbound response terlalu besar"));
|
|
12685
|
-
return;
|
|
12686
|
-
}
|
|
12687
|
-
chunks.push(chunk);
|
|
12688
|
-
});
|
|
12689
|
-
response.on("end", () => resolve19({
|
|
12690
|
-
status: response.statusCode ?? 0,
|
|
12691
|
-
headers: response.headers,
|
|
12692
|
-
body: Buffer.concat(chunks)
|
|
12693
|
-
}));
|
|
12694
|
-
});
|
|
12695
|
-
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
12696
|
-
timer9.unref?.();
|
|
12697
|
-
req.once("close", () => clearTimeout(timer9));
|
|
12698
|
-
req.once("error", reject2);
|
|
12699
|
-
if (input.body) req.write(input.body);
|
|
12700
|
-
req.end();
|
|
12701
|
-
});
|
|
12702
|
-
}
|
|
12703
|
-
async function safeRequest(options2, deps = {}) {
|
|
12704
|
-
if (options2.url.protocol !== "http:" && options2.url.protocol !== "https:") throw new Error("outbound scheme ditolak");
|
|
12705
|
-
if (options2.url.username || options2.url.password) throw new Error("outbound credential URL ditolak");
|
|
12706
|
-
const addresses = await (deps.lookupAll ?? defaultLookup2)(options2.url.hostname);
|
|
12707
|
-
if (!addresses.length) throw new Error("DNS tak mengembalikan alamat");
|
|
12708
|
-
if (!options2.allowPrivate) {
|
|
12709
|
-
const blocked = addresses.find((row) => isBlockedAddress(row.address));
|
|
12710
|
-
if (blocked) throw new Error(`alamat internal ditolak (${blocked.address})`);
|
|
12711
|
-
}
|
|
12712
|
-
const selected = addresses[0];
|
|
12713
|
-
return (deps.request ?? pinnedRequest)({ ...options2, address: selected.address, family: selected.family });
|
|
12714
|
-
}
|
|
12715
|
-
var defaultLookup2;
|
|
12716
|
-
var init_safe_outbound_request = __esm({
|
|
12717
|
-
"src/services/safe-outbound-request.ts"() {
|
|
12718
|
-
"use strict";
|
|
12719
|
-
init_ssrf();
|
|
12720
|
-
defaultLookup2 = async (host2) => (await dnsLookup2(host2, { all: true })).map((row) => ({ address: row.address, family: row.family }));
|
|
12721
|
-
}
|
|
12722
|
-
});
|
|
12723
|
-
|
|
12724
12831
|
// ../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
|
|
12725
12832
|
var require_constants = __commonJS({
|
|
12726
12833
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -24806,13 +24913,13 @@ init_src();
|
|
|
24806
24913
|
import { execFile } from "node:child_process";
|
|
24807
24914
|
import { promisify } from "node:util";
|
|
24808
24915
|
import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
|
|
24809
|
-
import { resolve as
|
|
24916
|
+
import { resolve as resolve10 } from "node:path";
|
|
24810
24917
|
|
|
24811
24918
|
// src/services/safe-repo-path.ts
|
|
24812
24919
|
import { constants } from "node:fs";
|
|
24813
|
-
import { lstat, mkdir, open as open2, realpath, rename, unlink } from "node:fs/promises";
|
|
24814
|
-
import { isAbsolute as isAbsolute3, join as
|
|
24815
|
-
import { randomUUID as
|
|
24920
|
+
import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
|
|
24921
|
+
import { isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve9, sep as sep2 } from "node:path";
|
|
24922
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24816
24923
|
import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
24817
24924
|
var PathContainmentError = class extends Error {
|
|
24818
24925
|
code = "PATH_CONTAINMENT";
|
|
@@ -24841,9 +24948,9 @@ async function ensureRepoParents(root, rel) {
|
|
|
24841
24948
|
const parts = components(rel).slice(0, -1);
|
|
24842
24949
|
let current = base2;
|
|
24843
24950
|
for (const part of parts) {
|
|
24844
|
-
current =
|
|
24951
|
+
current = join6(current, part);
|
|
24845
24952
|
try {
|
|
24846
|
-
await
|
|
24953
|
+
await mkdir2(current, { mode: 448 });
|
|
24847
24954
|
} catch (error) {
|
|
24848
24955
|
if (error.code !== "EEXIST") throw error;
|
|
24849
24956
|
}
|
|
@@ -24858,7 +24965,7 @@ function ensureRepoParentsSync(root, rel) {
|
|
|
24858
24965
|
const parts = components(rel).slice(0, -1);
|
|
24859
24966
|
let current = base2;
|
|
24860
24967
|
for (const part of parts) {
|
|
24861
|
-
current =
|
|
24968
|
+
current = join6(current, part);
|
|
24862
24969
|
try {
|
|
24863
24970
|
mkdirSync5(current, { mode: 448 });
|
|
24864
24971
|
} catch (error) {
|
|
@@ -24874,7 +24981,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24874
24981
|
const parts = components(rel);
|
|
24875
24982
|
let current = base2;
|
|
24876
24983
|
for (let i = 0; i < parts.length; i++) {
|
|
24877
|
-
current =
|
|
24984
|
+
current = join6(current, parts[i]);
|
|
24878
24985
|
const stat2 = await lstat(current).catch((error) => {
|
|
24879
24986
|
if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
|
|
24880
24987
|
return denied("komponen tidak ada");
|
|
@@ -24883,8 +24990,8 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24883
24990
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
24884
24991
|
if (i < parts.length - 1 && !stat2.isDirectory()) denied("parent bukan direktori");
|
|
24885
24992
|
}
|
|
24886
|
-
if (!beneath(base2,
|
|
24887
|
-
const parent =
|
|
24993
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24994
|
+
const parent = resolve9(current, "..");
|
|
24888
24995
|
const parentReal = await realpath(parent).catch(() => denied("parent tidak ada"));
|
|
24889
24996
|
if (!beneath(base2, parentReal)) denied("parent keluar root");
|
|
24890
24997
|
return { root: base2, path: current, parent: parentReal };
|
|
@@ -24894,7 +25001,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24894
25001
|
const parts = components(rel);
|
|
24895
25002
|
let current = base2;
|
|
24896
25003
|
for (let i = 0; i < parts.length; i++) {
|
|
24897
|
-
current =
|
|
25004
|
+
current = join6(current, parts[i]);
|
|
24898
25005
|
try {
|
|
24899
25006
|
const stat2 = lstatSync(current);
|
|
24900
25007
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
@@ -24905,7 +25012,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24905
25012
|
throw error;
|
|
24906
25013
|
}
|
|
24907
25014
|
}
|
|
24908
|
-
if (!beneath(base2,
|
|
25015
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24909
25016
|
return current;
|
|
24910
25017
|
}
|
|
24911
25018
|
function readRepoFileSync(root, rel) {
|
|
@@ -24921,10 +25028,10 @@ function readRepoFileSync(root, rel) {
|
|
|
24921
25028
|
function writeRepoFileAtomicSync(root, rel, data) {
|
|
24922
25029
|
ensureRepoParentsSync(root, rel);
|
|
24923
25030
|
const path2 = assertSafeRepoPathSync(root, rel, true);
|
|
24924
|
-
const parent =
|
|
25031
|
+
const parent = resolve9(path2, "..");
|
|
24925
25032
|
const parentRel = relative(realpathSync2(root), parent);
|
|
24926
25033
|
if (parentRel) assertSafeRepoPathSync(root, parentRel);
|
|
24927
|
-
const temp =
|
|
25034
|
+
const temp = join6(parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24928
25035
|
const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24929
25036
|
try {
|
|
24930
25037
|
writeFileSync3(fd, data);
|
|
@@ -24968,10 +25075,10 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
24968
25075
|
return denied("final component tak terbaca");
|
|
24969
25076
|
});
|
|
24970
25077
|
if (current?.isSymbolicLink() || current && !current.isFile()) denied("target bukan file regular");
|
|
24971
|
-
await
|
|
25078
|
+
await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
|
|
24972
25079
|
if (error.code !== "EEXIST") throw error;
|
|
24973
25080
|
});
|
|
24974
|
-
const temp =
|
|
25081
|
+
const temp = join6(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24975
25082
|
const handle = await open2(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24976
25083
|
try {
|
|
24977
25084
|
await handle.writeFile(data);
|
|
@@ -24986,7 +25093,7 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
24986
25093
|
if (finalAgain?.isSymbolicLink() || finalAgain && !finalAgain.isFile()) denied("target berubah");
|
|
24987
25094
|
await rename(temp, entry.path);
|
|
24988
25095
|
} catch (error) {
|
|
24989
|
-
await
|
|
25096
|
+
await unlink2(temp).catch(() => {
|
|
24990
25097
|
});
|
|
24991
25098
|
throw error;
|
|
24992
25099
|
}
|
|
@@ -25016,7 +25123,7 @@ function docsDirOf(repoDir) {
|
|
|
25016
25123
|
}
|
|
25017
25124
|
function resolveIndex(repoDir, docsDir) {
|
|
25018
25125
|
const rel = `${docsDir}/README.md`;
|
|
25019
|
-
return existsSync6(
|
|
25126
|
+
return existsSync6(resolve10(repoDir, rel)) ? rel : "";
|
|
25020
25127
|
}
|
|
25021
25128
|
var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
|
|
25022
25129
|
var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
@@ -25393,7 +25500,7 @@ import { existsSync as existsSync7 } from "node:fs";
|
|
|
25393
25500
|
import { execFile as execFile3 } from "node:child_process";
|
|
25394
25501
|
import { promisify as promisify3 } from "node:util";
|
|
25395
25502
|
import { rmSync as rmSync3 } from "node:fs";
|
|
25396
|
-
import { join as
|
|
25503
|
+
import { join as join7 } from "node:path";
|
|
25397
25504
|
var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25398
25505
|
var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
|
|
25399
25506
|
var exec3 = promisify3(execFile3);
|
|
@@ -25437,7 +25544,7 @@ async function integrateBranch(repoDir, src, op, target2) {
|
|
|
25437
25544
|
const tgt = await resolveTarget(repoDir, target2);
|
|
25438
25545
|
if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
|
|
25439
25546
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25440
|
-
const wt =
|
|
25547
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
|
|
25441
25548
|
await reclaim(repoDir, wt);
|
|
25442
25549
|
const baseRef = op === "merge" ? tgt.ref : source;
|
|
25443
25550
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
|
|
@@ -25498,7 +25605,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
|
|
|
25498
25605
|
const src = await resolveGraphSource(repoDir, source);
|
|
25499
25606
|
if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
|
|
25500
25607
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25501
|
-
const wt =
|
|
25608
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25502
25609
|
await reclaim(repoDir, wt);
|
|
25503
25610
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25504
25611
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25536,7 +25643,7 @@ async function replayCurrent(repoDir, source, cmd) {
|
|
|
25536
25643
|
const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
25537
25644
|
if (!current || current === "HEAD")
|
|
25538
25645
|
return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
|
|
25539
|
-
const wt =
|
|
25646
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25540
25647
|
await reclaim(repoDir, wt);
|
|
25541
25648
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25542
25649
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25583,24 +25690,24 @@ init_db();
|
|
|
25583
25690
|
// src/services/spec-review.ts
|
|
25584
25691
|
import { execFile as execFile4 } from "node:child_process";
|
|
25585
25692
|
import { promisify as promisify4 } from "node:util";
|
|
25586
|
-
import { mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
25693
|
+
import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
|
|
25587
25694
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
25588
|
-
import { join as
|
|
25695
|
+
import { join as join8, resolve as resolve11 } from "node:path";
|
|
25589
25696
|
var exec4 = promisify4(execFile4);
|
|
25590
25697
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
25591
25698
|
var MAX = 256 * 1024;
|
|
25592
|
-
var worktreeDir = (repoDir, specId) =>
|
|
25699
|
+
var worktreeDir = (repoDir, specId) => join8(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
|
|
25593
25700
|
async function withTempIndex(wt, fn2) {
|
|
25594
25701
|
const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
|
|
25595
|
-
const dir2 = await mkdtemp(
|
|
25596
|
-
const tmp =
|
|
25597
|
-
await copyFile(
|
|
25702
|
+
const dir2 = await mkdtemp(join8(tmpdir2(), "hanoman-idx-"));
|
|
25703
|
+
const tmp = join8(dir2, "index");
|
|
25704
|
+
await copyFile(resolve11(wt, idx), tmp);
|
|
25598
25705
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
25599
25706
|
try {
|
|
25600
25707
|
await exec4("git", ["add", "-A", "-N"], { cwd: wt, env, ...GIT3 });
|
|
25601
25708
|
return await fn2(env);
|
|
25602
25709
|
} finally {
|
|
25603
|
-
await
|
|
25710
|
+
await rm2(dir2, { recursive: true, force: true });
|
|
25604
25711
|
}
|
|
25605
25712
|
}
|
|
25606
25713
|
var splitZ = (s2) => s2.split("\0").filter(Boolean);
|
|
@@ -25804,7 +25911,7 @@ init_notifications();
|
|
|
25804
25911
|
|
|
25805
25912
|
// src/services/session-result.ts
|
|
25806
25913
|
init_db();
|
|
25807
|
-
import { randomUUID as
|
|
25914
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25808
25915
|
var WHITELIST = [
|
|
25809
25916
|
"projectId",
|
|
25810
25917
|
"specId",
|
|
@@ -25818,7 +25925,7 @@ var WHITELIST = [
|
|
|
25818
25925
|
"author"
|
|
25819
25926
|
];
|
|
25820
25927
|
async function recordSessionResult(input) {
|
|
25821
|
-
const id =
|
|
25928
|
+
const id = randomUUID4();
|
|
25822
25929
|
const data = { id };
|
|
25823
25930
|
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25824
25931
|
if (data.status === void 0) data.status = "done";
|
|
@@ -29167,7 +29274,7 @@ import { promisify as promisify7 } from "node:util";
|
|
|
29167
29274
|
import { execFile as execFile6 } from "node:child_process";
|
|
29168
29275
|
import { promisify as promisify6 } from "node:util";
|
|
29169
29276
|
import { existsSync as existsSync9 } from "node:fs";
|
|
29170
|
-
import { resolve as
|
|
29277
|
+
import { resolve as resolve12 } from "node:path";
|
|
29171
29278
|
var exec6 = promisify6(execFile6);
|
|
29172
29279
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
29173
29280
|
var MAX2 = 256 * 1024;
|
|
@@ -29175,7 +29282,7 @@ var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
|
29175
29282
|
function repoAbsPath(repoDir, rel, allowMissingFinal = false) {
|
|
29176
29283
|
if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
29177
29284
|
assertSafeRepoPathSync(repoDir, rel, allowMissingFinal, true);
|
|
29178
|
-
return
|
|
29285
|
+
return resolve12(repoDir, rel);
|
|
29179
29286
|
}
|
|
29180
29287
|
async function listRepoTree(repoDir, ref = "") {
|
|
29181
29288
|
if (!repoDir || !existsSync9(repoDir)) return [];
|
|
@@ -30065,14 +30172,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
30065
30172
|
// src/routes/fs.ts
|
|
30066
30173
|
import { readdir } from "node:fs/promises";
|
|
30067
30174
|
import { homedir as homedir4 } from "node:os";
|
|
30068
|
-
import { resolve as
|
|
30175
|
+
import { resolve as resolve13, dirname as dirname9, join as join9 } from "node:path";
|
|
30069
30176
|
async function fs_default(app2) {
|
|
30070
30177
|
app2.get("/fs/browse", async (req, reply) => {
|
|
30071
30178
|
const q = req.query.path;
|
|
30072
|
-
const dir2 = q && q.trim() ?
|
|
30179
|
+
const dir2 = q && q.trim() ? resolve13(q.trim()) : homedir4();
|
|
30073
30180
|
try {
|
|
30074
30181
|
const ents = await readdir(dir2, { withFileTypes: true });
|
|
30075
|
-
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path:
|
|
30182
|
+
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join9(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
30076
30183
|
const parent = dirname9(dir2);
|
|
30077
30184
|
return { path: dir2, parent: parent === dir2 ? null : parent, entries };
|
|
30078
30185
|
} catch {
|
|
@@ -30506,11 +30613,11 @@ function installCommand(m, agent, shell = shellBin()) {
|
|
|
30506
30613
|
}
|
|
30507
30614
|
|
|
30508
30615
|
// src/services/session-worktree.ts
|
|
30509
|
-
import { resolve as
|
|
30616
|
+
import { resolve as resolve14, sep as sep3 } from "node:path";
|
|
30510
30617
|
function ownsWorktree(repoDir, cwd) {
|
|
30511
|
-
const base2 =
|
|
30512
|
-
const dir2 =
|
|
30513
|
-
const container =
|
|
30618
|
+
const base2 = resolve14(repoDir);
|
|
30619
|
+
const dir2 = resolve14(cwd);
|
|
30620
|
+
const container = resolve14(base2, ".worktrees");
|
|
30514
30621
|
return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
|
|
30515
30622
|
}
|
|
30516
30623
|
|
|
@@ -30518,15 +30625,15 @@ function ownsWorktree(repoDir, cwd) {
|
|
|
30518
30625
|
init_src2();
|
|
30519
30626
|
init_db();
|
|
30520
30627
|
import { execFile as execFile8 } from "node:child_process";
|
|
30521
|
-
import { readdir as readdir2, rm as
|
|
30628
|
+
import { readdir as readdir2, rm as rm3 } from "node:fs/promises";
|
|
30522
30629
|
import { promisify as promisify9 } from "node:util";
|
|
30523
|
-
import { join as
|
|
30630
|
+
import { join as join10, resolve as resolve15 } from "node:path";
|
|
30524
30631
|
init_notifications();
|
|
30525
30632
|
var TICK_MS = 6e4;
|
|
30526
|
-
var trashDirOf = (repoDir) =>
|
|
30633
|
+
var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
|
|
30527
30634
|
var exec8 = promisify9(execFile8);
|
|
30528
30635
|
var prodReaperDeps = {
|
|
30529
|
-
rm: (path2) =>
|
|
30636
|
+
rm: (path2) => rm3(path2, { recursive: true, force: true }),
|
|
30530
30637
|
prune: async (repoDir) => {
|
|
30531
30638
|
try {
|
|
30532
30639
|
await exec8("git", ["worktree", "prune"], { cwd: repoDir, timeout: 3e4 });
|
|
@@ -30580,7 +30687,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
|
|
|
30580
30687
|
}
|
|
30581
30688
|
let removed = 0;
|
|
30582
30689
|
for (const entry of entries) {
|
|
30583
|
-
const path2 =
|
|
30690
|
+
const path2 = join10(dir2, entry);
|
|
30584
30691
|
const known = pending.get(path2);
|
|
30585
30692
|
const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
|
|
30586
30693
|
pending.set(path2, row);
|
|
@@ -30624,6 +30731,8 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30624
30731
|
init_notifications();
|
|
30625
30732
|
init_stage_machine();
|
|
30626
30733
|
init_pty();
|
|
30734
|
+
init_uploads();
|
|
30735
|
+
var ATTACHMENT_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
30627
30736
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
30628
30737
|
async function advanceStage(specId, repoDir, sessionId2, flow, worktree) {
|
|
30629
30738
|
const next = stageForRun(readPhases(phaseFilePath(repoDir, sessionId2), flow), worktree, specId);
|
|
@@ -30947,6 +31056,18 @@ async function terminal_default(app2, opts) {
|
|
|
30947
31056
|
killSession(id);
|
|
30948
31057
|
return reply.code(202).send({ cleanup: null });
|
|
30949
31058
|
});
|
|
31059
|
+
app2.post("/terminal/sessions/:id/attachments", async (req, reply) => {
|
|
31060
|
+
const { id } = req.params;
|
|
31061
|
+
if (!getSession(id)) return reply.code(404).send({ error: "not found" });
|
|
31062
|
+
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
31063
|
+
const part = await req.file?.();
|
|
31064
|
+
if (!part) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
31065
|
+
const buf = await part.toBuffer();
|
|
31066
|
+
if (part.file?.truncated) return reply.code(413).send({ error: "berkas melebihi 5 MB" });
|
|
31067
|
+
if (!ATTACHMENT_MIME.has(part.mimetype)) return reply.code(415).send({ error: "tipe berkas tak didukung" });
|
|
31068
|
+
const { path: path2 } = await saveSessionUpload(id, buf, part.mimetype);
|
|
31069
|
+
return { path: path2 };
|
|
31070
|
+
});
|
|
30950
31071
|
app2.get("/terminal/sessions/:id/ws", {
|
|
30951
31072
|
websocket: true,
|
|
30952
31073
|
preValidation: async (req, reply) => {
|
|
@@ -31185,7 +31306,7 @@ init_config2();
|
|
|
31185
31306
|
import { spawn as spawn3 } from "node:child_process";
|
|
31186
31307
|
import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
31187
31308
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
31188
|
-
import { dirname as dirname10, join as
|
|
31309
|
+
import { dirname as dirname10, join as join11 } from "node:path";
|
|
31189
31310
|
var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
|
|
31190
31311
|
function consoleArgv(t) {
|
|
31191
31312
|
return [
|
|
@@ -31200,8 +31321,8 @@ function consoleArgv(t) {
|
|
|
31200
31321
|
];
|
|
31201
31322
|
}
|
|
31202
31323
|
function askpassScript() {
|
|
31203
|
-
const dir2 = mkdtempSync(
|
|
31204
|
-
const path2 =
|
|
31324
|
+
const dir2 = mkdtempSync(join11(tmpdir3(), "hanoman-askpass-"));
|
|
31325
|
+
const path2 = join11(dir2, "askpass.sh");
|
|
31205
31326
|
writeFileSync4(path2, `#!/bin/sh
|
|
31206
31327
|
printf '%s' "$HANOMAN_SSH_PASSWORD"
|
|
31207
31328
|
`, { mode: 448 });
|
|
@@ -31262,15 +31383,15 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
31262
31383
|
// src/services/vps-audit.ts
|
|
31263
31384
|
init_db();
|
|
31264
31385
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
31265
|
-
import { join as
|
|
31386
|
+
import { join as join13 } from "node:path";
|
|
31266
31387
|
|
|
31267
31388
|
// src/runner/deps.ts
|
|
31268
31389
|
import { existsSync as existsSync11 } from "node:fs";
|
|
31269
|
-
import { dirname as dirname11, join as
|
|
31390
|
+
import { dirname as dirname11, join as join12 } from "node:path";
|
|
31270
31391
|
function repoRootFrom(startDir) {
|
|
31271
31392
|
let dir2 = startDir;
|
|
31272
31393
|
for (let i = 0; i < 8; i++) {
|
|
31273
|
-
if (existsSync11(
|
|
31394
|
+
if (existsSync11(join12(dir2, "pnpm-workspace.yaml"))) return dir2;
|
|
31274
31395
|
const parent = dirname11(dir2);
|
|
31275
31396
|
if (parent === dir2) break;
|
|
31276
31397
|
dir2 = parent;
|
|
@@ -31376,7 +31497,7 @@ function parseHealth(out3) {
|
|
|
31376
31497
|
return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
|
|
31377
31498
|
}
|
|
31378
31499
|
var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
|
|
31379
|
-
var scriptPath = (f) =>
|
|
31500
|
+
var scriptPath = (f) => join13(repoRoot(), "server", "scripts", "vps", f);
|
|
31380
31501
|
async function itemStatesOf(vpsId) {
|
|
31381
31502
|
const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
|
|
31382
31503
|
return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
|
|
@@ -31525,11 +31646,11 @@ init_config2();
|
|
|
31525
31646
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
31526
31647
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
31527
31648
|
import { homedir as homedir5 } from "node:os";
|
|
31528
|
-
import { join as
|
|
31529
|
-
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ??
|
|
31649
|
+
import { join as join14 } from "node:path";
|
|
31650
|
+
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join14(homedir5(), ".hanoman");
|
|
31530
31651
|
function ensureHanomanKey() {
|
|
31531
31652
|
const dir2 = keyDir();
|
|
31532
|
-
const privPath =
|
|
31653
|
+
const privPath = join14(dir2, "id_ed25519");
|
|
31533
31654
|
const pubPath = `${privPath}.pub`;
|
|
31534
31655
|
if (!existsSync12(privPath)) {
|
|
31535
31656
|
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
@@ -31767,7 +31888,7 @@ init_config2();
|
|
|
31767
31888
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
31768
31889
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
31769
31890
|
import { homedir as homedir7 } from "node:os";
|
|
31770
|
-
import { join as
|
|
31891
|
+
import { join as join15 } from "node:path";
|
|
31771
31892
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
31772
31893
|
var TTL_MS2 = 3e4;
|
|
31773
31894
|
var lastOk = null;
|
|
@@ -31780,7 +31901,7 @@ var LABELS = {
|
|
|
31780
31901
|
var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
31781
31902
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
31782
31903
|
function credsFile() {
|
|
31783
|
-
return
|
|
31904
|
+
return join15(effectiveStr("CLAUDE_CONFIG_DIR") ?? join15(homedir7(), ".claude"), ".credentials.json");
|
|
31784
31905
|
}
|
|
31785
31906
|
function readAccessToken() {
|
|
31786
31907
|
if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
|
|
@@ -31854,14 +31975,14 @@ async function getLimits() {
|
|
|
31854
31975
|
// src/services/codex-limits.ts
|
|
31855
31976
|
import { open as open3, readdir as readdir3, stat } from "node:fs/promises";
|
|
31856
31977
|
import { homedir as homedir8 } from "node:os";
|
|
31857
|
-
import { join as
|
|
31978
|
+
import { join as join16 } from "node:path";
|
|
31858
31979
|
var TTL_MS3 = 3e4;
|
|
31859
31980
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
31860
31981
|
var TAIL_BYTES = 512 * 1024;
|
|
31861
31982
|
var MAX_FILES = 8;
|
|
31862
31983
|
var cache2 = null;
|
|
31863
31984
|
var freshUntil2 = 0;
|
|
31864
|
-
var codexSessionsDir = () =>
|
|
31985
|
+
var codexSessionsDir = () => join16(process.env.CODEX_HOME ?? join16(homedir8(), ".codex"), "sessions");
|
|
31865
31986
|
var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
|
|
31866
31987
|
function windowLabel(minutes) {
|
|
31867
31988
|
if (minutes === 300) return "Sesi 5 jam";
|
|
@@ -31894,7 +32015,7 @@ async function recentRollouts(dir2) {
|
|
|
31894
32015
|
return [];
|
|
31895
32016
|
}
|
|
31896
32017
|
const stamped = await Promise.all(entries.map(async (rel) => {
|
|
31897
|
-
const full =
|
|
32018
|
+
const full = join16(dir2, rel);
|
|
31898
32019
|
try {
|
|
31899
32020
|
return { full, mtime: (await stat(full)).mtimeMs };
|
|
31900
32021
|
} catch {
|
|
@@ -32022,7 +32143,7 @@ init_src();
|
|
|
32022
32143
|
init_src();
|
|
32023
32144
|
init_config2();
|
|
32024
32145
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
32025
|
-
import { resolve as
|
|
32146
|
+
import { resolve as resolve16, dirname as dirname12 } from "node:path";
|
|
32026
32147
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32027
32148
|
var UPDATE_COMMAND = "npm i -g hanoman@latest";
|
|
32028
32149
|
function composeUpdate(x) {
|
|
@@ -32048,7 +32169,7 @@ var lastLatest = null;
|
|
|
32048
32169
|
var lastStatus = "unavailable";
|
|
32049
32170
|
function runningVersion() {
|
|
32050
32171
|
const here = dirname12(fileURLToPath3(import.meta.url));
|
|
32051
|
-
for (const p of [
|
|
32172
|
+
for (const p of [resolve16(here, "build-info.json"), resolve16(here, "../package.json")]) {
|
|
32052
32173
|
try {
|
|
32053
32174
|
const v = JSON.parse(readFileSync12(p, "utf8")).version;
|
|
32054
32175
|
if (typeof v === "string" && v) return v;
|
|
@@ -32399,54 +32520,7 @@ setAcceptedHook((row) => broadcastSyncLog(row));
|
|
|
32399
32520
|
init_sync();
|
|
32400
32521
|
init_sync_client();
|
|
32401
32522
|
init_conflicts();
|
|
32402
|
-
|
|
32403
|
-
// src/services/uploads.ts
|
|
32404
|
-
init_config2();
|
|
32405
|
-
init_safe_outbound_request();
|
|
32406
|
-
init_src2();
|
|
32407
|
-
import { mkdir as mkdir2, writeFile, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
32408
|
-
import { join as join16, resolve as resolve16 } from "node:path";
|
|
32409
|
-
function uploadDir() {
|
|
32410
|
-
return resolve16(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join16(resolveHome(), "uploads"));
|
|
32411
|
-
}
|
|
32412
|
-
async function readUpload(storageKey) {
|
|
32413
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32414
|
-
return readFile2(join16(uploadDir(), safe));
|
|
32415
|
-
}
|
|
32416
|
-
async function readUploadOrFetch(storageKey) {
|
|
32417
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32418
|
-
const target2 = join16(uploadDir(), safe);
|
|
32419
|
-
try {
|
|
32420
|
-
return await readFile2(target2);
|
|
32421
|
-
} catch {
|
|
32422
|
-
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
32423
|
-
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
32424
|
-
if (!base2 || !token) throw new Error(`lampiran ${safe} tak ada lokal & bukan client sync`);
|
|
32425
|
-
const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe)}`, `${base2.replace(/\/$/, "")}/`);
|
|
32426
|
-
const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
|
|
32427
|
-
const res = await safeRequest({
|
|
32428
|
-
url: url2,
|
|
32429
|
-
method: "GET",
|
|
32430
|
-
headers: { authorization: `Bearer ${token}` },
|
|
32431
|
-
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
32432
|
-
connectMs: 5e3,
|
|
32433
|
-
totalMs: 15e3,
|
|
32434
|
-
maxResponseBytes: 5 * 1024 * 1024
|
|
32435
|
-
});
|
|
32436
|
-
if (res.status < 200 || res.status >= 300) throw new Error(`fetch lampiran hub gagal: ${res.status}`);
|
|
32437
|
-
const buf = res.body;
|
|
32438
|
-
await mkdir2(uploadDir(), { recursive: true, mode: 448 });
|
|
32439
|
-
await writeFile(target2, buf, { mode: 384 });
|
|
32440
|
-
return buf;
|
|
32441
|
-
}
|
|
32442
|
-
}
|
|
32443
|
-
async function deleteUpload(storageKey) {
|
|
32444
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32445
|
-
await unlink2(join16(uploadDir(), safe)).catch(() => {
|
|
32446
|
-
});
|
|
32447
|
-
}
|
|
32448
|
-
|
|
32449
|
-
// src/routes/sync.ts
|
|
32523
|
+
init_uploads();
|
|
32450
32524
|
init_config2();
|
|
32451
32525
|
var zPush = external_exports.object({
|
|
32452
32526
|
records: external_exports.array(external_exports.object({
|
|
@@ -32597,12 +32671,12 @@ async function session_results_default(app2) {
|
|
|
32597
32671
|
// src/services/session-history.ts
|
|
32598
32672
|
init_db();
|
|
32599
32673
|
init_pty();
|
|
32600
|
-
import { randomUUID as
|
|
32674
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
32601
32675
|
|
|
32602
32676
|
// src/services/transcript-store.ts
|
|
32603
32677
|
init_config2();
|
|
32604
32678
|
init_src2();
|
|
32605
|
-
import { randomUUID as
|
|
32679
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
32606
32680
|
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
|
|
32607
32681
|
import { join as join17, resolve as resolve17, basename as basename4 } from "node:path";
|
|
32608
32682
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
@@ -32624,7 +32698,7 @@ async function saveTranscript(text) {
|
|
|
32624
32698
|
const { body, truncated } = clamp(text);
|
|
32625
32699
|
const dir2 = transcriptDir();
|
|
32626
32700
|
await mkdir3(dir2, { recursive: true, mode: 448 });
|
|
32627
|
-
const key = `${
|
|
32701
|
+
const key = `${randomUUID5()}.log`;
|
|
32628
32702
|
await writeFile2(join17(dir2, key), body, { encoding: "utf8", mode: 384 });
|
|
32629
32703
|
return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
|
|
32630
32704
|
}
|
|
@@ -32669,7 +32743,7 @@ async function titleFor(specId) {
|
|
|
32669
32743
|
async function beginSession(b) {
|
|
32670
32744
|
await prisma.sessionHistory.create({
|
|
32671
32745
|
data: {
|
|
32672
|
-
id:
|
|
32746
|
+
id: randomUUID6(),
|
|
32673
32747
|
sessionId: b.sessionId,
|
|
32674
32748
|
projectId: b.projectId,
|
|
32675
32749
|
specId: b.specId ?? null,
|
|
@@ -32874,8 +32948,9 @@ init_db();
|
|
|
32874
32948
|
|
|
32875
32949
|
// src/services/ticket.ts
|
|
32876
32950
|
init_db();
|
|
32877
|
-
|
|
32951
|
+
init_uploads();
|
|
32878
32952
|
init_src();
|
|
32953
|
+
import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
|
|
32879
32954
|
function hashAccessKey(key) {
|
|
32880
32955
|
return createHash7("sha256").update(key).digest("hex");
|
|
32881
32956
|
}
|
|
@@ -32920,9 +32995,10 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32920
32995
|
// src/services/ticket-intake.ts
|
|
32921
32996
|
init_db();
|
|
32922
32997
|
init_notifications();
|
|
32998
|
+
init_uploads();
|
|
32923
32999
|
|
|
32924
33000
|
// src/services/upload-pipeline.ts
|
|
32925
|
-
import { randomUUID as
|
|
33001
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
32926
33002
|
import { spawn as spawn4 } from "node:child_process";
|
|
32927
33003
|
import { mkdir as mkdir4, rename as rename2, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises";
|
|
32928
33004
|
import { basename as basename5, isAbsolute as isAbsolute4, join as join18 } from "node:path";
|
|
@@ -40288,6 +40364,7 @@ var dist_default = constructor_default;
|
|
|
40288
40364
|
|
|
40289
40365
|
// src/services/upload-pipeline.ts
|
|
40290
40366
|
init_db();
|
|
40367
|
+
init_uploads();
|
|
40291
40368
|
var UPLOAD_LIMITS = {
|
|
40292
40369
|
fileBytes: 5 * 1024 * 1024,
|
|
40293
40370
|
ticketBytes: 10 * 1024 * 1024,
|
|
@@ -40391,8 +40468,8 @@ async function processUpload(input, deps = {}) {
|
|
|
40391
40468
|
const quarantineDir = join18(storageDir, ".quarantine");
|
|
40392
40469
|
await mkdir4(quarantineDir, { recursive: true, mode: 448 });
|
|
40393
40470
|
await mkdir4(storageDir, { recursive: true, mode: 448 });
|
|
40394
|
-
const quarantine = join18(quarantineDir, `${
|
|
40395
|
-
const storageKey = `${
|
|
40471
|
+
const quarantine = join18(quarantineDir, `${randomUUID7()}.upload`);
|
|
40472
|
+
const storageKey = `${randomUUID7()}${type.extension}`;
|
|
40396
40473
|
await writeFile3(quarantine, normalized, { mode: 384, flag: "wx" });
|
|
40397
40474
|
try {
|
|
40398
40475
|
await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
|
|
@@ -40580,6 +40657,7 @@ async function help_default(app2) {
|
|
|
40580
40657
|
|
|
40581
40658
|
// src/routes/tickets.ts
|
|
40582
40659
|
init_db();
|
|
40660
|
+
init_uploads();
|
|
40583
40661
|
|
|
40584
40662
|
// src/services/ticket-accept.ts
|
|
40585
40663
|
init_db();
|
|
@@ -41516,7 +41594,7 @@ init_config2();
|
|
|
41516
41594
|
init_pty();
|
|
41517
41595
|
init_session_sandbox();
|
|
41518
41596
|
import { execFile as execFile10 } from "node:child_process";
|
|
41519
|
-
import { randomUUID as
|
|
41597
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
41520
41598
|
import { mkdirSync as mkdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
41521
41599
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
41522
41600
|
import { join as join19 } from "node:path";
|
|
@@ -41549,9 +41627,9 @@ function leadProcess(prompt, o, env = process.env) {
|
|
|
41549
41627
|
} };
|
|
41550
41628
|
const promptDir = join19(tmpdir4(), "hanoman-prompts");
|
|
41551
41629
|
mkdirSync9(promptDir, { recursive: true, mode: 448 });
|
|
41552
|
-
const promptFile = join19(promptDir, `oneshot-${
|
|
41630
|
+
const promptFile = join19(promptDir, `oneshot-${randomUUID8()}`);
|
|
41553
41631
|
writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
|
|
41554
|
-
const workspace = o.cwd ?? join19(tmpdir4(), `hanoman-oneshot-${
|
|
41632
|
+
const workspace = o.cwd ?? join19(tmpdir4(), `hanoman-oneshot-${randomUUID8()}`);
|
|
41555
41633
|
if (!o.cwd) mkdirSync9(workspace, { recursive: false, mode: 448 });
|
|
41556
41634
|
try {
|
|
41557
41635
|
const argsWithoutPrompt = directArgs.slice(0, -1);
|
|
@@ -45299,7 +45377,12 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45299
45377
|
done(err, void 0);
|
|
45300
45378
|
}
|
|
45301
45379
|
});
|
|
45302
|
-
app2.register(websocket, {
|
|
45380
|
+
app2.register(websocket, {
|
|
45381
|
+
options: {
|
|
45382
|
+
maxPayload: MAX_WS_MESSAGE_BYTES,
|
|
45383
|
+
perMessageDeflate: { zlibDeflateOptions: { level: 6, memLevel: 7 }, concurrencyLimit: 10 }
|
|
45384
|
+
}
|
|
45385
|
+
});
|
|
45303
45386
|
app2.addHook("onClose", async () => {
|
|
45304
45387
|
await stopTelegramRuntime();
|
|
45305
45388
|
detachAll();
|
|
@@ -45745,7 +45828,7 @@ init_tap();
|
|
|
45745
45828
|
// src/services/webhooks/emit.ts
|
|
45746
45829
|
init_src();
|
|
45747
45830
|
init_db();
|
|
45748
|
-
import { randomUUID as
|
|
45831
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
45749
45832
|
function skipped(def2, row) {
|
|
45750
45833
|
if (!def2.skipWhen || !row) return false;
|
|
45751
45834
|
return row[def2.skipWhen.field] === def2.skipWhen.equals;
|
|
@@ -45811,7 +45894,7 @@ async function emitWebhook(i) {
|
|
|
45811
45894
|
const targets = matchingEndpoints(type, projectId);
|
|
45812
45895
|
if (!targets.length) return;
|
|
45813
45896
|
const name2 = projectId ? (await prisma.project.findUnique({ where: { id: projectId }, select: { name: true } }))?.name ?? null : null;
|
|
45814
|
-
const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${
|
|
45897
|
+
const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID9().replace(/-/g, "")}`);
|
|
45815
45898
|
if (!env) return;
|
|
45816
45899
|
await enqueueEnvelope(env, targets);
|
|
45817
45900
|
} catch (e) {
|
|
@@ -46161,6 +46244,7 @@ async function secureHanomanHome(opts) {
|
|
|
46161
46244
|
|
|
46162
46245
|
// src/services/retention.ts
|
|
46163
46246
|
init_db();
|
|
46247
|
+
init_uploads();
|
|
46164
46248
|
var RETENTION_DAYS = {
|
|
46165
46249
|
tickets: 90,
|
|
46166
46250
|
newTickets: 180,
|
|
@@ -46275,6 +46359,7 @@ function startRetentionSweep() {
|
|
|
46275
46359
|
}
|
|
46276
46360
|
|
|
46277
46361
|
// src/server.ts
|
|
46362
|
+
init_uploads();
|
|
46278
46363
|
var app = buildApp();
|
|
46279
46364
|
var port = Number(process.env.PORT ?? 8787);
|
|
46280
46365
|
var host = process.env.HOST ?? "127.0.0.1";
|