hanoman 0.1.42 → 0.1.44
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 +6 -0
- package/dist/server.js +754 -495
- package/package.json +1 -1
- package/web/assets/{index-TRbs6vrh.css → index-ChXVk9Y2.css} +1 -1
- package/web/assets/{index-B7aHqLDK.js → index-CzdyLXKP.js} +1544 -1541
- package/web/index.html +2 -2
package/dist/server.js
CHANGED
|
@@ -6092,6 +6092,9 @@ var init_api = __esm({
|
|
|
6092
6092
|
ideCompare: (id, from, to) => `${API}/projects/${id}/compare?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
|
6093
6093
|
ideCompareFile: (id, from, to, path2) => `${API}/projects/${id}/compare/file?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&path=${encodeURIComponent(path2)}`,
|
|
6094
6094
|
ideGit: (id) => `${API}/projects/${id}/git`,
|
|
6095
|
+
// ADR-0121 · operasi berkas Explorer: satu path untuk buat/rename/hapus, satu untuk unggah.
|
|
6096
|
+
ideEntry: (id, path2) => `${API}/projects/${id}/entry${path2 ? `?path=${encodeURIComponent(path2)}` : ""}`,
|
|
6097
|
+
ideUpload: (id) => `${API}/projects/${id}/upload`,
|
|
6095
6098
|
ideGitMerge: (id) => `${API}/projects/${id}/git/merge`,
|
|
6096
6099
|
// SPEC-229 · merge git graph isolasi
|
|
6097
6100
|
// SPEC-234 · status working tree (staged/unstaged) + diff satu file working tree
|
|
@@ -6119,6 +6122,9 @@ var init_api = __esm({
|
|
|
6119
6122
|
sessionReviewFile: (id, path2) => `${API}/terminal/sessions/${id}/review/${path2}`,
|
|
6120
6123
|
sessionIntegrate: (id) => `${API}/terminal/sessions/${id}/integrate`,
|
|
6121
6124
|
terminalWs: (id) => `${API}/terminal/sessions/${id}/ws`,
|
|
6125
|
+
// SPEC-816 · lampiran gambar sesi (multipart). Di bawah prefix /terminal supaya ikut capability
|
|
6126
|
+
// `sessions` yang sudah ada — POST menurunkan cabang tulisnya tanpa perubahan peta.
|
|
6127
|
+
terminalAttachments: (id) => `${API}/terminal/sessions/${id}/attachments`,
|
|
6122
6128
|
// SPEC-362 · ADR-0079 · riwayat sesi. Di bawah prefix /terminal supaya ikut capability
|
|
6123
6129
|
// `sessions` yang sudah ada (services/agent-capabilities.ts) tanpa menambah domain baru.
|
|
6124
6130
|
sessionHistory: (qs = "") => `${API}/terminal/history${qs}`,
|
|
@@ -6263,11 +6269,11 @@ function isExternalLink(target2) {
|
|
|
6263
6269
|
return !target2 || /^(https?:|#|mailto:)/.test(target2);
|
|
6264
6270
|
}
|
|
6265
6271
|
function resolveLink(fromRel, target2) {
|
|
6266
|
-
const
|
|
6267
|
-
if (!
|
|
6268
|
-
if (
|
|
6272
|
+
const clean2 = target2.trim().split(/\s+/)[0].split("#")[0].split("\\").join("/");
|
|
6273
|
+
if (!clean2) return "";
|
|
6274
|
+
if (clean2.startsWith("/")) return clean2.slice(1).split("/").filter((p) => p && p !== ".").join("/");
|
|
6269
6275
|
const dir2 = fromRel.includes("/") ? fromRel.slice(0, fromRel.lastIndexOf("/")) : "";
|
|
6270
|
-
const parts = (dir2 ? dir2.split("/") : []).concat(
|
|
6276
|
+
const parts = (dir2 ? dir2.split("/") : []).concat(clean2.replace(/^\.\//, "").split("/"));
|
|
6271
6277
|
const out3 = [];
|
|
6272
6278
|
for (const p of parts) {
|
|
6273
6279
|
if (p === "" || p === ".") continue;
|
|
@@ -9362,6 +9368,400 @@ var init_session_id = __esm({
|
|
|
9362
9368
|
}
|
|
9363
9369
|
});
|
|
9364
9370
|
|
|
9371
|
+
// src/services/secret-box.ts
|
|
9372
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
9373
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9374
|
+
import { dirname as dirname6, join as join4 } from "node:path";
|
|
9375
|
+
function isEncrypted(value) {
|
|
9376
|
+
return value.startsWith(ENC_PREFIX);
|
|
9377
|
+
}
|
|
9378
|
+
function encryptWithKey(plain, key) {
|
|
9379
|
+
const iv = randomBytes(IV_BYTES);
|
|
9380
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
9381
|
+
const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
9382
|
+
const tag2 = cipher.getAuthTag();
|
|
9383
|
+
return `${ENC_PREFIX}${iv.toString("base64url")}:${tag2.toString("base64url")}:${body.toString("base64url")}`;
|
|
9384
|
+
}
|
|
9385
|
+
function decryptWithKey(value, key) {
|
|
9386
|
+
if (!isEncrypted(value)) return value;
|
|
9387
|
+
const parts = value.slice(ENC_PREFIX.length).split(":");
|
|
9388
|
+
if (parts.length !== 3) return null;
|
|
9389
|
+
const [ivRaw, tagRaw, bodyRaw] = parts;
|
|
9390
|
+
try {
|
|
9391
|
+
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivRaw, "base64url"));
|
|
9392
|
+
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
9393
|
+
return Buffer.concat([decipher.update(Buffer.from(bodyRaw, "base64url")), decipher.final()]).toString("utf8");
|
|
9394
|
+
} catch {
|
|
9395
|
+
return null;
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
function fromEnv(raw2) {
|
|
9399
|
+
for (const enc2 of ["base64url", "base64", "hex"]) {
|
|
9400
|
+
const buf = Buffer.from(raw2, enc2);
|
|
9401
|
+
if (buf.length === KEY_BYTES) return buf;
|
|
9402
|
+
}
|
|
9403
|
+
return null;
|
|
9404
|
+
}
|
|
9405
|
+
function secretKeyPath() {
|
|
9406
|
+
return join4(resolveHome(), "secret.key");
|
|
9407
|
+
}
|
|
9408
|
+
function secretKey() {
|
|
9409
|
+
if (cached) return cached;
|
|
9410
|
+
const override = process.env.HANOMAN_SECRET_KEY?.trim();
|
|
9411
|
+
if (override) {
|
|
9412
|
+
const key2 = fromEnv(override);
|
|
9413
|
+
if (!key2) throw new Error(`HANOMAN_SECRET_KEY harus 32 byte (hex/base64), dapat ${override.length} karakter`);
|
|
9414
|
+
cached = key2;
|
|
9415
|
+
return key2;
|
|
9416
|
+
}
|
|
9417
|
+
const path2 = secretKeyPath();
|
|
9418
|
+
if (existsSync5(path2)) {
|
|
9419
|
+
const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
|
|
9420
|
+
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9421
|
+
cached = key2;
|
|
9422
|
+
return key2;
|
|
9423
|
+
}
|
|
9424
|
+
const key = randomBytes(KEY_BYTES);
|
|
9425
|
+
mkdirSync3(dirname6(path2), { recursive: true });
|
|
9426
|
+
writeFileSync(path2, key.toString("base64url"), { mode: 384 });
|
|
9427
|
+
chmodSync2(path2, 384);
|
|
9428
|
+
cached = key;
|
|
9429
|
+
return key;
|
|
9430
|
+
}
|
|
9431
|
+
function encryptSecret(plain) {
|
|
9432
|
+
return encryptWithKey(plain, secretKey());
|
|
9433
|
+
}
|
|
9434
|
+
function decryptSecret(value) {
|
|
9435
|
+
return decryptWithKey(value, secretKey());
|
|
9436
|
+
}
|
|
9437
|
+
var ENC_PREFIX, ALGO, IV_BYTES, KEY_BYTES, cached;
|
|
9438
|
+
var init_secret_box = __esm({
|
|
9439
|
+
"src/services/secret-box.ts"() {
|
|
9440
|
+
"use strict";
|
|
9441
|
+
init_src2();
|
|
9442
|
+
ENC_PREFIX = "enc:v1:";
|
|
9443
|
+
ALGO = "aes-256-gcm";
|
|
9444
|
+
IV_BYTES = 12;
|
|
9445
|
+
KEY_BYTES = 32;
|
|
9446
|
+
cached = null;
|
|
9447
|
+
}
|
|
9448
|
+
});
|
|
9449
|
+
|
|
9450
|
+
// src/config.ts
|
|
9451
|
+
var config_exports = {};
|
|
9452
|
+
__export(config_exports, {
|
|
9453
|
+
clearConfig: () => clearConfig,
|
|
9454
|
+
effectiveBool: () => effectiveBool,
|
|
9455
|
+
effectiveInt: () => effectiveInt,
|
|
9456
|
+
effectiveStr: () => effectiveStr,
|
|
9457
|
+
loadConfig: () => loadConfig,
|
|
9458
|
+
rawDbValue: () => rawDbValue,
|
|
9459
|
+
setConfig: () => setConfig,
|
|
9460
|
+
setConfigsAtomic: () => setConfigsAtomic,
|
|
9461
|
+
sourceOf: () => sourceOf
|
|
9462
|
+
});
|
|
9463
|
+
async function loadConfig() {
|
|
9464
|
+
const rows = await prisma.runtimeConfig.findMany();
|
|
9465
|
+
const next = /* @__PURE__ */ new Map();
|
|
9466
|
+
for (const r of rows) {
|
|
9467
|
+
if (!isSecret(r.key)) {
|
|
9468
|
+
next.set(r.key, r.value);
|
|
9469
|
+
continue;
|
|
9470
|
+
}
|
|
9471
|
+
const plain = decryptSecret(r.value);
|
|
9472
|
+
if (plain === null) {
|
|
9473
|
+
console.error(`config: nilai '${r.key}' tak bisa didekripsi \u2014 diabaikan`);
|
|
9474
|
+
continue;
|
|
9475
|
+
}
|
|
9476
|
+
next.set(r.key, plain);
|
|
9477
|
+
}
|
|
9478
|
+
cache = next;
|
|
9479
|
+
}
|
|
9480
|
+
function rawDbValue(key) {
|
|
9481
|
+
return cache.get(key);
|
|
9482
|
+
}
|
|
9483
|
+
function effectiveStr(key) {
|
|
9484
|
+
return cache.get(key) ?? process.env[key] ?? configEntry(key)?.default;
|
|
9485
|
+
}
|
|
9486
|
+
function effectiveInt(key) {
|
|
9487
|
+
const v = effectiveStr(key);
|
|
9488
|
+
return v === void 0 ? void 0 : Number(v);
|
|
9489
|
+
}
|
|
9490
|
+
function effectiveBool(key) {
|
|
9491
|
+
const v = effectiveStr(key);
|
|
9492
|
+
return v === "1" || v === "true";
|
|
9493
|
+
}
|
|
9494
|
+
function sourceOf(key) {
|
|
9495
|
+
if (cache.has(key)) return "db";
|
|
9496
|
+
if (process.env[key] !== void 0) return "env";
|
|
9497
|
+
return "default";
|
|
9498
|
+
}
|
|
9499
|
+
async function setConfig(key, value) {
|
|
9500
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9501
|
+
await prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9502
|
+
cache.set(key, value);
|
|
9503
|
+
}
|
|
9504
|
+
async function setConfigsAtomic(values) {
|
|
9505
|
+
await prisma.$transaction(Object.entries(values).map(([key, value]) => {
|
|
9506
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9507
|
+
return prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9508
|
+
}));
|
|
9509
|
+
for (const [key, value] of Object.entries(values)) cache.set(key, value);
|
|
9510
|
+
}
|
|
9511
|
+
async function clearConfig(key) {
|
|
9512
|
+
await prisma.runtimeConfig.deleteMany({ where: { key } });
|
|
9513
|
+
cache.delete(key);
|
|
9514
|
+
}
|
|
9515
|
+
var cache, isSecret;
|
|
9516
|
+
var init_config2 = __esm({
|
|
9517
|
+
"src/config.ts"() {
|
|
9518
|
+
"use strict";
|
|
9519
|
+
init_db();
|
|
9520
|
+
init_src();
|
|
9521
|
+
init_secret_box();
|
|
9522
|
+
cache = /* @__PURE__ */ new Map();
|
|
9523
|
+
isSecret = (key) => configEntry(key)?.kind === "secret";
|
|
9524
|
+
}
|
|
9525
|
+
});
|
|
9526
|
+
|
|
9527
|
+
// src/services/webhooks/ssrf.ts
|
|
9528
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9529
|
+
import { isIPv4 } from "node:net";
|
|
9530
|
+
function validateWebhookUrl(raw2) {
|
|
9531
|
+
let url2;
|
|
9532
|
+
try {
|
|
9533
|
+
url2 = new URL(raw2.trim());
|
|
9534
|
+
} catch {
|
|
9535
|
+
return { ok: false, error: "URL tak valid" };
|
|
9536
|
+
}
|
|
9537
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:")
|
|
9538
|
+
return { ok: false, error: "hanya http atau https" };
|
|
9539
|
+
if (url2.username || url2.password)
|
|
9540
|
+
return { ok: false, error: "kredensial di URL tak diizinkan" };
|
|
9541
|
+
if (!url2.hostname) return { ok: false, error: "hostname kosong" };
|
|
9542
|
+
return { ok: true, url: url2 };
|
|
9543
|
+
}
|
|
9544
|
+
function blockedV4(ip) {
|
|
9545
|
+
const p = ip.split(".").map(Number);
|
|
9546
|
+
if (p.length !== 4 || p.some((n2) => !Number.isInteger(n2) || n2 < 0 || n2 > 255)) return true;
|
|
9547
|
+
const [a, b] = p;
|
|
9548
|
+
if (a === 0 || a === 127) return true;
|
|
9549
|
+
if (a === 10) return true;
|
|
9550
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
9551
|
+
if (a === 192 && b === 168) return true;
|
|
9552
|
+
if (a === 169 && b === 254) return true;
|
|
9553
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
9554
|
+
if (a === 192 && b === 0) return true;
|
|
9555
|
+
if (a >= 224) return true;
|
|
9556
|
+
return false;
|
|
9557
|
+
}
|
|
9558
|
+
function isBlockedAddress(ip) {
|
|
9559
|
+
const v = ip.trim().toLowerCase();
|
|
9560
|
+
if (!v) return true;
|
|
9561
|
+
const mapped = v.startsWith("::ffff:") ? v.slice(7) : v;
|
|
9562
|
+
if (isIPv4(mapped)) return blockedV4(mapped);
|
|
9563
|
+
if (!v.includes(":")) return true;
|
|
9564
|
+
if (v === "::" || v === "::1") return true;
|
|
9565
|
+
if (v.startsWith("fe80") || v.startsWith("fec0")) return true;
|
|
9566
|
+
if (/^f[cd]/.test(v)) return true;
|
|
9567
|
+
if (v.startsWith("ff")) return true;
|
|
9568
|
+
return false;
|
|
9569
|
+
}
|
|
9570
|
+
function checkUrlShape(raw2, allowPrivate) {
|
|
9571
|
+
const parsed = validateWebhookUrl(raw2);
|
|
9572
|
+
if (!parsed.ok) return parsed;
|
|
9573
|
+
if (allowPrivate) return parsed;
|
|
9574
|
+
const host2 = parsed.url.hostname.replace(/^\[|\]$/g, "");
|
|
9575
|
+
const literal = isIPv4(host2) || host2.includes(":");
|
|
9576
|
+
if (literal && isBlockedAddress(host2))
|
|
9577
|
+
return {
|
|
9578
|
+
ok: false,
|
|
9579
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9580
|
+
};
|
|
9581
|
+
if (host2 === "localhost" || host2.endsWith(".localhost"))
|
|
9582
|
+
return {
|
|
9583
|
+
ok: false,
|
|
9584
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9585
|
+
};
|
|
9586
|
+
return parsed;
|
|
9587
|
+
}
|
|
9588
|
+
async function checkDestination(url2, allowPrivate, lookup = defaultLookup) {
|
|
9589
|
+
if (allowPrivate) return { ok: true };
|
|
9590
|
+
const host2 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
9591
|
+
let addrs;
|
|
9592
|
+
try {
|
|
9593
|
+
addrs = await lookup(host2);
|
|
9594
|
+
} catch (e) {
|
|
9595
|
+
return { ok: false, error: `DNS gagal: ${e.message}` };
|
|
9596
|
+
}
|
|
9597
|
+
if (!addrs.length) return { ok: false, error: "DNS tak mengembalikan alamat" };
|
|
9598
|
+
for (const a of addrs)
|
|
9599
|
+
if (isBlockedAddress(a.address))
|
|
9600
|
+
return {
|
|
9601
|
+
ok: false,
|
|
9602
|
+
error: `alamat internal ditolak (${a.address}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9603
|
+
};
|
|
9604
|
+
return { ok: true };
|
|
9605
|
+
}
|
|
9606
|
+
var defaultLookup;
|
|
9607
|
+
var init_ssrf = __esm({
|
|
9608
|
+
"src/services/webhooks/ssrf.ts"() {
|
|
9609
|
+
"use strict";
|
|
9610
|
+
defaultLookup = (host2) => dnsLookup(host2, { all: true });
|
|
9611
|
+
}
|
|
9612
|
+
});
|
|
9613
|
+
|
|
9614
|
+
// src/services/safe-outbound-request.ts
|
|
9615
|
+
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
9616
|
+
import { request as httpRequest } from "node:http";
|
|
9617
|
+
import { request as httpsRequest } from "node:https";
|
|
9618
|
+
async function pinnedRequest(input) {
|
|
9619
|
+
return new Promise((resolve19, reject2) => {
|
|
9620
|
+
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
9621
|
+
const req = transport({
|
|
9622
|
+
protocol: input.url.protocol,
|
|
9623
|
+
hostname: input.url.hostname,
|
|
9624
|
+
port: input.url.port || void 0,
|
|
9625
|
+
method: input.method,
|
|
9626
|
+
path: `${input.url.pathname}${input.url.search}`,
|
|
9627
|
+
headers: input.headers,
|
|
9628
|
+
servername: input.url.hostname,
|
|
9629
|
+
// Node >= 20 menyalakan `autoSelectFamily` secara default: socket memanggil `lookup` dengan
|
|
9630
|
+
// `all: true` dan membaca `addresses[0].address` dari hasilnya. Menjawab dalam bentuk skalar
|
|
9631
|
+
// di situ memberi `undefined` → ERR_INVALID_IP_ADDRESS sebelum satu paket pun keluar, dan
|
|
9632
|
+
// pemanggil (sync tick, webhook) menelannya sebagai "offline". Kedua bentuk dijawab supaya
|
|
9633
|
+
// pinning tetap benar apa pun setelan family-nya.
|
|
9634
|
+
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
9635
|
+
timeout: input.connectMs
|
|
9636
|
+
}, (response) => {
|
|
9637
|
+
const chunks = [];
|
|
9638
|
+
let bytes = 0;
|
|
9639
|
+
response.on("data", (chunk) => {
|
|
9640
|
+
bytes += chunk.length;
|
|
9641
|
+
if (bytes > input.maxResponseBytes) {
|
|
9642
|
+
response.destroy(new Error("outbound response terlalu besar"));
|
|
9643
|
+
return;
|
|
9644
|
+
}
|
|
9645
|
+
chunks.push(chunk);
|
|
9646
|
+
});
|
|
9647
|
+
response.on("end", () => resolve19({
|
|
9648
|
+
status: response.statusCode ?? 0,
|
|
9649
|
+
headers: response.headers,
|
|
9650
|
+
body: Buffer.concat(chunks)
|
|
9651
|
+
}));
|
|
9652
|
+
});
|
|
9653
|
+
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
9654
|
+
timer9.unref?.();
|
|
9655
|
+
req.once("close", () => clearTimeout(timer9));
|
|
9656
|
+
req.once("error", reject2);
|
|
9657
|
+
if (input.body) req.write(input.body);
|
|
9658
|
+
req.end();
|
|
9659
|
+
});
|
|
9660
|
+
}
|
|
9661
|
+
async function safeRequest(options2, deps = {}) {
|
|
9662
|
+
if (options2.url.protocol !== "http:" && options2.url.protocol !== "https:") throw new Error("outbound scheme ditolak");
|
|
9663
|
+
if (options2.url.username || options2.url.password) throw new Error("outbound credential URL ditolak");
|
|
9664
|
+
const addresses = await (deps.lookupAll ?? defaultLookup2)(options2.url.hostname);
|
|
9665
|
+
if (!addresses.length) throw new Error("DNS tak mengembalikan alamat");
|
|
9666
|
+
if (!options2.allowPrivate) {
|
|
9667
|
+
const blocked = addresses.find((row) => isBlockedAddress(row.address));
|
|
9668
|
+
if (blocked) throw new Error(`alamat internal ditolak (${blocked.address})`);
|
|
9669
|
+
}
|
|
9670
|
+
const selected = addresses[0];
|
|
9671
|
+
return (deps.request ?? pinnedRequest)({ ...options2, address: selected.address, family: selected.family });
|
|
9672
|
+
}
|
|
9673
|
+
var defaultLookup2;
|
|
9674
|
+
var init_safe_outbound_request = __esm({
|
|
9675
|
+
"src/services/safe-outbound-request.ts"() {
|
|
9676
|
+
"use strict";
|
|
9677
|
+
init_ssrf();
|
|
9678
|
+
defaultLookup2 = async (host2) => (await dnsLookup2(host2, { all: true })).map((row) => ({ address: row.address, family: row.family }));
|
|
9679
|
+
}
|
|
9680
|
+
});
|
|
9681
|
+
|
|
9682
|
+
// src/services/uploads.ts
|
|
9683
|
+
import { randomUUID } from "node:crypto";
|
|
9684
|
+
import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
|
|
9685
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
9686
|
+
function extFor(mimeType) {
|
|
9687
|
+
return EXT[mimeType] ?? ".bin";
|
|
9688
|
+
}
|
|
9689
|
+
function uploadDir() {
|
|
9690
|
+
return resolve8(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join5(resolveHome(), "uploads"));
|
|
9691
|
+
}
|
|
9692
|
+
function sessionUploadDir(sessionId2) {
|
|
9693
|
+
if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
|
|
9694
|
+
return join5(uploadDir(), "terminal", sessionId2);
|
|
9695
|
+
}
|
|
9696
|
+
async function saveSessionUpload(sessionId2, buf, mimeType) {
|
|
9697
|
+
const dir2 = sessionUploadDir(sessionId2);
|
|
9698
|
+
await mkdir(dir2, { recursive: true, mode: 448 });
|
|
9699
|
+
const path2 = join5(dir2, `${randomUUID()}${extFor(mimeType)}`);
|
|
9700
|
+
await writeFile(path2, buf, { mode: 384 });
|
|
9701
|
+
return { path: path2, size: buf.length };
|
|
9702
|
+
}
|
|
9703
|
+
async function dropSessionUploads(sessionId2) {
|
|
9704
|
+
let dir2;
|
|
9705
|
+
try {
|
|
9706
|
+
dir2 = sessionUploadDir(sessionId2);
|
|
9707
|
+
} catch {
|
|
9708
|
+
return;
|
|
9709
|
+
}
|
|
9710
|
+
await rm(dir2, { recursive: true, force: true }).catch(() => {
|
|
9711
|
+
});
|
|
9712
|
+
}
|
|
9713
|
+
async function readUpload(storageKey) {
|
|
9714
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9715
|
+
return readFile2(join5(uploadDir(), safe));
|
|
9716
|
+
}
|
|
9717
|
+
async function readUploadOrFetch(storageKey) {
|
|
9718
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9719
|
+
const target2 = join5(uploadDir(), safe);
|
|
9720
|
+
try {
|
|
9721
|
+
return await readFile2(target2);
|
|
9722
|
+
} catch {
|
|
9723
|
+
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
9724
|
+
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
9725
|
+
if (!base2 || !token) throw new Error(`lampiran ${safe} tak ada lokal & bukan client sync`);
|
|
9726
|
+
const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe)}`, `${base2.replace(/\/$/, "")}/`);
|
|
9727
|
+
const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
|
|
9728
|
+
const res = await safeRequest({
|
|
9729
|
+
url: url2,
|
|
9730
|
+
method: "GET",
|
|
9731
|
+
headers: { authorization: `Bearer ${token}` },
|
|
9732
|
+
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
9733
|
+
connectMs: 5e3,
|
|
9734
|
+
totalMs: 15e3,
|
|
9735
|
+
maxResponseBytes: 5 * 1024 * 1024
|
|
9736
|
+
});
|
|
9737
|
+
if (res.status < 200 || res.status >= 300) throw new Error(`fetch lampiran hub gagal: ${res.status}`);
|
|
9738
|
+
const buf = res.body;
|
|
9739
|
+
await mkdir(uploadDir(), { recursive: true, mode: 448 });
|
|
9740
|
+
await writeFile(target2, buf, { mode: 384 });
|
|
9741
|
+
return buf;
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9744
|
+
async function deleteUpload(storageKey) {
|
|
9745
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9746
|
+
await unlink(join5(uploadDir(), safe)).catch(() => {
|
|
9747
|
+
});
|
|
9748
|
+
}
|
|
9749
|
+
var EXT, SESSION_ID;
|
|
9750
|
+
var init_uploads = __esm({
|
|
9751
|
+
"src/services/uploads.ts"() {
|
|
9752
|
+
"use strict";
|
|
9753
|
+
init_config2();
|
|
9754
|
+
init_safe_outbound_request();
|
|
9755
|
+
init_src2();
|
|
9756
|
+
EXT = {
|
|
9757
|
+
"image/png": ".png",
|
|
9758
|
+
"image/jpeg": ".jpg",
|
|
9759
|
+
"image/webp": ".webp"
|
|
9760
|
+
};
|
|
9761
|
+
SESSION_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9762
|
+
}
|
|
9763
|
+
});
|
|
9764
|
+
|
|
9365
9765
|
// src/services/tui-dialog.ts
|
|
9366
9766
|
function readChoiceDialog(paneText2) {
|
|
9367
9767
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
@@ -9594,162 +9994,6 @@ var init_tui_dialog = __esm({
|
|
|
9594
9994
|
}
|
|
9595
9995
|
});
|
|
9596
9996
|
|
|
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
9997
|
// src/services/session-sandbox.ts
|
|
9754
9998
|
function assertRuntimeBoundary(env, runtime) {
|
|
9755
9999
|
if (env.NODE_ENV !== "production") return;
|
|
@@ -9836,7 +10080,7 @@ var init_session_sandbox = __esm({
|
|
|
9836
10080
|
import { spawn } from "node-pty";
|
|
9837
10081
|
import { execFileSync } from "node:child_process";
|
|
9838
10082
|
import { createRequire } from "node:module";
|
|
9839
|
-
import { randomUUID } from "node:crypto";
|
|
10083
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9840
10084
|
import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9841
10085
|
import { dirname as dirname7 } from "node:path";
|
|
9842
10086
|
import { tmpdir } from "node:os";
|
|
@@ -10283,6 +10527,8 @@ function killSession(id) {
|
|
|
10283
10527
|
drop(id);
|
|
10284
10528
|
tmux("kill-session", "-t", name(id));
|
|
10285
10529
|
emitDeath({ sessionId: id, exitCode: p.exited ? p.code : null, transcript });
|
|
10530
|
+
void dropSessionUploads(id).catch(() => {
|
|
10531
|
+
});
|
|
10286
10532
|
return true;
|
|
10287
10533
|
}
|
|
10288
10534
|
function detachAll() {
|
|
@@ -10313,6 +10559,7 @@ var init_pty = __esm({
|
|
|
10313
10559
|
init_src();
|
|
10314
10560
|
init_session_phases();
|
|
10315
10561
|
init_session_id();
|
|
10562
|
+
init_uploads();
|
|
10316
10563
|
init_tui_dialog();
|
|
10317
10564
|
init_config2();
|
|
10318
10565
|
init_session_sandbox();
|
|
@@ -10348,7 +10595,7 @@ var init_pty = __esm({
|
|
|
10348
10595
|
}
|
|
10349
10596
|
};
|
|
10350
10597
|
sq = (s2) => `'${s2.split("'").join("'\\''")}'`;
|
|
10351
|
-
idFor = (specId) => specId ? sessionIdForSpec(specId) :
|
|
10598
|
+
idFor = (specId) => specId ? sessionIdForSpec(specId) : randomUUID2().slice(0, 8);
|
|
10352
10599
|
FMT = [
|
|
10353
10600
|
"#{session_name}",
|
|
10354
10601
|
"#{@hanoman_project}",
|
|
@@ -11382,9 +11629,9 @@ var init_config3 = __esm({
|
|
|
11382
11629
|
// src/services/telegram/protocol.ts
|
|
11383
11630
|
import { createHash as createHash2 } from "node:crypto";
|
|
11384
11631
|
function normalizeTelegramCommand(text) {
|
|
11385
|
-
const
|
|
11386
|
-
const match =
|
|
11387
|
-
if (!match) return
|
|
11632
|
+
const clean2 = text.trim();
|
|
11633
|
+
const match = clean2.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
|
|
11634
|
+
if (!match) return clean2;
|
|
11388
11635
|
const args = match[2]?.trim().replace(/\s+/g, " ");
|
|
11389
11636
|
return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
|
|
11390
11637
|
}
|
|
@@ -11450,19 +11697,19 @@ function inboundDigest(input) {
|
|
|
11450
11697
|
return createHash2("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
|
|
11451
11698
|
}
|
|
11452
11699
|
function sanitizeTelegramOutput(text, exactSecrets = []) {
|
|
11453
|
-
let
|
|
11700
|
+
let clean2 = text.replace(ANSI, "").replace(CONTROLS, "");
|
|
11454
11701
|
for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
11455
|
-
|
|
11702
|
+
clean2 = clean2.split(secret).join("[REDACTED]");
|
|
11456
11703
|
}
|
|
11457
|
-
for (const pattern of CREDENTIAL_PATTERNS)
|
|
11458
|
-
return
|
|
11704
|
+
for (const pattern of CREDENTIAL_PATTERNS) clean2 = clean2.replace(pattern, "[REDACTED]");
|
|
11705
|
+
return clean2.trim();
|
|
11459
11706
|
}
|
|
11460
11707
|
function splitTelegramText(text, max = 4096, exactSecrets = []) {
|
|
11461
11708
|
if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
|
|
11462
|
-
const
|
|
11463
|
-
if (!
|
|
11709
|
+
const clean2 = sanitizeTelegramOutput(text, exactSecrets);
|
|
11710
|
+
if (!clean2) return [];
|
|
11464
11711
|
const chunks = [];
|
|
11465
|
-
let rest =
|
|
11712
|
+
let rest = clean2;
|
|
11466
11713
|
while (rest.length > max) {
|
|
11467
11714
|
let cut = max;
|
|
11468
11715
|
const before2 = rest.charCodeAt(cut - 1);
|
|
@@ -12584,161 +12831,6 @@ var init_conflicts = __esm({
|
|
|
12584
12831
|
}
|
|
12585
12832
|
});
|
|
12586
12833
|
|
|
12587
|
-
// src/services/webhooks/ssrf.ts
|
|
12588
|
-
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12589
|
-
import { isIPv4 } from "node:net";
|
|
12590
|
-
function validateWebhookUrl(raw2) {
|
|
12591
|
-
let url2;
|
|
12592
|
-
try {
|
|
12593
|
-
url2 = new URL(raw2.trim());
|
|
12594
|
-
} catch {
|
|
12595
|
-
return { ok: false, error: "URL tak valid" };
|
|
12596
|
-
}
|
|
12597
|
-
if (url2.protocol !== "http:" && url2.protocol !== "https:")
|
|
12598
|
-
return { ok: false, error: "hanya http atau https" };
|
|
12599
|
-
if (url2.username || url2.password)
|
|
12600
|
-
return { ok: false, error: "kredensial di URL tak diizinkan" };
|
|
12601
|
-
if (!url2.hostname) return { ok: false, error: "hostname kosong" };
|
|
12602
|
-
return { ok: true, url: url2 };
|
|
12603
|
-
}
|
|
12604
|
-
function blockedV4(ip) {
|
|
12605
|
-
const p = ip.split(".").map(Number);
|
|
12606
|
-
if (p.length !== 4 || p.some((n2) => !Number.isInteger(n2) || n2 < 0 || n2 > 255)) return true;
|
|
12607
|
-
const [a, b] = p;
|
|
12608
|
-
if (a === 0 || a === 127) return true;
|
|
12609
|
-
if (a === 10) return true;
|
|
12610
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12611
|
-
if (a === 192 && b === 168) return true;
|
|
12612
|
-
if (a === 169 && b === 254) return true;
|
|
12613
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12614
|
-
if (a === 192 && b === 0) return true;
|
|
12615
|
-
if (a >= 224) return true;
|
|
12616
|
-
return false;
|
|
12617
|
-
}
|
|
12618
|
-
function isBlockedAddress(ip) {
|
|
12619
|
-
const v = ip.trim().toLowerCase();
|
|
12620
|
-
if (!v) return true;
|
|
12621
|
-
const mapped = v.startsWith("::ffff:") ? v.slice(7) : v;
|
|
12622
|
-
if (isIPv4(mapped)) return blockedV4(mapped);
|
|
12623
|
-
if (!v.includes(":")) return true;
|
|
12624
|
-
if (v === "::" || v === "::1") return true;
|
|
12625
|
-
if (v.startsWith("fe80") || v.startsWith("fec0")) return true;
|
|
12626
|
-
if (/^f[cd]/.test(v)) return true;
|
|
12627
|
-
if (v.startsWith("ff")) return true;
|
|
12628
|
-
return false;
|
|
12629
|
-
}
|
|
12630
|
-
function checkUrlShape(raw2, allowPrivate) {
|
|
12631
|
-
const parsed = validateWebhookUrl(raw2);
|
|
12632
|
-
if (!parsed.ok) return parsed;
|
|
12633
|
-
if (allowPrivate) return parsed;
|
|
12634
|
-
const host2 = parsed.url.hostname.replace(/^\[|\]$/g, "");
|
|
12635
|
-
const literal = isIPv4(host2) || host2.includes(":");
|
|
12636
|
-
if (literal && isBlockedAddress(host2))
|
|
12637
|
-
return {
|
|
12638
|
-
ok: false,
|
|
12639
|
-
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12640
|
-
};
|
|
12641
|
-
if (host2 === "localhost" || host2.endsWith(".localhost"))
|
|
12642
|
-
return {
|
|
12643
|
-
ok: false,
|
|
12644
|
-
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12645
|
-
};
|
|
12646
|
-
return parsed;
|
|
12647
|
-
}
|
|
12648
|
-
async function checkDestination(url2, allowPrivate, lookup = defaultLookup) {
|
|
12649
|
-
if (allowPrivate) return { ok: true };
|
|
12650
|
-
const host2 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
12651
|
-
let addrs;
|
|
12652
|
-
try {
|
|
12653
|
-
addrs = await lookup(host2);
|
|
12654
|
-
} catch (e) {
|
|
12655
|
-
return { ok: false, error: `DNS gagal: ${e.message}` };
|
|
12656
|
-
}
|
|
12657
|
-
if (!addrs.length) return { ok: false, error: "DNS tak mengembalikan alamat" };
|
|
12658
|
-
for (const a of addrs)
|
|
12659
|
-
if (isBlockedAddress(a.address))
|
|
12660
|
-
return {
|
|
12661
|
-
ok: false,
|
|
12662
|
-
error: `alamat internal ditolak (${a.address}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
12663
|
-
};
|
|
12664
|
-
return { ok: true };
|
|
12665
|
-
}
|
|
12666
|
-
var defaultLookup;
|
|
12667
|
-
var init_ssrf = __esm({
|
|
12668
|
-
"src/services/webhooks/ssrf.ts"() {
|
|
12669
|
-
"use strict";
|
|
12670
|
-
defaultLookup = (host2) => dnsLookup(host2, { all: true });
|
|
12671
|
-
}
|
|
12672
|
-
});
|
|
12673
|
-
|
|
12674
|
-
// src/services/safe-outbound-request.ts
|
|
12675
|
-
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
12676
|
-
import { request as httpRequest } from "node:http";
|
|
12677
|
-
import { request as httpsRequest } from "node:https";
|
|
12678
|
-
async function pinnedRequest(input) {
|
|
12679
|
-
return new Promise((resolve19, reject2) => {
|
|
12680
|
-
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
12681
|
-
const req = transport({
|
|
12682
|
-
protocol: input.url.protocol,
|
|
12683
|
-
hostname: input.url.hostname,
|
|
12684
|
-
port: input.url.port || void 0,
|
|
12685
|
-
method: input.method,
|
|
12686
|
-
path: `${input.url.pathname}${input.url.search}`,
|
|
12687
|
-
headers: input.headers,
|
|
12688
|
-
servername: input.url.hostname,
|
|
12689
|
-
// Node >= 20 menyalakan `autoSelectFamily` secara default: socket memanggil `lookup` dengan
|
|
12690
|
-
// `all: true` dan membaca `addresses[0].address` dari hasilnya. Menjawab dalam bentuk skalar
|
|
12691
|
-
// di situ memberi `undefined` → ERR_INVALID_IP_ADDRESS sebelum satu paket pun keluar, dan
|
|
12692
|
-
// pemanggil (sync tick, webhook) menelannya sebagai "offline". Kedua bentuk dijawab supaya
|
|
12693
|
-
// pinning tetap benar apa pun setelan family-nya.
|
|
12694
|
-
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
12695
|
-
timeout: input.connectMs
|
|
12696
|
-
}, (response) => {
|
|
12697
|
-
const chunks = [];
|
|
12698
|
-
let bytes = 0;
|
|
12699
|
-
response.on("data", (chunk) => {
|
|
12700
|
-
bytes += chunk.length;
|
|
12701
|
-
if (bytes > input.maxResponseBytes) {
|
|
12702
|
-
response.destroy(new Error("outbound response terlalu besar"));
|
|
12703
|
-
return;
|
|
12704
|
-
}
|
|
12705
|
-
chunks.push(chunk);
|
|
12706
|
-
});
|
|
12707
|
-
response.on("end", () => resolve19({
|
|
12708
|
-
status: response.statusCode ?? 0,
|
|
12709
|
-
headers: response.headers,
|
|
12710
|
-
body: Buffer.concat(chunks)
|
|
12711
|
-
}));
|
|
12712
|
-
});
|
|
12713
|
-
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
12714
|
-
timer9.unref?.();
|
|
12715
|
-
req.once("close", () => clearTimeout(timer9));
|
|
12716
|
-
req.once("error", reject2);
|
|
12717
|
-
if (input.body) req.write(input.body);
|
|
12718
|
-
req.end();
|
|
12719
|
-
});
|
|
12720
|
-
}
|
|
12721
|
-
async function safeRequest(options2, deps = {}) {
|
|
12722
|
-
if (options2.url.protocol !== "http:" && options2.url.protocol !== "https:") throw new Error("outbound scheme ditolak");
|
|
12723
|
-
if (options2.url.username || options2.url.password) throw new Error("outbound credential URL ditolak");
|
|
12724
|
-
const addresses = await (deps.lookupAll ?? defaultLookup2)(options2.url.hostname);
|
|
12725
|
-
if (!addresses.length) throw new Error("DNS tak mengembalikan alamat");
|
|
12726
|
-
if (!options2.allowPrivate) {
|
|
12727
|
-
const blocked = addresses.find((row) => isBlockedAddress(row.address));
|
|
12728
|
-
if (blocked) throw new Error(`alamat internal ditolak (${blocked.address})`);
|
|
12729
|
-
}
|
|
12730
|
-
const selected = addresses[0];
|
|
12731
|
-
return (deps.request ?? pinnedRequest)({ ...options2, address: selected.address, family: selected.family });
|
|
12732
|
-
}
|
|
12733
|
-
var defaultLookup2;
|
|
12734
|
-
var init_safe_outbound_request = __esm({
|
|
12735
|
-
"src/services/safe-outbound-request.ts"() {
|
|
12736
|
-
"use strict";
|
|
12737
|
-
init_ssrf();
|
|
12738
|
-
defaultLookup2 = async (host2) => (await dnsLookup2(host2, { all: true })).map((row) => ({ address: row.address, family: row.family }));
|
|
12739
|
-
}
|
|
12740
|
-
});
|
|
12741
|
-
|
|
12742
12834
|
// ../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
|
|
12743
12835
|
var require_constants = __commonJS({
|
|
12744
12836
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -18469,11 +18561,11 @@ var require_clean = __commonJS({
|
|
|
18469
18561
|
"../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/clean.js"(exports, module) {
|
|
18470
18562
|
"use strict";
|
|
18471
18563
|
var parse3 = require_parse();
|
|
18472
|
-
var
|
|
18564
|
+
var clean2 = (version2, options2) => {
|
|
18473
18565
|
const s2 = parse3(version2.trim().replace(/^[=v]+/, ""), options2);
|
|
18474
18566
|
return s2 ? s2.version : null;
|
|
18475
18567
|
};
|
|
18476
|
-
module.exports =
|
|
18568
|
+
module.exports = clean2;
|
|
18477
18569
|
}
|
|
18478
18570
|
});
|
|
18479
18571
|
|
|
@@ -19870,7 +19962,7 @@ var require_semver2 = __commonJS({
|
|
|
19870
19962
|
var identifiers = require_identifiers();
|
|
19871
19963
|
var parse3 = require_parse();
|
|
19872
19964
|
var valid = require_valid();
|
|
19873
|
-
var
|
|
19965
|
+
var clean2 = require_clean();
|
|
19874
19966
|
var inc = require_inc();
|
|
19875
19967
|
var diff = require_diff();
|
|
19876
19968
|
var major = require_major();
|
|
@@ -19909,7 +20001,7 @@ var require_semver2 = __commonJS({
|
|
|
19909
20001
|
module.exports = {
|
|
19910
20002
|
parse: parse3,
|
|
19911
20003
|
valid,
|
|
19912
|
-
clean,
|
|
20004
|
+
clean: clean2,
|
|
19913
20005
|
inc,
|
|
19914
20006
|
diff,
|
|
19915
20007
|
major,
|
|
@@ -24267,7 +24359,7 @@ var require_multipart2 = __commonJS({
|
|
|
24267
24359
|
var Busboy = require_main();
|
|
24268
24360
|
var os = __require("node:os");
|
|
24269
24361
|
var fp = require_fastify_plugin();
|
|
24270
|
-
var { createWriteStream } = __require("node:fs");
|
|
24362
|
+
var { createWriteStream: createWriteStream2 } = __require("node:fs");
|
|
24271
24363
|
var { unlink: unlink6 } = __require("node:fs/promises");
|
|
24272
24364
|
var path2 = __require("node:path");
|
|
24273
24365
|
var { generateId } = require_generateId();
|
|
@@ -24647,7 +24739,7 @@ var require_multipart2 = __commonJS({
|
|
|
24647
24739
|
continue;
|
|
24648
24740
|
}
|
|
24649
24741
|
const filepath = path2.join(tmpdir5, generateId() + path2.extname(part.filename || "file" + i++));
|
|
24650
|
-
const target2 =
|
|
24742
|
+
const target2 = createWriteStream2(filepath);
|
|
24651
24743
|
try {
|
|
24652
24744
|
this.tmpUploads.push(filepath);
|
|
24653
24745
|
await pump2(part.file, target2);
|
|
@@ -24824,13 +24916,13 @@ init_src();
|
|
|
24824
24916
|
import { execFile } from "node:child_process";
|
|
24825
24917
|
import { promisify } from "node:util";
|
|
24826
24918
|
import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
|
|
24827
|
-
import { resolve as
|
|
24919
|
+
import { resolve as resolve10 } from "node:path";
|
|
24828
24920
|
|
|
24829
24921
|
// src/services/safe-repo-path.ts
|
|
24830
24922
|
import { constants } from "node:fs";
|
|
24831
|
-
import { lstat, mkdir, open as open2, realpath, rename, unlink } from "node:fs/promises";
|
|
24832
|
-
import { isAbsolute as isAbsolute3, join as
|
|
24833
|
-
import { randomUUID as
|
|
24923
|
+
import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
|
|
24924
|
+
import { isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve9, sep as sep2 } from "node:path";
|
|
24925
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24834
24926
|
import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
24835
24927
|
var PathContainmentError = class extends Error {
|
|
24836
24928
|
code = "PATH_CONTAINMENT";
|
|
@@ -24859,9 +24951,9 @@ async function ensureRepoParents(root, rel) {
|
|
|
24859
24951
|
const parts = components(rel).slice(0, -1);
|
|
24860
24952
|
let current = base2;
|
|
24861
24953
|
for (const part of parts) {
|
|
24862
|
-
current =
|
|
24954
|
+
current = join6(current, part);
|
|
24863
24955
|
try {
|
|
24864
|
-
await
|
|
24956
|
+
await mkdir2(current, { mode: 448 });
|
|
24865
24957
|
} catch (error) {
|
|
24866
24958
|
if (error.code !== "EEXIST") throw error;
|
|
24867
24959
|
}
|
|
@@ -24876,7 +24968,7 @@ function ensureRepoParentsSync(root, rel) {
|
|
|
24876
24968
|
const parts = components(rel).slice(0, -1);
|
|
24877
24969
|
let current = base2;
|
|
24878
24970
|
for (const part of parts) {
|
|
24879
|
-
current =
|
|
24971
|
+
current = join6(current, part);
|
|
24880
24972
|
try {
|
|
24881
24973
|
mkdirSync5(current, { mode: 448 });
|
|
24882
24974
|
} catch (error) {
|
|
@@ -24892,8 +24984,9 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24892
24984
|
const parts = components(rel);
|
|
24893
24985
|
let current = base2;
|
|
24894
24986
|
for (let i = 0; i < parts.length; i++) {
|
|
24895
|
-
current =
|
|
24987
|
+
current = join6(current, parts[i]);
|
|
24896
24988
|
const stat2 = await lstat(current).catch((error) => {
|
|
24989
|
+
if (error.code === "ENOENT" && opts.allowMissingTail) return null;
|
|
24897
24990
|
if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
|
|
24898
24991
|
return denied("komponen tidak ada");
|
|
24899
24992
|
});
|
|
@@ -24901,8 +24994,8 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24901
24994
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
24902
24995
|
if (i < parts.length - 1 && !stat2.isDirectory()) denied("parent bukan direktori");
|
|
24903
24996
|
}
|
|
24904
|
-
if (!beneath(base2,
|
|
24905
|
-
const parent =
|
|
24997
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24998
|
+
const parent = resolve9(current, "..");
|
|
24906
24999
|
const parentReal = await realpath(parent).catch(() => denied("parent tidak ada"));
|
|
24907
25000
|
if (!beneath(base2, parentReal)) denied("parent keluar root");
|
|
24908
25001
|
return { root: base2, path: current, parent: parentReal };
|
|
@@ -24912,7 +25005,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24912
25005
|
const parts = components(rel);
|
|
24913
25006
|
let current = base2;
|
|
24914
25007
|
for (let i = 0; i < parts.length; i++) {
|
|
24915
|
-
current =
|
|
25008
|
+
current = join6(current, parts[i]);
|
|
24916
25009
|
try {
|
|
24917
25010
|
const stat2 = lstatSync(current);
|
|
24918
25011
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
@@ -24923,7 +25016,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24923
25016
|
throw error;
|
|
24924
25017
|
}
|
|
24925
25018
|
}
|
|
24926
|
-
if (!beneath(base2,
|
|
25019
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24927
25020
|
return current;
|
|
24928
25021
|
}
|
|
24929
25022
|
function readRepoFileSync(root, rel) {
|
|
@@ -24939,10 +25032,10 @@ function readRepoFileSync(root, rel) {
|
|
|
24939
25032
|
function writeRepoFileAtomicSync(root, rel, data) {
|
|
24940
25033
|
ensureRepoParentsSync(root, rel);
|
|
24941
25034
|
const path2 = assertSafeRepoPathSync(root, rel, true);
|
|
24942
|
-
const parent =
|
|
25035
|
+
const parent = resolve9(path2, "..");
|
|
24943
25036
|
const parentRel = relative(realpathSync2(root), parent);
|
|
24944
25037
|
if (parentRel) assertSafeRepoPathSync(root, parentRel);
|
|
24945
|
-
const temp =
|
|
25038
|
+
const temp = join6(parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24946
25039
|
const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24947
25040
|
try {
|
|
24948
25041
|
writeFileSync3(fd, data);
|
|
@@ -24986,10 +25079,10 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
24986
25079
|
return denied("final component tak terbaca");
|
|
24987
25080
|
});
|
|
24988
25081
|
if (current?.isSymbolicLink() || current && !current.isFile()) denied("target bukan file regular");
|
|
24989
|
-
await
|
|
25082
|
+
await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
|
|
24990
25083
|
if (error.code !== "EEXIST") throw error;
|
|
24991
25084
|
});
|
|
24992
|
-
const temp =
|
|
25085
|
+
const temp = join6(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24993
25086
|
const handle = await open2(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24994
25087
|
try {
|
|
24995
25088
|
await handle.writeFile(data);
|
|
@@ -25004,7 +25097,7 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
25004
25097
|
if (finalAgain?.isSymbolicLink() || finalAgain && !finalAgain.isFile()) denied("target berubah");
|
|
25005
25098
|
await rename(temp, entry.path);
|
|
25006
25099
|
} catch (error) {
|
|
25007
|
-
await
|
|
25100
|
+
await unlink2(temp).catch(() => {
|
|
25008
25101
|
});
|
|
25009
25102
|
throw error;
|
|
25010
25103
|
}
|
|
@@ -25034,7 +25127,7 @@ function docsDirOf(repoDir) {
|
|
|
25034
25127
|
}
|
|
25035
25128
|
function resolveIndex(repoDir, docsDir) {
|
|
25036
25129
|
const rel = `${docsDir}/README.md`;
|
|
25037
|
-
return existsSync6(
|
|
25130
|
+
return existsSync6(resolve10(repoDir, rel)) ? rel : "";
|
|
25038
25131
|
}
|
|
25039
25132
|
var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
|
|
25040
25133
|
var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
@@ -25411,7 +25504,7 @@ import { existsSync as existsSync7 } from "node:fs";
|
|
|
25411
25504
|
import { execFile as execFile3 } from "node:child_process";
|
|
25412
25505
|
import { promisify as promisify3 } from "node:util";
|
|
25413
25506
|
import { rmSync as rmSync3 } from "node:fs";
|
|
25414
|
-
import { join as
|
|
25507
|
+
import { join as join7 } from "node:path";
|
|
25415
25508
|
var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25416
25509
|
var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
|
|
25417
25510
|
var exec3 = promisify3(execFile3);
|
|
@@ -25455,7 +25548,7 @@ async function integrateBranch(repoDir, src, op, target2) {
|
|
|
25455
25548
|
const tgt = await resolveTarget(repoDir, target2);
|
|
25456
25549
|
if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
|
|
25457
25550
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25458
|
-
const wt =
|
|
25551
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
|
|
25459
25552
|
await reclaim(repoDir, wt);
|
|
25460
25553
|
const baseRef = op === "merge" ? tgt.ref : source;
|
|
25461
25554
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
|
|
@@ -25516,7 +25609,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
|
|
|
25516
25609
|
const src = await resolveGraphSource(repoDir, source);
|
|
25517
25610
|
if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
|
|
25518
25611
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25519
|
-
const wt =
|
|
25612
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25520
25613
|
await reclaim(repoDir, wt);
|
|
25521
25614
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25522
25615
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25554,7 +25647,7 @@ async function replayCurrent(repoDir, source, cmd) {
|
|
|
25554
25647
|
const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
25555
25648
|
if (!current || current === "HEAD")
|
|
25556
25649
|
return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
|
|
25557
|
-
const wt =
|
|
25650
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25558
25651
|
await reclaim(repoDir, wt);
|
|
25559
25652
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25560
25653
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25601,24 +25694,24 @@ init_db();
|
|
|
25601
25694
|
// src/services/spec-review.ts
|
|
25602
25695
|
import { execFile as execFile4 } from "node:child_process";
|
|
25603
25696
|
import { promisify as promisify4 } from "node:util";
|
|
25604
|
-
import { mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
25697
|
+
import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
|
|
25605
25698
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
25606
|
-
import { join as
|
|
25699
|
+
import { join as join8, resolve as resolve11 } from "node:path";
|
|
25607
25700
|
var exec4 = promisify4(execFile4);
|
|
25608
25701
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
25609
25702
|
var MAX = 256 * 1024;
|
|
25610
|
-
var worktreeDir = (repoDir, specId) =>
|
|
25703
|
+
var worktreeDir = (repoDir, specId) => join8(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
|
|
25611
25704
|
async function withTempIndex(wt, fn2) {
|
|
25612
25705
|
const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
|
|
25613
|
-
const dir2 = await mkdtemp(
|
|
25614
|
-
const tmp =
|
|
25615
|
-
await copyFile(
|
|
25706
|
+
const dir2 = await mkdtemp(join8(tmpdir2(), "hanoman-idx-"));
|
|
25707
|
+
const tmp = join8(dir2, "index");
|
|
25708
|
+
await copyFile(resolve11(wt, idx), tmp);
|
|
25616
25709
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
25617
25710
|
try {
|
|
25618
25711
|
await exec4("git", ["add", "-A", "-N"], { cwd: wt, env, ...GIT3 });
|
|
25619
25712
|
return await fn2(env);
|
|
25620
25713
|
} finally {
|
|
25621
|
-
await
|
|
25714
|
+
await rm2(dir2, { recursive: true, force: true });
|
|
25622
25715
|
}
|
|
25623
25716
|
}
|
|
25624
25717
|
var splitZ = (s2) => s2.split("\0").filter(Boolean);
|
|
@@ -25822,7 +25915,7 @@ init_notifications();
|
|
|
25822
25915
|
|
|
25823
25916
|
// src/services/session-result.ts
|
|
25824
25917
|
init_db();
|
|
25825
|
-
import { randomUUID as
|
|
25918
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25826
25919
|
var WHITELIST = [
|
|
25827
25920
|
"projectId",
|
|
25828
25921
|
"specId",
|
|
@@ -25836,7 +25929,7 @@ var WHITELIST = [
|
|
|
25836
25929
|
"author"
|
|
25837
25930
|
];
|
|
25838
25931
|
async function recordSessionResult(input) {
|
|
25839
|
-
const id =
|
|
25932
|
+
const id = randomUUID4();
|
|
25840
25933
|
const data = { id };
|
|
25841
25934
|
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25842
25935
|
if (data.status === void 0) data.status = "done";
|
|
@@ -29185,7 +29278,7 @@ import { promisify as promisify7 } from "node:util";
|
|
|
29185
29278
|
import { execFile as execFile6 } from "node:child_process";
|
|
29186
29279
|
import { promisify as promisify6 } from "node:util";
|
|
29187
29280
|
import { existsSync as existsSync9 } from "node:fs";
|
|
29188
|
-
import { resolve as
|
|
29281
|
+
import { resolve as resolve12 } from "node:path";
|
|
29189
29282
|
var exec6 = promisify6(execFile6);
|
|
29190
29283
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
29191
29284
|
var MAX2 = 256 * 1024;
|
|
@@ -29193,7 +29286,7 @@ var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
|
29193
29286
|
function repoAbsPath(repoDir, rel, allowMissingFinal = false) {
|
|
29194
29287
|
if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
29195
29288
|
assertSafeRepoPathSync(repoDir, rel, allowMissingFinal, true);
|
|
29196
|
-
return
|
|
29289
|
+
return resolve12(repoDir, rel);
|
|
29197
29290
|
}
|
|
29198
29291
|
async function listRepoTree(repoDir, ref = "") {
|
|
29199
29292
|
if (!repoDir || !existsSync9(repoDir)) return [];
|
|
@@ -29284,10 +29377,10 @@ async function listGraph(repoDir, limit = 200, opts = {}) {
|
|
|
29284
29377
|
const [sha, parents, author, at, subject, refs2] = line.split(US);
|
|
29285
29378
|
const tags = [], branchRefs = [];
|
|
29286
29379
|
for (const d of (refs2 ?? "").split(",").map((r) => r.trim()).filter(Boolean)) {
|
|
29287
|
-
const
|
|
29288
|
-
if (
|
|
29289
|
-
if (
|
|
29290
|
-
else branchRefs.push(
|
|
29380
|
+
const clean2 = d.replace(/^HEAD -> /, "");
|
|
29381
|
+
if (clean2 === "HEAD") continue;
|
|
29382
|
+
if (clean2.startsWith("tag: ")) tags.push(clean2.slice(5));
|
|
29383
|
+
else branchRefs.push(clean2);
|
|
29291
29384
|
}
|
|
29292
29385
|
return {
|
|
29293
29386
|
sha,
|
|
@@ -29763,6 +29856,85 @@ async function deleteBranches(repoDir, names, opts) {
|
|
|
29763
29856
|
return { base: report.base, results };
|
|
29764
29857
|
}
|
|
29765
29858
|
|
|
29859
|
+
// src/services/repo-fs.ts
|
|
29860
|
+
import { lstat as lstat2, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
29861
|
+
import { createWriteStream } from "node:fs";
|
|
29862
|
+
import { join as join9 } from "node:path";
|
|
29863
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
29864
|
+
import { pipeline } from "node:stream/promises";
|
|
29865
|
+
var EntryExistsError = class extends Error {
|
|
29866
|
+
code = "ENTRY_EXISTS";
|
|
29867
|
+
};
|
|
29868
|
+
var EntryMissingError = class extends Error {
|
|
29869
|
+
code = "ENTRY_MISSING";
|
|
29870
|
+
};
|
|
29871
|
+
var EntryTargetInsideError = class extends Error {
|
|
29872
|
+
code = "ENTRY_TARGET_INSIDE";
|
|
29873
|
+
};
|
|
29874
|
+
function assertNotGit(rel) {
|
|
29875
|
+
if (rel.split(/[\\/]/).includes(".git"))
|
|
29876
|
+
throw new PathContainmentError("repository path ditolak: tidak boleh menyentuh .git");
|
|
29877
|
+
}
|
|
29878
|
+
var clean = (s2) => s2.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
29879
|
+
function joinRel(dir2, name2) {
|
|
29880
|
+
const d = clean(dir2), n2 = clean(name2);
|
|
29881
|
+
if (!n2) throw new PathContainmentError("repository path ditolak: nama berkas kosong");
|
|
29882
|
+
return d ? `${d}/${n2}` : n2;
|
|
29883
|
+
}
|
|
29884
|
+
async function entryKind(repoDir, rel) {
|
|
29885
|
+
assertNotGit(rel);
|
|
29886
|
+
const { path: path2 } = await resolveRepoEntry(repoDir, rel, { allowMissingTail: true });
|
|
29887
|
+
const st = await lstat2(path2).catch(() => null);
|
|
29888
|
+
if (!st) return null;
|
|
29889
|
+
return st.isDirectory() ? "dir" : "file";
|
|
29890
|
+
}
|
|
29891
|
+
async function createEntry(repoDir, rel, kind) {
|
|
29892
|
+
if (await entryKind(repoDir, rel)) throw new EntryExistsError(`sudah ada: ${rel}`);
|
|
29893
|
+
await writeRepoFileAtomic(repoDir, kind === "dir" ? `${clean(rel)}/.gitkeep` : rel, "");
|
|
29894
|
+
return { path: rel };
|
|
29895
|
+
}
|
|
29896
|
+
async function renameEntry(repoDir, from, to) {
|
|
29897
|
+
const kind = await entryKind(repoDir, from);
|
|
29898
|
+
if (!kind) throw new EntryMissingError(`tidak ada: ${from}`);
|
|
29899
|
+
if (await entryKind(repoDir, to)) throw new EntryExistsError(`sudah ada: ${to}`);
|
|
29900
|
+
if (kind === "dir" && `${clean(to)}/`.startsWith(`${clean(from)}/`))
|
|
29901
|
+
throw new EntryTargetInsideError("tujuan di dalam sumber");
|
|
29902
|
+
await ensureRepoParents(repoDir, to);
|
|
29903
|
+
const src = await resolveRepoEntry(repoDir, from);
|
|
29904
|
+
const dst = await resolveRepoEntry(repoDir, to, { allowMissingFinal: true });
|
|
29905
|
+
await rename2(src.path, dst.path);
|
|
29906
|
+
return { from, to };
|
|
29907
|
+
}
|
|
29908
|
+
async function deleteEntry(repoDir, rel) {
|
|
29909
|
+
const kind = await entryKind(repoDir, rel);
|
|
29910
|
+
if (!kind) throw new EntryMissingError(`tidak ada: ${rel}`);
|
|
29911
|
+
const { path: path2 } = await resolveRepoEntry(repoDir, rel);
|
|
29912
|
+
await rm3(path2, { recursive: kind === "dir", force: false });
|
|
29913
|
+
return { path: rel, kind };
|
|
29914
|
+
}
|
|
29915
|
+
async function saveUpload(repoDir, rel, source, opts = {}) {
|
|
29916
|
+
assertNotGit(rel);
|
|
29917
|
+
await ensureRepoParents(repoDir, rel);
|
|
29918
|
+
const entry = await resolveRepoEntry(repoDir, rel, { allowMissingFinal: true });
|
|
29919
|
+
const current = await lstat2(entry.path).catch(() => null);
|
|
29920
|
+
if (current && !opts.overwrite) return { status: "exists" };
|
|
29921
|
+
if (current && !current.isFile())
|
|
29922
|
+
throw new PathContainmentError("repository path ditolak: target bukan file regular");
|
|
29923
|
+
const temp = join9(entry.parent, `.hanoman-${randomUUID5()}.tmp`);
|
|
29924
|
+
try {
|
|
29925
|
+
await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
|
|
29926
|
+
if (opts.isTruncated?.()) {
|
|
29927
|
+
await rm3(temp, { force: true });
|
|
29928
|
+
return { status: "too-large" };
|
|
29929
|
+
}
|
|
29930
|
+
await rename2(temp, entry.path);
|
|
29931
|
+
return { status: "written" };
|
|
29932
|
+
} catch (error) {
|
|
29933
|
+
await rm3(temp, { force: true });
|
|
29934
|
+
throw error;
|
|
29935
|
+
}
|
|
29936
|
+
}
|
|
29937
|
+
|
|
29766
29938
|
// src/routes/ide.ts
|
|
29767
29939
|
async function repoOf(id) {
|
|
29768
29940
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
@@ -29781,6 +29953,19 @@ async function lockInputs(id) {
|
|
|
29781
29953
|
sessionBranches: new Set(sessions)
|
|
29782
29954
|
};
|
|
29783
29955
|
}
|
|
29956
|
+
function entryError(reply, e) {
|
|
29957
|
+
if (e instanceof EntryMissingError) return reply.code(404).send({ error: "not found" });
|
|
29958
|
+
if (e instanceof EntryExistsError) return reply.code(409).send({ error: "sudah ada" });
|
|
29959
|
+
if (e instanceof EntryTargetInsideError) return reply.code(400).send({ error: "tujuan di dalam sumber" });
|
|
29960
|
+
return reply.code(400).send({ error: e.message });
|
|
29961
|
+
}
|
|
29962
|
+
var UPLOAD_LIMITS = {
|
|
29963
|
+
fileSize: 100 * 1024 * 1024,
|
|
29964
|
+
files: 1e3,
|
|
29965
|
+
fields: 10,
|
|
29966
|
+
fieldSize: 1024 * 1024
|
|
29967
|
+
};
|
|
29968
|
+
var UPLOAD_TOTAL_MAX = 2 * 1024 * 1024 * 1024;
|
|
29784
29969
|
async function ide_default(app2) {
|
|
29785
29970
|
app2.get("/projects/:id/tree", async (req, reply) => {
|
|
29786
29971
|
const repoDir = await repoOf(req.params.id);
|
|
@@ -29845,6 +30030,102 @@ async function ide_default(app2) {
|
|
|
29845
30030
|
return reply.code(400).send({ error: e.message });
|
|
29846
30031
|
}
|
|
29847
30032
|
});
|
|
30033
|
+
app2.post("/projects/:id/entry", async (req, reply) => {
|
|
30034
|
+
const repoDir = await repoOf(req.params.id);
|
|
30035
|
+
if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
|
|
30036
|
+
if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
|
|
30037
|
+
const b = req.body;
|
|
30038
|
+
if (!b?.path || typeof b.path !== "string") return reply.code(400).send({ error: "path wajib" });
|
|
30039
|
+
if (b.kind !== "file" && b.kind !== "dir") return reply.code(400).send({ error: "kind harus file atau dir" });
|
|
30040
|
+
try {
|
|
30041
|
+
return reply.code(201).send(await createEntry(repoDir, b.path, b.kind));
|
|
30042
|
+
} catch (e) {
|
|
30043
|
+
return entryError(reply, e);
|
|
30044
|
+
}
|
|
30045
|
+
});
|
|
30046
|
+
app2.patch("/projects/:id/entry", async (req, reply) => {
|
|
30047
|
+
const repoDir = await repoOf(req.params.id);
|
|
30048
|
+
if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
|
|
30049
|
+
if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
|
|
30050
|
+
const b = req.body;
|
|
30051
|
+
if (!b?.from || !b?.to || typeof b.from !== "string" || typeof b.to !== "string")
|
|
30052
|
+
return reply.code(400).send({ error: "from & to wajib" });
|
|
30053
|
+
try {
|
|
30054
|
+
return await renameEntry(repoDir, b.from, b.to);
|
|
30055
|
+
} catch (e) {
|
|
30056
|
+
return entryError(reply, e);
|
|
30057
|
+
}
|
|
30058
|
+
});
|
|
30059
|
+
app2.delete("/projects/:id/entry", async (req, reply) => {
|
|
30060
|
+
const repoDir = await repoOf(req.params.id);
|
|
30061
|
+
if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
|
|
30062
|
+
if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
|
|
30063
|
+
const { path: path2 } = req.query;
|
|
30064
|
+
if (!path2) return reply.code(400).send({ error: "path wajib" });
|
|
30065
|
+
try {
|
|
30066
|
+
return await deleteEntry(repoDir, path2);
|
|
30067
|
+
} catch (e) {
|
|
30068
|
+
return entryError(reply, e);
|
|
30069
|
+
}
|
|
30070
|
+
});
|
|
30071
|
+
app2.post("/projects/:id/upload", async (req, reply) => {
|
|
30072
|
+
const repoDir = await repoOf(req.params.id);
|
|
30073
|
+
if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
|
|
30074
|
+
if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
|
|
30075
|
+
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
30076
|
+
const totalMax = Number(process.env.HANOMAN_IDE_UPLOAD_MAX_BYTES) || UPLOAD_TOTAL_MAX;
|
|
30077
|
+
let dir2 = "", overwrite = false, manifest = null, seen = 0, total = 0;
|
|
30078
|
+
const written = [];
|
|
30079
|
+
const skipped2 = [];
|
|
30080
|
+
for await (const part of req.parts({ limits: UPLOAD_LIMITS })) {
|
|
30081
|
+
if (part.type === "field") {
|
|
30082
|
+
if (part.fieldname === "dir") dir2 = String(part.value ?? "");
|
|
30083
|
+
else if (part.fieldname === "overwrite") overwrite = part.value === "1" || part.value === "true";
|
|
30084
|
+
else if (part.fieldname === "manifest") {
|
|
30085
|
+
try {
|
|
30086
|
+
const parsed = JSON.parse(String(part.value));
|
|
30087
|
+
if (!Array.isArray(parsed) || parsed.some((p) => typeof p !== "string")) throw new Error("bentuk");
|
|
30088
|
+
manifest = parsed;
|
|
30089
|
+
} catch {
|
|
30090
|
+
return reply.code(400).send({ error: "manifest tak sah" });
|
|
30091
|
+
}
|
|
30092
|
+
}
|
|
30093
|
+
continue;
|
|
30094
|
+
}
|
|
30095
|
+
const name2 = manifest ? manifest[seen] : part.filename;
|
|
30096
|
+
seen++;
|
|
30097
|
+
if (name2 === void 0) {
|
|
30098
|
+
part.file.resume();
|
|
30099
|
+
return reply.code(400).send({ error: "manifest tak cocok dengan berkas" });
|
|
30100
|
+
}
|
|
30101
|
+
let rel = name2;
|
|
30102
|
+
try {
|
|
30103
|
+
rel = joinRel(dir2, name2);
|
|
30104
|
+
if (total >= totalMax) {
|
|
30105
|
+
part.file.resume();
|
|
30106
|
+
skipped2.push({ path: rel, reason: "budget" });
|
|
30107
|
+
continue;
|
|
30108
|
+
}
|
|
30109
|
+
const r = await saveUpload(repoDir, rel, part.file, {
|
|
30110
|
+
overwrite,
|
|
30111
|
+
isTruncated: () => part.file.truncated === true
|
|
30112
|
+
});
|
|
30113
|
+
if (r.status === "written") {
|
|
30114
|
+
written.push(rel);
|
|
30115
|
+
total += Number(part.file.bytesRead ?? 0);
|
|
30116
|
+
} else {
|
|
30117
|
+
part.file.resume();
|
|
30118
|
+
skipped2.push({ path: rel, reason: r.status });
|
|
30119
|
+
}
|
|
30120
|
+
} catch {
|
|
30121
|
+
part.file.resume();
|
|
30122
|
+
skipped2.push({ path: name2, reason: "denied" });
|
|
30123
|
+
}
|
|
30124
|
+
}
|
|
30125
|
+
if (manifest && seen !== manifest.length)
|
|
30126
|
+
return reply.code(400).send({ error: "manifest tak cocok dengan berkas" });
|
|
30127
|
+
return { written, skipped: skipped2 };
|
|
30128
|
+
});
|
|
29848
30129
|
app2.get("/projects/:id/graph", async (req, reply) => {
|
|
29849
30130
|
const repoDir = await repoOf(req.params.id);
|
|
29850
30131
|
if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
|
|
@@ -30083,14 +30364,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
30083
30364
|
// src/routes/fs.ts
|
|
30084
30365
|
import { readdir } from "node:fs/promises";
|
|
30085
30366
|
import { homedir as homedir4 } from "node:os";
|
|
30086
|
-
import { resolve as
|
|
30367
|
+
import { resolve as resolve13, dirname as dirname9, join as join10 } from "node:path";
|
|
30087
30368
|
async function fs_default(app2) {
|
|
30088
30369
|
app2.get("/fs/browse", async (req, reply) => {
|
|
30089
30370
|
const q = req.query.path;
|
|
30090
|
-
const dir2 = q && q.trim() ?
|
|
30371
|
+
const dir2 = q && q.trim() ? resolve13(q.trim()) : homedir4();
|
|
30091
30372
|
try {
|
|
30092
30373
|
const ents = await readdir(dir2, { withFileTypes: true });
|
|
30093
|
-
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path:
|
|
30374
|
+
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join10(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
30094
30375
|
const parent = dirname9(dir2);
|
|
30095
30376
|
return { path: dir2, parent: parent === dir2 ? null : parent, entries };
|
|
30096
30377
|
} catch {
|
|
@@ -30524,11 +30805,11 @@ function installCommand(m, agent, shell = shellBin()) {
|
|
|
30524
30805
|
}
|
|
30525
30806
|
|
|
30526
30807
|
// src/services/session-worktree.ts
|
|
30527
|
-
import { resolve as
|
|
30808
|
+
import { resolve as resolve14, sep as sep3 } from "node:path";
|
|
30528
30809
|
function ownsWorktree(repoDir, cwd) {
|
|
30529
|
-
const base2 =
|
|
30530
|
-
const dir2 =
|
|
30531
|
-
const container =
|
|
30810
|
+
const base2 = resolve14(repoDir);
|
|
30811
|
+
const dir2 = resolve14(cwd);
|
|
30812
|
+
const container = resolve14(base2, ".worktrees");
|
|
30532
30813
|
return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
|
|
30533
30814
|
}
|
|
30534
30815
|
|
|
@@ -30536,15 +30817,15 @@ function ownsWorktree(repoDir, cwd) {
|
|
|
30536
30817
|
init_src2();
|
|
30537
30818
|
init_db();
|
|
30538
30819
|
import { execFile as execFile8 } from "node:child_process";
|
|
30539
|
-
import { readdir as readdir2, rm as
|
|
30820
|
+
import { readdir as readdir2, rm as rm4 } from "node:fs/promises";
|
|
30540
30821
|
import { promisify as promisify9 } from "node:util";
|
|
30541
|
-
import { join as
|
|
30822
|
+
import { join as join11, resolve as resolve15 } from "node:path";
|
|
30542
30823
|
init_notifications();
|
|
30543
30824
|
var TICK_MS = 6e4;
|
|
30544
|
-
var trashDirOf = (repoDir) =>
|
|
30825
|
+
var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
|
|
30545
30826
|
var exec8 = promisify9(execFile8);
|
|
30546
30827
|
var prodReaperDeps = {
|
|
30547
|
-
rm: (path2) =>
|
|
30828
|
+
rm: (path2) => rm4(path2, { recursive: true, force: true }),
|
|
30548
30829
|
prune: async (repoDir) => {
|
|
30549
30830
|
try {
|
|
30550
30831
|
await exec8("git", ["worktree", "prune"], { cwd: repoDir, timeout: 3e4 });
|
|
@@ -30598,7 +30879,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
|
|
|
30598
30879
|
}
|
|
30599
30880
|
let removed = 0;
|
|
30600
30881
|
for (const entry of entries) {
|
|
30601
|
-
const path2 =
|
|
30882
|
+
const path2 = join11(dir2, entry);
|
|
30602
30883
|
const known = pending.get(path2);
|
|
30603
30884
|
const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
|
|
30604
30885
|
pending.set(path2, row);
|
|
@@ -30642,6 +30923,8 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30642
30923
|
init_notifications();
|
|
30643
30924
|
init_stage_machine();
|
|
30644
30925
|
init_pty();
|
|
30926
|
+
init_uploads();
|
|
30927
|
+
var ATTACHMENT_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
30645
30928
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
30646
30929
|
async function advanceStage(specId, repoDir, sessionId2, flow, worktree) {
|
|
30647
30930
|
const next = stageForRun(readPhases(phaseFilePath(repoDir, sessionId2), flow), worktree, specId);
|
|
@@ -30965,6 +31248,18 @@ async function terminal_default(app2, opts) {
|
|
|
30965
31248
|
killSession(id);
|
|
30966
31249
|
return reply.code(202).send({ cleanup: null });
|
|
30967
31250
|
});
|
|
31251
|
+
app2.post("/terminal/sessions/:id/attachments", async (req, reply) => {
|
|
31252
|
+
const { id } = req.params;
|
|
31253
|
+
if (!getSession(id)) return reply.code(404).send({ error: "not found" });
|
|
31254
|
+
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
31255
|
+
const part = await req.file?.();
|
|
31256
|
+
if (!part) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
31257
|
+
const buf = await part.toBuffer();
|
|
31258
|
+
if (part.file?.truncated) return reply.code(413).send({ error: "berkas melebihi 5 MB" });
|
|
31259
|
+
if (!ATTACHMENT_MIME.has(part.mimetype)) return reply.code(415).send({ error: "tipe berkas tak didukung" });
|
|
31260
|
+
const { path: path2 } = await saveSessionUpload(id, buf, part.mimetype);
|
|
31261
|
+
return { path: path2 };
|
|
31262
|
+
});
|
|
30968
31263
|
app2.get("/terminal/sessions/:id/ws", {
|
|
30969
31264
|
websocket: true,
|
|
30970
31265
|
preValidation: async (req, reply) => {
|
|
@@ -31203,7 +31498,7 @@ init_config2();
|
|
|
31203
31498
|
import { spawn as spawn3 } from "node:child_process";
|
|
31204
31499
|
import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
31205
31500
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
31206
|
-
import { dirname as dirname10, join as
|
|
31501
|
+
import { dirname as dirname10, join as join12 } from "node:path";
|
|
31207
31502
|
var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
|
|
31208
31503
|
function consoleArgv(t) {
|
|
31209
31504
|
return [
|
|
@@ -31218,8 +31513,8 @@ function consoleArgv(t) {
|
|
|
31218
31513
|
];
|
|
31219
31514
|
}
|
|
31220
31515
|
function askpassScript() {
|
|
31221
|
-
const dir2 = mkdtempSync(
|
|
31222
|
-
const path2 =
|
|
31516
|
+
const dir2 = mkdtempSync(join12(tmpdir3(), "hanoman-askpass-"));
|
|
31517
|
+
const path2 = join12(dir2, "askpass.sh");
|
|
31223
31518
|
writeFileSync4(path2, `#!/bin/sh
|
|
31224
31519
|
printf '%s' "$HANOMAN_SSH_PASSWORD"
|
|
31225
31520
|
`, { mode: 448 });
|
|
@@ -31280,15 +31575,15 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
31280
31575
|
// src/services/vps-audit.ts
|
|
31281
31576
|
init_db();
|
|
31282
31577
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
31283
|
-
import { join as
|
|
31578
|
+
import { join as join14 } from "node:path";
|
|
31284
31579
|
|
|
31285
31580
|
// src/runner/deps.ts
|
|
31286
31581
|
import { existsSync as existsSync11 } from "node:fs";
|
|
31287
|
-
import { dirname as dirname11, join as
|
|
31582
|
+
import { dirname as dirname11, join as join13 } from "node:path";
|
|
31288
31583
|
function repoRootFrom(startDir) {
|
|
31289
31584
|
let dir2 = startDir;
|
|
31290
31585
|
for (let i = 0; i < 8; i++) {
|
|
31291
|
-
if (existsSync11(
|
|
31586
|
+
if (existsSync11(join13(dir2, "pnpm-workspace.yaml"))) return dir2;
|
|
31292
31587
|
const parent = dirname11(dir2);
|
|
31293
31588
|
if (parent === dir2) break;
|
|
31294
31589
|
dir2 = parent;
|
|
@@ -31394,7 +31689,7 @@ function parseHealth(out3) {
|
|
|
31394
31689
|
return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
|
|
31395
31690
|
}
|
|
31396
31691
|
var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
|
|
31397
|
-
var scriptPath = (f) =>
|
|
31692
|
+
var scriptPath = (f) => join14(repoRoot(), "server", "scripts", "vps", f);
|
|
31398
31693
|
async function itemStatesOf(vpsId) {
|
|
31399
31694
|
const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
|
|
31400
31695
|
return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
|
|
@@ -31543,11 +31838,11 @@ init_config2();
|
|
|
31543
31838
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
31544
31839
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
31545
31840
|
import { homedir as homedir5 } from "node:os";
|
|
31546
|
-
import { join as
|
|
31547
|
-
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ??
|
|
31841
|
+
import { join as join15 } from "node:path";
|
|
31842
|
+
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join15(homedir5(), ".hanoman");
|
|
31548
31843
|
function ensureHanomanKey() {
|
|
31549
31844
|
const dir2 = keyDir();
|
|
31550
|
-
const privPath =
|
|
31845
|
+
const privPath = join15(dir2, "id_ed25519");
|
|
31551
31846
|
const pubPath = `${privPath}.pub`;
|
|
31552
31847
|
if (!existsSync12(privPath)) {
|
|
31553
31848
|
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
@@ -31785,7 +32080,7 @@ init_config2();
|
|
|
31785
32080
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
31786
32081
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
31787
32082
|
import { homedir as homedir7 } from "node:os";
|
|
31788
|
-
import { join as
|
|
32083
|
+
import { join as join16 } from "node:path";
|
|
31789
32084
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
31790
32085
|
var TTL_MS2 = 3e4;
|
|
31791
32086
|
var lastOk = null;
|
|
@@ -31798,7 +32093,7 @@ var LABELS = {
|
|
|
31798
32093
|
var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
31799
32094
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
31800
32095
|
function credsFile() {
|
|
31801
|
-
return
|
|
32096
|
+
return join16(effectiveStr("CLAUDE_CONFIG_DIR") ?? join16(homedir7(), ".claude"), ".credentials.json");
|
|
31802
32097
|
}
|
|
31803
32098
|
function readAccessToken() {
|
|
31804
32099
|
if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
|
|
@@ -31872,14 +32167,14 @@ async function getLimits() {
|
|
|
31872
32167
|
// src/services/codex-limits.ts
|
|
31873
32168
|
import { open as open3, readdir as readdir3, stat } from "node:fs/promises";
|
|
31874
32169
|
import { homedir as homedir8 } from "node:os";
|
|
31875
|
-
import { join as
|
|
32170
|
+
import { join as join17 } from "node:path";
|
|
31876
32171
|
var TTL_MS3 = 3e4;
|
|
31877
32172
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
31878
32173
|
var TAIL_BYTES = 512 * 1024;
|
|
31879
32174
|
var MAX_FILES = 8;
|
|
31880
32175
|
var cache2 = null;
|
|
31881
32176
|
var freshUntil2 = 0;
|
|
31882
|
-
var codexSessionsDir = () =>
|
|
32177
|
+
var codexSessionsDir = () => join17(process.env.CODEX_HOME ?? join17(homedir8(), ".codex"), "sessions");
|
|
31883
32178
|
var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
|
|
31884
32179
|
function windowLabel(minutes) {
|
|
31885
32180
|
if (minutes === 300) return "Sesi 5 jam";
|
|
@@ -31912,7 +32207,7 @@ async function recentRollouts(dir2) {
|
|
|
31912
32207
|
return [];
|
|
31913
32208
|
}
|
|
31914
32209
|
const stamped = await Promise.all(entries.map(async (rel) => {
|
|
31915
|
-
const full =
|
|
32210
|
+
const full = join17(dir2, rel);
|
|
31916
32211
|
try {
|
|
31917
32212
|
return { full, mtime: (await stat(full)).mtimeMs };
|
|
31918
32213
|
} catch {
|
|
@@ -32040,7 +32335,7 @@ init_src();
|
|
|
32040
32335
|
init_src();
|
|
32041
32336
|
init_config2();
|
|
32042
32337
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
32043
|
-
import { resolve as
|
|
32338
|
+
import { resolve as resolve16, dirname as dirname12 } from "node:path";
|
|
32044
32339
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32045
32340
|
var UPDATE_COMMAND = "npm i -g hanoman@latest";
|
|
32046
32341
|
function composeUpdate(x) {
|
|
@@ -32066,7 +32361,7 @@ var lastLatest = null;
|
|
|
32066
32361
|
var lastStatus = "unavailable";
|
|
32067
32362
|
function runningVersion() {
|
|
32068
32363
|
const here = dirname12(fileURLToPath3(import.meta.url));
|
|
32069
|
-
for (const p of [
|
|
32364
|
+
for (const p of [resolve16(here, "build-info.json"), resolve16(here, "../package.json")]) {
|
|
32070
32365
|
try {
|
|
32071
32366
|
const v = JSON.parse(readFileSync12(p, "utf8")).version;
|
|
32072
32367
|
if (typeof v === "string" && v) return v;
|
|
@@ -32417,54 +32712,7 @@ setAcceptedHook((row) => broadcastSyncLog(row));
|
|
|
32417
32712
|
init_sync();
|
|
32418
32713
|
init_sync_client();
|
|
32419
32714
|
init_conflicts();
|
|
32420
|
-
|
|
32421
|
-
// src/services/uploads.ts
|
|
32422
|
-
init_config2();
|
|
32423
|
-
init_safe_outbound_request();
|
|
32424
|
-
init_src2();
|
|
32425
|
-
import { mkdir as mkdir2, writeFile, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
32426
|
-
import { join as join16, resolve as resolve16 } from "node:path";
|
|
32427
|
-
function uploadDir() {
|
|
32428
|
-
return resolve16(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join16(resolveHome(), "uploads"));
|
|
32429
|
-
}
|
|
32430
|
-
async function readUpload(storageKey) {
|
|
32431
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32432
|
-
return readFile2(join16(uploadDir(), safe));
|
|
32433
|
-
}
|
|
32434
|
-
async function readUploadOrFetch(storageKey) {
|
|
32435
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32436
|
-
const target2 = join16(uploadDir(), safe);
|
|
32437
|
-
try {
|
|
32438
|
-
return await readFile2(target2);
|
|
32439
|
-
} catch {
|
|
32440
|
-
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
32441
|
-
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
32442
|
-
if (!base2 || !token) throw new Error(`lampiran ${safe} tak ada lokal & bukan client sync`);
|
|
32443
|
-
const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe)}`, `${base2.replace(/\/$/, "")}/`);
|
|
32444
|
-
const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
|
|
32445
|
-
const res = await safeRequest({
|
|
32446
|
-
url: url2,
|
|
32447
|
-
method: "GET",
|
|
32448
|
-
headers: { authorization: `Bearer ${token}` },
|
|
32449
|
-
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
32450
|
-
connectMs: 5e3,
|
|
32451
|
-
totalMs: 15e3,
|
|
32452
|
-
maxResponseBytes: 5 * 1024 * 1024
|
|
32453
|
-
});
|
|
32454
|
-
if (res.status < 200 || res.status >= 300) throw new Error(`fetch lampiran hub gagal: ${res.status}`);
|
|
32455
|
-
const buf = res.body;
|
|
32456
|
-
await mkdir2(uploadDir(), { recursive: true, mode: 448 });
|
|
32457
|
-
await writeFile(target2, buf, { mode: 384 });
|
|
32458
|
-
return buf;
|
|
32459
|
-
}
|
|
32460
|
-
}
|
|
32461
|
-
async function deleteUpload(storageKey) {
|
|
32462
|
-
const safe = storageKey.replace(/[/\\]/g, "");
|
|
32463
|
-
await unlink2(join16(uploadDir(), safe)).catch(() => {
|
|
32464
|
-
});
|
|
32465
|
-
}
|
|
32466
|
-
|
|
32467
|
-
// src/routes/sync.ts
|
|
32715
|
+
init_uploads();
|
|
32468
32716
|
init_config2();
|
|
32469
32717
|
var zPush = external_exports.object({
|
|
32470
32718
|
records: external_exports.array(external_exports.object({
|
|
@@ -32615,17 +32863,17 @@ async function session_results_default(app2) {
|
|
|
32615
32863
|
// src/services/session-history.ts
|
|
32616
32864
|
init_db();
|
|
32617
32865
|
init_pty();
|
|
32618
|
-
import { randomUUID as
|
|
32866
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
32619
32867
|
|
|
32620
32868
|
// src/services/transcript-store.ts
|
|
32621
32869
|
init_config2();
|
|
32622
32870
|
init_src2();
|
|
32623
|
-
import { randomUUID as
|
|
32871
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
32624
32872
|
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
|
|
32625
|
-
import { join as
|
|
32873
|
+
import { join as join18, resolve as resolve17, basename as basename4 } from "node:path";
|
|
32626
32874
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
32627
32875
|
function transcriptDir() {
|
|
32628
|
-
return resolve17(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ??
|
|
32876
|
+
return resolve17(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join18(resolveHome(), "transcripts"));
|
|
32629
32877
|
}
|
|
32630
32878
|
function clamp(text) {
|
|
32631
32879
|
const buf = Buffer.from(text, "utf8");
|
|
@@ -32642,21 +32890,21 @@ async function saveTranscript(text) {
|
|
|
32642
32890
|
const { body, truncated } = clamp(text);
|
|
32643
32891
|
const dir2 = transcriptDir();
|
|
32644
32892
|
await mkdir3(dir2, { recursive: true, mode: 448 });
|
|
32645
|
-
const key = `${
|
|
32646
|
-
await writeFile2(
|
|
32893
|
+
const key = `${randomUUID6()}.log`;
|
|
32894
|
+
await writeFile2(join18(dir2, key), body, { encoding: "utf8", mode: 384 });
|
|
32647
32895
|
return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
|
|
32648
32896
|
}
|
|
32649
32897
|
async function readTranscript(key) {
|
|
32650
32898
|
if (!key) return null;
|
|
32651
32899
|
try {
|
|
32652
|
-
return await readFile3(
|
|
32900
|
+
return await readFile3(join18(transcriptDir(), basename4(key)), "utf8");
|
|
32653
32901
|
} catch {
|
|
32654
32902
|
return null;
|
|
32655
32903
|
}
|
|
32656
32904
|
}
|
|
32657
32905
|
async function deleteTranscript(key) {
|
|
32658
32906
|
if (!key) return;
|
|
32659
|
-
await unlink3(
|
|
32907
|
+
await unlink3(join18(transcriptDir(), basename4(key))).catch(() => {
|
|
32660
32908
|
});
|
|
32661
32909
|
}
|
|
32662
32910
|
|
|
@@ -32687,7 +32935,7 @@ async function titleFor(specId) {
|
|
|
32687
32935
|
async function beginSession(b) {
|
|
32688
32936
|
await prisma.sessionHistory.create({
|
|
32689
32937
|
data: {
|
|
32690
|
-
id:
|
|
32938
|
+
id: randomUUID7(),
|
|
32691
32939
|
sessionId: b.sessionId,
|
|
32692
32940
|
projectId: b.projectId,
|
|
32693
32941
|
specId: b.specId ?? null,
|
|
@@ -32892,8 +33140,9 @@ init_db();
|
|
|
32892
33140
|
|
|
32893
33141
|
// src/services/ticket.ts
|
|
32894
33142
|
init_db();
|
|
32895
|
-
|
|
33143
|
+
init_uploads();
|
|
32896
33144
|
init_src();
|
|
33145
|
+
import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
|
|
32897
33146
|
function hashAccessKey(key) {
|
|
32898
33147
|
return createHash7("sha256").update(key).digest("hex");
|
|
32899
33148
|
}
|
|
@@ -32938,12 +33187,13 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32938
33187
|
// src/services/ticket-intake.ts
|
|
32939
33188
|
init_db();
|
|
32940
33189
|
init_notifications();
|
|
33190
|
+
init_uploads();
|
|
32941
33191
|
|
|
32942
33192
|
// src/services/upload-pipeline.ts
|
|
32943
|
-
import { randomUUID as
|
|
33193
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
32944
33194
|
import { spawn as spawn4 } from "node:child_process";
|
|
32945
|
-
import { mkdir as mkdir4, rename as
|
|
32946
|
-
import { basename as basename5, isAbsolute as isAbsolute4, join as
|
|
33195
|
+
import { mkdir as mkdir4, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises";
|
|
33196
|
+
import { basename as basename5, isAbsolute as isAbsolute4, join as join19 } from "node:path";
|
|
32947
33197
|
|
|
32948
33198
|
// ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
|
|
32949
33199
|
var defaultMessages = "End-Of-Stream";
|
|
@@ -40306,7 +40556,8 @@ var dist_default = constructor_default;
|
|
|
40306
40556
|
|
|
40307
40557
|
// src/services/upload-pipeline.ts
|
|
40308
40558
|
init_db();
|
|
40309
|
-
|
|
40559
|
+
init_uploads();
|
|
40560
|
+
var UPLOAD_LIMITS2 = {
|
|
40310
40561
|
fileBytes: 5 * 1024 * 1024,
|
|
40311
40562
|
ticketBytes: 10 * 1024 * 1024,
|
|
40312
40563
|
projectBytes: 250 * 1024 * 1024,
|
|
@@ -40358,7 +40609,7 @@ function scannerFromEnv(path2) {
|
|
|
40358
40609
|
const timer9 = setTimeout(() => {
|
|
40359
40610
|
child.kill("SIGKILL");
|
|
40360
40611
|
reject2(new UploadError("UPLOAD_SCAN", "scanner timeout"));
|
|
40361
|
-
},
|
|
40612
|
+
}, UPLOAD_LIMITS2.scanMs);
|
|
40362
40613
|
child.once("error", (error) => {
|
|
40363
40614
|
clearTimeout(timer9);
|
|
40364
40615
|
reject2(error);
|
|
@@ -40375,9 +40626,9 @@ function safeFilename(input, extension2) {
|
|
|
40375
40626
|
return `${stem || "upload"}${extension2}`;
|
|
40376
40627
|
}
|
|
40377
40628
|
async function processUpload(input, deps = {}) {
|
|
40378
|
-
if (input.buffer.byteLength === 0 || input.buffer.byteLength >
|
|
40629
|
+
if (input.buffer.byteLength === 0 || input.buffer.byteLength > UPLOAD_LIMITS2.fileBytes)
|
|
40379
40630
|
throw new UploadError("UPLOAD_QUOTA", "file quota exceeded");
|
|
40380
|
-
if (input.ticketBytes + input.buffer.byteLength >
|
|
40631
|
+
if (input.ticketBytes + input.buffer.byteLength > UPLOAD_LIMITS2.ticketBytes)
|
|
40381
40632
|
throw new UploadError("UPLOAD_QUOTA", "ticket quota exceeded");
|
|
40382
40633
|
const detected = await fileTypeFromBuffer(input.buffer);
|
|
40383
40634
|
if (!detected || !(detected.mime in TYPES) || detected.mime !== input.clientMime)
|
|
@@ -40388,14 +40639,14 @@ async function processUpload(input, deps = {}) {
|
|
|
40388
40639
|
let height;
|
|
40389
40640
|
let normalized;
|
|
40390
40641
|
try {
|
|
40391
|
-
const source = dist_default(input.buffer, { failOn: "error", limitInputPixels:
|
|
40392
|
-
const metadata2 = await timeout2(source.metadata(),
|
|
40642
|
+
const source = dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS2.pixels, sequentialRead: true });
|
|
40643
|
+
const metadata2 = await timeout2(source.metadata(), UPLOAD_LIMITS2.decodeMs, "UPLOAD_DECODE");
|
|
40393
40644
|
width = metadata2.width ?? 0;
|
|
40394
40645
|
height = metadata2.height ?? 0;
|
|
40395
|
-
if (!width || !height || width >
|
|
40646
|
+
if (!width || !height || width > UPLOAD_LIMITS2.dimension || height > UPLOAD_LIMITS2.dimension || width * height > UPLOAD_LIMITS2.pixels) throw new UploadError("UPLOAD_PIXELS", "image dimensions exceed policy");
|
|
40396
40647
|
normalized = await timeout2(
|
|
40397
|
-
dist_default(input.buffer, { failOn: "error", limitInputPixels:
|
|
40398
|
-
|
|
40648
|
+
dist_default(input.buffer, { failOn: "error", limitInputPixels: UPLOAD_LIMITS2.pixels, sequentialRead: true }).rotate().toFormat(type.format).toBuffer(),
|
|
40649
|
+
UPLOAD_LIMITS2.decodeMs,
|
|
40399
40650
|
"UPLOAD_DECODE"
|
|
40400
40651
|
);
|
|
40401
40652
|
} catch (error) {
|
|
@@ -40403,21 +40654,21 @@ async function processUpload(input, deps = {}) {
|
|
|
40403
40654
|
throw new UploadError("UPLOAD_DECODE", "image decode failed");
|
|
40404
40655
|
}
|
|
40405
40656
|
const usage2 = await (deps.usage ?? defaultUsage)(input.projectId);
|
|
40406
|
-
if (usage2.project + normalized.byteLength >
|
|
40657
|
+
if (usage2.project + normalized.byteLength > UPLOAD_LIMITS2.projectBytes || usage2.global + normalized.byteLength > UPLOAD_LIMITS2.globalBytes)
|
|
40407
40658
|
throw new UploadError("UPLOAD_QUOTA", "storage quota exceeded");
|
|
40408
40659
|
const storageDir = deps.storageDir ?? uploadDir();
|
|
40409
|
-
const quarantineDir =
|
|
40660
|
+
const quarantineDir = join19(storageDir, ".quarantine");
|
|
40410
40661
|
await mkdir4(quarantineDir, { recursive: true, mode: 448 });
|
|
40411
40662
|
await mkdir4(storageDir, { recursive: true, mode: 448 });
|
|
40412
|
-
const quarantine =
|
|
40413
|
-
const storageKey = `${
|
|
40663
|
+
const quarantine = join19(quarantineDir, `${randomUUID8()}.upload`);
|
|
40664
|
+
const storageKey = `${randomUUID8()}${type.extension}`;
|
|
40414
40665
|
await writeFile3(quarantine, normalized, { mode: 384, flag: "wx" });
|
|
40415
40666
|
try {
|
|
40416
|
-
await timeout2((deps.scanner ?? scannerFromEnv)(quarantine),
|
|
40667
|
+
await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS2.scanMs, "UPLOAD_SCAN");
|
|
40417
40668
|
const finalUsage = await (deps.usage ?? defaultUsage)(input.projectId);
|
|
40418
|
-
if (finalUsage.project + normalized.byteLength >
|
|
40669
|
+
if (finalUsage.project + normalized.byteLength > UPLOAD_LIMITS2.projectBytes || finalUsage.global + normalized.byteLength > UPLOAD_LIMITS2.globalBytes)
|
|
40419
40670
|
throw new UploadError("UPLOAD_QUOTA", "storage quota exceeded");
|
|
40420
|
-
await
|
|
40671
|
+
await rename3(quarantine, join19(storageDir, storageKey));
|
|
40421
40672
|
} catch (error) {
|
|
40422
40673
|
await unlink4(quarantine).catch(() => {
|
|
40423
40674
|
});
|
|
@@ -40598,6 +40849,7 @@ async function help_default(app2) {
|
|
|
40598
40849
|
|
|
40599
40850
|
// src/routes/tickets.ts
|
|
40600
40851
|
init_db();
|
|
40852
|
+
init_uploads();
|
|
40601
40853
|
|
|
40602
40854
|
// src/services/ticket-accept.ts
|
|
40603
40855
|
init_db();
|
|
@@ -41534,10 +41786,10 @@ init_config2();
|
|
|
41534
41786
|
init_pty();
|
|
41535
41787
|
init_session_sandbox();
|
|
41536
41788
|
import { execFile as execFile10 } from "node:child_process";
|
|
41537
|
-
import { randomUUID as
|
|
41789
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
41538
41790
|
import { mkdirSync as mkdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
41539
41791
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
41540
|
-
import { join as
|
|
41792
|
+
import { join as join20 } from "node:path";
|
|
41541
41793
|
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
41542
41794
|
function leadArgv(o) {
|
|
41543
41795
|
if (o.agent === "codex") {
|
|
@@ -41565,11 +41817,11 @@ function leadProcess(prompt, o, env = process.env) {
|
|
|
41565
41817
|
const mode = env.HANOMAN_SESSION_SANDBOX ?? (env.NODE_ENV === "production" ? "required" : "off");
|
|
41566
41818
|
if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
|
|
41567
41819
|
} };
|
|
41568
|
-
const promptDir =
|
|
41820
|
+
const promptDir = join20(tmpdir4(), "hanoman-prompts");
|
|
41569
41821
|
mkdirSync9(promptDir, { recursive: true, mode: 448 });
|
|
41570
|
-
const promptFile =
|
|
41822
|
+
const promptFile = join20(promptDir, `oneshot-${randomUUID9()}`);
|
|
41571
41823
|
writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
|
|
41572
|
-
const workspace = o.cwd ??
|
|
41824
|
+
const workspace = o.cwd ?? join20(tmpdir4(), `hanoman-oneshot-${randomUUID9()}`);
|
|
41573
41825
|
if (!o.cwd) mkdirSync9(workspace, { recursive: false, mode: 448 });
|
|
41574
41826
|
try {
|
|
41575
41827
|
const argsWithoutPrompt = directArgs.slice(0, -1);
|
|
@@ -43358,7 +43610,7 @@ init_db();
|
|
|
43358
43610
|
init_src();
|
|
43359
43611
|
import { readFileSync as readFileSync13 } from "node:fs";
|
|
43360
43612
|
import { homedir as homedir9 } from "node:os";
|
|
43361
|
-
import { join as
|
|
43613
|
+
import { join as join21 } from "node:path";
|
|
43362
43614
|
var home = () => process.env.HOME || homedir9();
|
|
43363
43615
|
var readJson2 = (path2) => {
|
|
43364
43616
|
try {
|
|
@@ -43375,7 +43627,7 @@ var serversOf = (node) => {
|
|
|
43375
43627
|
var codexServers = () => {
|
|
43376
43628
|
let text;
|
|
43377
43629
|
try {
|
|
43378
|
-
text = readFileSync13(
|
|
43630
|
+
text = readFileSync13(join21(home(), ".codex", "config.toml"), "utf8");
|
|
43379
43631
|
} catch {
|
|
43380
43632
|
return [];
|
|
43381
43633
|
}
|
|
@@ -43388,12 +43640,12 @@ var codexServers = () => {
|
|
|
43388
43640
|
};
|
|
43389
43641
|
function mcpServerNames(repoDir) {
|
|
43390
43642
|
const names = [];
|
|
43391
|
-
const claudeJson = readJson2(
|
|
43643
|
+
const claudeJson = readJson2(join21(home(), ".claude.json"));
|
|
43392
43644
|
names.push(...serversOf(claudeJson));
|
|
43393
43645
|
if (repoDir) {
|
|
43394
43646
|
const projects = claudeJson?.projects;
|
|
43395
43647
|
if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
|
|
43396
|
-
names.push(...serversOf(readJson2(
|
|
43648
|
+
names.push(...serversOf(readJson2(join21(repoDir, ".mcp.json"))));
|
|
43397
43649
|
}
|
|
43398
43650
|
names.push(...codexServers());
|
|
43399
43651
|
return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
@@ -44846,17 +45098,17 @@ init_db();
|
|
|
44846
45098
|
|
|
44847
45099
|
// src/services/bootstrap.ts
|
|
44848
45100
|
import { createHash as createHash9, randomBytes as randomBytes9, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
44849
|
-
import { chmod, lstat as
|
|
44850
|
-
import { join as
|
|
45101
|
+
import { chmod, lstat as lstat3, mkdir as mkdir5, readFile as readFile4, unlink as unlink5, writeFile as writeFile4 } from "node:fs/promises";
|
|
45102
|
+
import { join as join22 } from "node:path";
|
|
44851
45103
|
var TTL_MS5 = 15 * 6e4;
|
|
44852
45104
|
var SETUP_TOKEN_FILE = "setup.token";
|
|
44853
45105
|
var BootstrapError = class extends Error {
|
|
44854
45106
|
code = "BOOTSTRAP_PROOF";
|
|
44855
45107
|
};
|
|
44856
|
-
var tokenPath = (home3) =>
|
|
45108
|
+
var tokenPath = (home3) => join22(home3, SETUP_TOKEN_FILE);
|
|
44857
45109
|
async function readStored(home3) {
|
|
44858
45110
|
try {
|
|
44859
|
-
const info = await
|
|
45111
|
+
const info = await lstat3(tokenPath(home3));
|
|
44860
45112
|
if (info.isSymbolicLink() || !info.isFile()) throw new BootstrapError("invalid setup proof");
|
|
44861
45113
|
const [token, expires] = (await readFile4(tokenPath(home3), "utf8")).trim().split("\n");
|
|
44862
45114
|
const expiresAt = Date.parse(expires ?? "");
|
|
@@ -44869,7 +45121,7 @@ async function readStored(home3) {
|
|
|
44869
45121
|
}
|
|
44870
45122
|
async function ensureSetupToken(home3, now = Date.now()) {
|
|
44871
45123
|
await mkdir5(home3, { recursive: true, mode: 448 });
|
|
44872
|
-
const homeInfo = await
|
|
45124
|
+
const homeInfo = await lstat3(home3);
|
|
44873
45125
|
if (homeInfo.isSymbolicLink() || !homeInfo.isDirectory()) throw new BootstrapError("invalid setup home");
|
|
44874
45126
|
await chmod(home3, 448);
|
|
44875
45127
|
try {
|
|
@@ -45063,7 +45315,12 @@ var IDE_SUBS = /* @__PURE__ */ new Set([
|
|
|
45063
45315
|
"remotes",
|
|
45064
45316
|
"compare",
|
|
45065
45317
|
"archive",
|
|
45066
|
-
"pr-url"
|
|
45318
|
+
"pr-url",
|
|
45319
|
+
// ADR-0121 · operasi berkas Explorer. `rw()` menurunkan read/write DARI METHOD, jadi
|
|
45320
|
+
// POST/PATCH/DELETE menuntut ide:write — capability yang sudah memberi hak menimpa isi
|
|
45321
|
+
// berkas apa pun lewat PUT /file (hindari kelas bug SPEC-405: prefix tanpa lihat method).
|
|
45322
|
+
"entry",
|
|
45323
|
+
"upload"
|
|
45067
45324
|
]);
|
|
45068
45325
|
function capabilityForRoute(method, path2) {
|
|
45069
45326
|
const read2 = method === "GET" || method === "HEAD";
|
|
@@ -45768,7 +46025,7 @@ init_tap();
|
|
|
45768
46025
|
// src/services/webhooks/emit.ts
|
|
45769
46026
|
init_src();
|
|
45770
46027
|
init_db();
|
|
45771
|
-
import { randomUUID as
|
|
46028
|
+
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
45772
46029
|
function skipped(def2, row) {
|
|
45773
46030
|
if (!def2.skipWhen || !row) return false;
|
|
45774
46031
|
return row[def2.skipWhen.field] === def2.skipWhen.equals;
|
|
@@ -45834,7 +46091,7 @@ async function emitWebhook(i) {
|
|
|
45834
46091
|
const targets = matchingEndpoints(type, projectId);
|
|
45835
46092
|
if (!targets.length) return;
|
|
45836
46093
|
const name2 = projectId ? (await prisma.project.findUnique({ where: { id: projectId }, select: { name: true } }))?.name ?? null : null;
|
|
45837
|
-
const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${
|
|
46094
|
+
const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID10().replace(/-/g, "")}`);
|
|
45838
46095
|
if (!env) return;
|
|
45839
46096
|
await enqueueEnvelope(env, targets);
|
|
45840
46097
|
} catch (e) {
|
|
@@ -46146,14 +46403,14 @@ init_session_sandbox();
|
|
|
46146
46403
|
init_src2();
|
|
46147
46404
|
|
|
46148
46405
|
// src/services/secure-home.ts
|
|
46149
|
-
import { chmod as chmod2, lstat as
|
|
46406
|
+
import { chmod as chmod2, lstat as lstat4, mkdir as mkdir6 } from "node:fs/promises";
|
|
46150
46407
|
import { isAbsolute as isAbsolute6, resolve as resolve18 } from "node:path";
|
|
46151
46408
|
var HomePermissionError = class extends Error {
|
|
46152
46409
|
code = "HOME_SYMLINK";
|
|
46153
46410
|
};
|
|
46154
46411
|
async function assertNoSymlink(path2, allowMissing) {
|
|
46155
46412
|
try {
|
|
46156
|
-
const info = await
|
|
46413
|
+
const info = await lstat4(resolve18(path2));
|
|
46157
46414
|
if (info.isSymbolicLink()) throw new HomePermissionError(`symlink ditolak: ${path2}`);
|
|
46158
46415
|
} catch (error) {
|
|
46159
46416
|
if (error.code === "ENOENT" && allowMissing) return;
|
|
@@ -46184,6 +46441,7 @@ async function secureHanomanHome(opts) {
|
|
|
46184
46441
|
|
|
46185
46442
|
// src/services/retention.ts
|
|
46186
46443
|
init_db();
|
|
46444
|
+
init_uploads();
|
|
46187
46445
|
var RETENTION_DAYS = {
|
|
46188
46446
|
tickets: 90,
|
|
46189
46447
|
newTickets: 180,
|
|
@@ -46298,6 +46556,7 @@ function startRetentionSweep() {
|
|
|
46298
46556
|
}
|
|
46299
46557
|
|
|
46300
46558
|
// src/server.ts
|
|
46559
|
+
init_uploads();
|
|
46301
46560
|
var app = buildApp();
|
|
46302
46561
|
var port = Number(process.env.PORT ?? 8787);
|
|
46303
46562
|
var host = process.env.HOST ?? "127.0.0.1";
|