hanoman 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +3 -0
- package/dist/server.js +498 -436
- package/package.json +1 -1
- package/web/assets/{index-B7aHqLDK.js → index-e01zHm_c.js} +1531 -1529
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -6119,6 +6119,9 @@ var init_api = __esm({
|
|
|
6119
6119
|
sessionReviewFile: (id, path2) => `${API}/terminal/sessions/${id}/review/${path2}`,
|
|
6120
6120
|
sessionIntegrate: (id) => `${API}/terminal/sessions/${id}/integrate`,
|
|
6121
6121
|
terminalWs: (id) => `${API}/terminal/sessions/${id}/ws`,
|
|
6122
|
+
// SPEC-816 · lampiran gambar sesi (multipart). Di bawah prefix /terminal supaya ikut capability
|
|
6123
|
+
// `sessions` yang sudah ada — POST menurunkan cabang tulisnya tanpa perubahan peta.
|
|
6124
|
+
terminalAttachments: (id) => `${API}/terminal/sessions/${id}/attachments`,
|
|
6122
6125
|
// SPEC-362 · ADR-0079 · riwayat sesi. Di bawah prefix /terminal supaya ikut capability
|
|
6123
6126
|
// `sessions` yang sudah ada (services/agent-capabilities.ts) tanpa menambah domain baru.
|
|
6124
6127
|
sessionHistory: (qs = "") => `${API}/terminal/history${qs}`,
|
|
@@ -9362,6 +9365,400 @@ var init_session_id = __esm({
|
|
|
9362
9365
|
}
|
|
9363
9366
|
});
|
|
9364
9367
|
|
|
9368
|
+
// src/services/secret-box.ts
|
|
9369
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
9370
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9371
|
+
import { dirname as dirname6, join as join4 } from "node:path";
|
|
9372
|
+
function isEncrypted(value) {
|
|
9373
|
+
return value.startsWith(ENC_PREFIX);
|
|
9374
|
+
}
|
|
9375
|
+
function encryptWithKey(plain, key) {
|
|
9376
|
+
const iv = randomBytes(IV_BYTES);
|
|
9377
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
9378
|
+
const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
9379
|
+
const tag2 = cipher.getAuthTag();
|
|
9380
|
+
return `${ENC_PREFIX}${iv.toString("base64url")}:${tag2.toString("base64url")}:${body.toString("base64url")}`;
|
|
9381
|
+
}
|
|
9382
|
+
function decryptWithKey(value, key) {
|
|
9383
|
+
if (!isEncrypted(value)) return value;
|
|
9384
|
+
const parts = value.slice(ENC_PREFIX.length).split(":");
|
|
9385
|
+
if (parts.length !== 3) return null;
|
|
9386
|
+
const [ivRaw, tagRaw, bodyRaw] = parts;
|
|
9387
|
+
try {
|
|
9388
|
+
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivRaw, "base64url"));
|
|
9389
|
+
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
9390
|
+
return Buffer.concat([decipher.update(Buffer.from(bodyRaw, "base64url")), decipher.final()]).toString("utf8");
|
|
9391
|
+
} catch {
|
|
9392
|
+
return null;
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
function fromEnv(raw2) {
|
|
9396
|
+
for (const enc2 of ["base64url", "base64", "hex"]) {
|
|
9397
|
+
const buf = Buffer.from(raw2, enc2);
|
|
9398
|
+
if (buf.length === KEY_BYTES) return buf;
|
|
9399
|
+
}
|
|
9400
|
+
return null;
|
|
9401
|
+
}
|
|
9402
|
+
function secretKeyPath() {
|
|
9403
|
+
return join4(resolveHome(), "secret.key");
|
|
9404
|
+
}
|
|
9405
|
+
function secretKey() {
|
|
9406
|
+
if (cached) return cached;
|
|
9407
|
+
const override = process.env.HANOMAN_SECRET_KEY?.trim();
|
|
9408
|
+
if (override) {
|
|
9409
|
+
const key2 = fromEnv(override);
|
|
9410
|
+
if (!key2) throw new Error(`HANOMAN_SECRET_KEY harus 32 byte (hex/base64), dapat ${override.length} karakter`);
|
|
9411
|
+
cached = key2;
|
|
9412
|
+
return key2;
|
|
9413
|
+
}
|
|
9414
|
+
const path2 = secretKeyPath();
|
|
9415
|
+
if (existsSync5(path2)) {
|
|
9416
|
+
const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
|
|
9417
|
+
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9418
|
+
cached = key2;
|
|
9419
|
+
return key2;
|
|
9420
|
+
}
|
|
9421
|
+
const key = randomBytes(KEY_BYTES);
|
|
9422
|
+
mkdirSync3(dirname6(path2), { recursive: true });
|
|
9423
|
+
writeFileSync(path2, key.toString("base64url"), { mode: 384 });
|
|
9424
|
+
chmodSync2(path2, 384);
|
|
9425
|
+
cached = key;
|
|
9426
|
+
return key;
|
|
9427
|
+
}
|
|
9428
|
+
function encryptSecret(plain) {
|
|
9429
|
+
return encryptWithKey(plain, secretKey());
|
|
9430
|
+
}
|
|
9431
|
+
function decryptSecret(value) {
|
|
9432
|
+
return decryptWithKey(value, secretKey());
|
|
9433
|
+
}
|
|
9434
|
+
var ENC_PREFIX, ALGO, IV_BYTES, KEY_BYTES, cached;
|
|
9435
|
+
var init_secret_box = __esm({
|
|
9436
|
+
"src/services/secret-box.ts"() {
|
|
9437
|
+
"use strict";
|
|
9438
|
+
init_src2();
|
|
9439
|
+
ENC_PREFIX = "enc:v1:";
|
|
9440
|
+
ALGO = "aes-256-gcm";
|
|
9441
|
+
IV_BYTES = 12;
|
|
9442
|
+
KEY_BYTES = 32;
|
|
9443
|
+
cached = null;
|
|
9444
|
+
}
|
|
9445
|
+
});
|
|
9446
|
+
|
|
9447
|
+
// src/config.ts
|
|
9448
|
+
var config_exports = {};
|
|
9449
|
+
__export(config_exports, {
|
|
9450
|
+
clearConfig: () => clearConfig,
|
|
9451
|
+
effectiveBool: () => effectiveBool,
|
|
9452
|
+
effectiveInt: () => effectiveInt,
|
|
9453
|
+
effectiveStr: () => effectiveStr,
|
|
9454
|
+
loadConfig: () => loadConfig,
|
|
9455
|
+
rawDbValue: () => rawDbValue,
|
|
9456
|
+
setConfig: () => setConfig,
|
|
9457
|
+
setConfigsAtomic: () => setConfigsAtomic,
|
|
9458
|
+
sourceOf: () => sourceOf
|
|
9459
|
+
});
|
|
9460
|
+
async function loadConfig() {
|
|
9461
|
+
const rows = await prisma.runtimeConfig.findMany();
|
|
9462
|
+
const next = /* @__PURE__ */ new Map();
|
|
9463
|
+
for (const r of rows) {
|
|
9464
|
+
if (!isSecret(r.key)) {
|
|
9465
|
+
next.set(r.key, r.value);
|
|
9466
|
+
continue;
|
|
9467
|
+
}
|
|
9468
|
+
const plain = decryptSecret(r.value);
|
|
9469
|
+
if (plain === null) {
|
|
9470
|
+
console.error(`config: nilai '${r.key}' tak bisa didekripsi \u2014 diabaikan`);
|
|
9471
|
+
continue;
|
|
9472
|
+
}
|
|
9473
|
+
next.set(r.key, plain);
|
|
9474
|
+
}
|
|
9475
|
+
cache = next;
|
|
9476
|
+
}
|
|
9477
|
+
function rawDbValue(key) {
|
|
9478
|
+
return cache.get(key);
|
|
9479
|
+
}
|
|
9480
|
+
function effectiveStr(key) {
|
|
9481
|
+
return cache.get(key) ?? process.env[key] ?? configEntry(key)?.default;
|
|
9482
|
+
}
|
|
9483
|
+
function effectiveInt(key) {
|
|
9484
|
+
const v = effectiveStr(key);
|
|
9485
|
+
return v === void 0 ? void 0 : Number(v);
|
|
9486
|
+
}
|
|
9487
|
+
function effectiveBool(key) {
|
|
9488
|
+
const v = effectiveStr(key);
|
|
9489
|
+
return v === "1" || v === "true";
|
|
9490
|
+
}
|
|
9491
|
+
function sourceOf(key) {
|
|
9492
|
+
if (cache.has(key)) return "db";
|
|
9493
|
+
if (process.env[key] !== void 0) return "env";
|
|
9494
|
+
return "default";
|
|
9495
|
+
}
|
|
9496
|
+
async function setConfig(key, value) {
|
|
9497
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9498
|
+
await prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9499
|
+
cache.set(key, value);
|
|
9500
|
+
}
|
|
9501
|
+
async function setConfigsAtomic(values) {
|
|
9502
|
+
await prisma.$transaction(Object.entries(values).map(([key, value]) => {
|
|
9503
|
+
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9504
|
+
return prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9505
|
+
}));
|
|
9506
|
+
for (const [key, value] of Object.entries(values)) cache.set(key, value);
|
|
9507
|
+
}
|
|
9508
|
+
async function clearConfig(key) {
|
|
9509
|
+
await prisma.runtimeConfig.deleteMany({ where: { key } });
|
|
9510
|
+
cache.delete(key);
|
|
9511
|
+
}
|
|
9512
|
+
var cache, isSecret;
|
|
9513
|
+
var init_config2 = __esm({
|
|
9514
|
+
"src/config.ts"() {
|
|
9515
|
+
"use strict";
|
|
9516
|
+
init_db();
|
|
9517
|
+
init_src();
|
|
9518
|
+
init_secret_box();
|
|
9519
|
+
cache = /* @__PURE__ */ new Map();
|
|
9520
|
+
isSecret = (key) => configEntry(key)?.kind === "secret";
|
|
9521
|
+
}
|
|
9522
|
+
});
|
|
9523
|
+
|
|
9524
|
+
// src/services/webhooks/ssrf.ts
|
|
9525
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9526
|
+
import { isIPv4 } from "node:net";
|
|
9527
|
+
function validateWebhookUrl(raw2) {
|
|
9528
|
+
let url2;
|
|
9529
|
+
try {
|
|
9530
|
+
url2 = new URL(raw2.trim());
|
|
9531
|
+
} catch {
|
|
9532
|
+
return { ok: false, error: "URL tak valid" };
|
|
9533
|
+
}
|
|
9534
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:")
|
|
9535
|
+
return { ok: false, error: "hanya http atau https" };
|
|
9536
|
+
if (url2.username || url2.password)
|
|
9537
|
+
return { ok: false, error: "kredensial di URL tak diizinkan" };
|
|
9538
|
+
if (!url2.hostname) return { ok: false, error: "hostname kosong" };
|
|
9539
|
+
return { ok: true, url: url2 };
|
|
9540
|
+
}
|
|
9541
|
+
function blockedV4(ip) {
|
|
9542
|
+
const p = ip.split(".").map(Number);
|
|
9543
|
+
if (p.length !== 4 || p.some((n2) => !Number.isInteger(n2) || n2 < 0 || n2 > 255)) return true;
|
|
9544
|
+
const [a, b] = p;
|
|
9545
|
+
if (a === 0 || a === 127) return true;
|
|
9546
|
+
if (a === 10) return true;
|
|
9547
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
9548
|
+
if (a === 192 && b === 168) return true;
|
|
9549
|
+
if (a === 169 && b === 254) return true;
|
|
9550
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
9551
|
+
if (a === 192 && b === 0) return true;
|
|
9552
|
+
if (a >= 224) return true;
|
|
9553
|
+
return false;
|
|
9554
|
+
}
|
|
9555
|
+
function isBlockedAddress(ip) {
|
|
9556
|
+
const v = ip.trim().toLowerCase();
|
|
9557
|
+
if (!v) return true;
|
|
9558
|
+
const mapped = v.startsWith("::ffff:") ? v.slice(7) : v;
|
|
9559
|
+
if (isIPv4(mapped)) return blockedV4(mapped);
|
|
9560
|
+
if (!v.includes(":")) return true;
|
|
9561
|
+
if (v === "::" || v === "::1") return true;
|
|
9562
|
+
if (v.startsWith("fe80") || v.startsWith("fec0")) return true;
|
|
9563
|
+
if (/^f[cd]/.test(v)) return true;
|
|
9564
|
+
if (v.startsWith("ff")) return true;
|
|
9565
|
+
return false;
|
|
9566
|
+
}
|
|
9567
|
+
function checkUrlShape(raw2, allowPrivate) {
|
|
9568
|
+
const parsed = validateWebhookUrl(raw2);
|
|
9569
|
+
if (!parsed.ok) return parsed;
|
|
9570
|
+
if (allowPrivate) return parsed;
|
|
9571
|
+
const host2 = parsed.url.hostname.replace(/^\[|\]$/g, "");
|
|
9572
|
+
const literal = isIPv4(host2) || host2.includes(":");
|
|
9573
|
+
if (literal && isBlockedAddress(host2))
|
|
9574
|
+
return {
|
|
9575
|
+
ok: false,
|
|
9576
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9577
|
+
};
|
|
9578
|
+
if (host2 === "localhost" || host2.endsWith(".localhost"))
|
|
9579
|
+
return {
|
|
9580
|
+
ok: false,
|
|
9581
|
+
error: `alamat internal ditolak (${host2}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9582
|
+
};
|
|
9583
|
+
return parsed;
|
|
9584
|
+
}
|
|
9585
|
+
async function checkDestination(url2, allowPrivate, lookup = defaultLookup) {
|
|
9586
|
+
if (allowPrivate) return { ok: true };
|
|
9587
|
+
const host2 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
9588
|
+
let addrs;
|
|
9589
|
+
try {
|
|
9590
|
+
addrs = await lookup(host2);
|
|
9591
|
+
} catch (e) {
|
|
9592
|
+
return { ok: false, error: `DNS gagal: ${e.message}` };
|
|
9593
|
+
}
|
|
9594
|
+
if (!addrs.length) return { ok: false, error: "DNS tak mengembalikan alamat" };
|
|
9595
|
+
for (const a of addrs)
|
|
9596
|
+
if (isBlockedAddress(a.address))
|
|
9597
|
+
return {
|
|
9598
|
+
ok: false,
|
|
9599
|
+
error: `alamat internal ditolak (${a.address}) \u2014 nyalakan "izinkan alamat internal" bila memang disengaja`
|
|
9600
|
+
};
|
|
9601
|
+
return { ok: true };
|
|
9602
|
+
}
|
|
9603
|
+
var defaultLookup;
|
|
9604
|
+
var init_ssrf = __esm({
|
|
9605
|
+
"src/services/webhooks/ssrf.ts"() {
|
|
9606
|
+
"use strict";
|
|
9607
|
+
defaultLookup = (host2) => dnsLookup(host2, { all: true });
|
|
9608
|
+
}
|
|
9609
|
+
});
|
|
9610
|
+
|
|
9611
|
+
// src/services/safe-outbound-request.ts
|
|
9612
|
+
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
9613
|
+
import { request as httpRequest } from "node:http";
|
|
9614
|
+
import { request as httpsRequest } from "node:https";
|
|
9615
|
+
async function pinnedRequest(input) {
|
|
9616
|
+
return new Promise((resolve19, reject2) => {
|
|
9617
|
+
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
9618
|
+
const req = transport({
|
|
9619
|
+
protocol: input.url.protocol,
|
|
9620
|
+
hostname: input.url.hostname,
|
|
9621
|
+
port: input.url.port || void 0,
|
|
9622
|
+
method: input.method,
|
|
9623
|
+
path: `${input.url.pathname}${input.url.search}`,
|
|
9624
|
+
headers: input.headers,
|
|
9625
|
+
servername: input.url.hostname,
|
|
9626
|
+
// Node >= 20 menyalakan `autoSelectFamily` secara default: socket memanggil `lookup` dengan
|
|
9627
|
+
// `all: true` dan membaca `addresses[0].address` dari hasilnya. Menjawab dalam bentuk skalar
|
|
9628
|
+
// di situ memberi `undefined` → ERR_INVALID_IP_ADDRESS sebelum satu paket pun keluar, dan
|
|
9629
|
+
// pemanggil (sync tick, webhook) menelannya sebagai "offline". Kedua bentuk dijawab supaya
|
|
9630
|
+
// pinning tetap benar apa pun setelan family-nya.
|
|
9631
|
+
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
9632
|
+
timeout: input.connectMs
|
|
9633
|
+
}, (response) => {
|
|
9634
|
+
const chunks = [];
|
|
9635
|
+
let bytes = 0;
|
|
9636
|
+
response.on("data", (chunk) => {
|
|
9637
|
+
bytes += chunk.length;
|
|
9638
|
+
if (bytes > input.maxResponseBytes) {
|
|
9639
|
+
response.destroy(new Error("outbound response terlalu besar"));
|
|
9640
|
+
return;
|
|
9641
|
+
}
|
|
9642
|
+
chunks.push(chunk);
|
|
9643
|
+
});
|
|
9644
|
+
response.on("end", () => resolve19({
|
|
9645
|
+
status: response.statusCode ?? 0,
|
|
9646
|
+
headers: response.headers,
|
|
9647
|
+
body: Buffer.concat(chunks)
|
|
9648
|
+
}));
|
|
9649
|
+
});
|
|
9650
|
+
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
9651
|
+
timer9.unref?.();
|
|
9652
|
+
req.once("close", () => clearTimeout(timer9));
|
|
9653
|
+
req.once("error", reject2);
|
|
9654
|
+
if (input.body) req.write(input.body);
|
|
9655
|
+
req.end();
|
|
9656
|
+
});
|
|
9657
|
+
}
|
|
9658
|
+
async function safeRequest(options2, deps = {}) {
|
|
9659
|
+
if (options2.url.protocol !== "http:" && options2.url.protocol !== "https:") throw new Error("outbound scheme ditolak");
|
|
9660
|
+
if (options2.url.username || options2.url.password) throw new Error("outbound credential URL ditolak");
|
|
9661
|
+
const addresses = await (deps.lookupAll ?? defaultLookup2)(options2.url.hostname);
|
|
9662
|
+
if (!addresses.length) throw new Error("DNS tak mengembalikan alamat");
|
|
9663
|
+
if (!options2.allowPrivate) {
|
|
9664
|
+
const blocked = addresses.find((row) => isBlockedAddress(row.address));
|
|
9665
|
+
if (blocked) throw new Error(`alamat internal ditolak (${blocked.address})`);
|
|
9666
|
+
}
|
|
9667
|
+
const selected = addresses[0];
|
|
9668
|
+
return (deps.request ?? pinnedRequest)({ ...options2, address: selected.address, family: selected.family });
|
|
9669
|
+
}
|
|
9670
|
+
var defaultLookup2;
|
|
9671
|
+
var init_safe_outbound_request = __esm({
|
|
9672
|
+
"src/services/safe-outbound-request.ts"() {
|
|
9673
|
+
"use strict";
|
|
9674
|
+
init_ssrf();
|
|
9675
|
+
defaultLookup2 = async (host2) => (await dnsLookup2(host2, { all: true })).map((row) => ({ address: row.address, family: row.family }));
|
|
9676
|
+
}
|
|
9677
|
+
});
|
|
9678
|
+
|
|
9679
|
+
// src/services/uploads.ts
|
|
9680
|
+
import { randomUUID } from "node:crypto";
|
|
9681
|
+
import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
|
|
9682
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
9683
|
+
function extFor(mimeType) {
|
|
9684
|
+
return EXT[mimeType] ?? ".bin";
|
|
9685
|
+
}
|
|
9686
|
+
function uploadDir() {
|
|
9687
|
+
return resolve8(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join5(resolveHome(), "uploads"));
|
|
9688
|
+
}
|
|
9689
|
+
function sessionUploadDir(sessionId2) {
|
|
9690
|
+
if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
|
|
9691
|
+
return join5(uploadDir(), "terminal", sessionId2);
|
|
9692
|
+
}
|
|
9693
|
+
async function saveSessionUpload(sessionId2, buf, mimeType) {
|
|
9694
|
+
const dir2 = sessionUploadDir(sessionId2);
|
|
9695
|
+
await mkdir(dir2, { recursive: true, mode: 448 });
|
|
9696
|
+
const path2 = join5(dir2, `${randomUUID()}${extFor(mimeType)}`);
|
|
9697
|
+
await writeFile(path2, buf, { mode: 384 });
|
|
9698
|
+
return { path: path2, size: buf.length };
|
|
9699
|
+
}
|
|
9700
|
+
async function dropSessionUploads(sessionId2) {
|
|
9701
|
+
let dir2;
|
|
9702
|
+
try {
|
|
9703
|
+
dir2 = sessionUploadDir(sessionId2);
|
|
9704
|
+
} catch {
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
await rm(dir2, { recursive: true, force: true }).catch(() => {
|
|
9708
|
+
});
|
|
9709
|
+
}
|
|
9710
|
+
async function readUpload(storageKey) {
|
|
9711
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9712
|
+
return readFile2(join5(uploadDir(), safe));
|
|
9713
|
+
}
|
|
9714
|
+
async function readUploadOrFetch(storageKey) {
|
|
9715
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9716
|
+
const target2 = join5(uploadDir(), safe);
|
|
9717
|
+
try {
|
|
9718
|
+
return await readFile2(target2);
|
|
9719
|
+
} catch {
|
|
9720
|
+
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
9721
|
+
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
9722
|
+
if (!base2 || !token) throw new Error(`lampiran ${safe} tak ada lokal & bukan client sync`);
|
|
9723
|
+
const url2 = new URL(`/api/sync/attachments/${encodeURIComponent(safe)}`, `${base2.replace(/\/$/, "")}/`);
|
|
9724
|
+
const loopback = ["localhost", "127.0.0.1", "::1"].includes(url2.hostname);
|
|
9725
|
+
const res = await safeRequest({
|
|
9726
|
+
url: url2,
|
|
9727
|
+
method: "GET",
|
|
9728
|
+
headers: { authorization: `Bearer ${token}` },
|
|
9729
|
+
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
9730
|
+
connectMs: 5e3,
|
|
9731
|
+
totalMs: 15e3,
|
|
9732
|
+
maxResponseBytes: 5 * 1024 * 1024
|
|
9733
|
+
});
|
|
9734
|
+
if (res.status < 200 || res.status >= 300) throw new Error(`fetch lampiran hub gagal: ${res.status}`);
|
|
9735
|
+
const buf = res.body;
|
|
9736
|
+
await mkdir(uploadDir(), { recursive: true, mode: 448 });
|
|
9737
|
+
await writeFile(target2, buf, { mode: 384 });
|
|
9738
|
+
return buf;
|
|
9739
|
+
}
|
|
9740
|
+
}
|
|
9741
|
+
async function deleteUpload(storageKey) {
|
|
9742
|
+
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9743
|
+
await unlink(join5(uploadDir(), safe)).catch(() => {
|
|
9744
|
+
});
|
|
9745
|
+
}
|
|
9746
|
+
var EXT, SESSION_ID;
|
|
9747
|
+
var init_uploads = __esm({
|
|
9748
|
+
"src/services/uploads.ts"() {
|
|
9749
|
+
"use strict";
|
|
9750
|
+
init_config2();
|
|
9751
|
+
init_safe_outbound_request();
|
|
9752
|
+
init_src2();
|
|
9753
|
+
EXT = {
|
|
9754
|
+
"image/png": ".png",
|
|
9755
|
+
"image/jpeg": ".jpg",
|
|
9756
|
+
"image/webp": ".webp"
|
|
9757
|
+
};
|
|
9758
|
+
SESSION_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9759
|
+
}
|
|
9760
|
+
});
|
|
9761
|
+
|
|
9365
9762
|
// src/services/tui-dialog.ts
|
|
9366
9763
|
function readChoiceDialog(paneText2) {
|
|
9367
9764
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
@@ -9594,162 +9991,6 @@ var init_tui_dialog = __esm({
|
|
|
9594
9991
|
}
|
|
9595
9992
|
});
|
|
9596
9993
|
|
|
9597
|
-
// src/services/secret-box.ts
|
|
9598
|
-
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
9599
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9600
|
-
import { dirname as dirname6, join as join4 } from "node:path";
|
|
9601
|
-
function isEncrypted(value) {
|
|
9602
|
-
return value.startsWith(ENC_PREFIX);
|
|
9603
|
-
}
|
|
9604
|
-
function encryptWithKey(plain, key) {
|
|
9605
|
-
const iv = randomBytes(IV_BYTES);
|
|
9606
|
-
const cipher = createCipheriv(ALGO, key, iv);
|
|
9607
|
-
const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
9608
|
-
const tag2 = cipher.getAuthTag();
|
|
9609
|
-
return `${ENC_PREFIX}${iv.toString("base64url")}:${tag2.toString("base64url")}:${body.toString("base64url")}`;
|
|
9610
|
-
}
|
|
9611
|
-
function decryptWithKey(value, key) {
|
|
9612
|
-
if (!isEncrypted(value)) return value;
|
|
9613
|
-
const parts = value.slice(ENC_PREFIX.length).split(":");
|
|
9614
|
-
if (parts.length !== 3) return null;
|
|
9615
|
-
const [ivRaw, tagRaw, bodyRaw] = parts;
|
|
9616
|
-
try {
|
|
9617
|
-
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivRaw, "base64url"));
|
|
9618
|
-
decipher.setAuthTag(Buffer.from(tagRaw, "base64url"));
|
|
9619
|
-
return Buffer.concat([decipher.update(Buffer.from(bodyRaw, "base64url")), decipher.final()]).toString("utf8");
|
|
9620
|
-
} catch {
|
|
9621
|
-
return null;
|
|
9622
|
-
}
|
|
9623
|
-
}
|
|
9624
|
-
function fromEnv(raw2) {
|
|
9625
|
-
for (const enc2 of ["base64url", "base64", "hex"]) {
|
|
9626
|
-
const buf = Buffer.from(raw2, enc2);
|
|
9627
|
-
if (buf.length === KEY_BYTES) return buf;
|
|
9628
|
-
}
|
|
9629
|
-
return null;
|
|
9630
|
-
}
|
|
9631
|
-
function secretKeyPath() {
|
|
9632
|
-
return join4(resolveHome(), "secret.key");
|
|
9633
|
-
}
|
|
9634
|
-
function secretKey() {
|
|
9635
|
-
if (cached) return cached;
|
|
9636
|
-
const override = process.env.HANOMAN_SECRET_KEY?.trim();
|
|
9637
|
-
if (override) {
|
|
9638
|
-
const key2 = fromEnv(override);
|
|
9639
|
-
if (!key2) throw new Error(`HANOMAN_SECRET_KEY harus 32 byte (hex/base64), dapat ${override.length} karakter`);
|
|
9640
|
-
cached = key2;
|
|
9641
|
-
return key2;
|
|
9642
|
-
}
|
|
9643
|
-
const path2 = secretKeyPath();
|
|
9644
|
-
if (existsSync5(path2)) {
|
|
9645
|
-
const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
|
|
9646
|
-
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9647
|
-
cached = key2;
|
|
9648
|
-
return key2;
|
|
9649
|
-
}
|
|
9650
|
-
const key = randomBytes(KEY_BYTES);
|
|
9651
|
-
mkdirSync3(dirname6(path2), { recursive: true });
|
|
9652
|
-
writeFileSync(path2, key.toString("base64url"), { mode: 384 });
|
|
9653
|
-
chmodSync2(path2, 384);
|
|
9654
|
-
cached = key;
|
|
9655
|
-
return key;
|
|
9656
|
-
}
|
|
9657
|
-
function encryptSecret(plain) {
|
|
9658
|
-
return encryptWithKey(plain, secretKey());
|
|
9659
|
-
}
|
|
9660
|
-
function decryptSecret(value) {
|
|
9661
|
-
return decryptWithKey(value, secretKey());
|
|
9662
|
-
}
|
|
9663
|
-
var ENC_PREFIX, ALGO, IV_BYTES, KEY_BYTES, cached;
|
|
9664
|
-
var init_secret_box = __esm({
|
|
9665
|
-
"src/services/secret-box.ts"() {
|
|
9666
|
-
"use strict";
|
|
9667
|
-
init_src2();
|
|
9668
|
-
ENC_PREFIX = "enc:v1:";
|
|
9669
|
-
ALGO = "aes-256-gcm";
|
|
9670
|
-
IV_BYTES = 12;
|
|
9671
|
-
KEY_BYTES = 32;
|
|
9672
|
-
cached = null;
|
|
9673
|
-
}
|
|
9674
|
-
});
|
|
9675
|
-
|
|
9676
|
-
// src/config.ts
|
|
9677
|
-
var config_exports = {};
|
|
9678
|
-
__export(config_exports, {
|
|
9679
|
-
clearConfig: () => clearConfig,
|
|
9680
|
-
effectiveBool: () => effectiveBool,
|
|
9681
|
-
effectiveInt: () => effectiveInt,
|
|
9682
|
-
effectiveStr: () => effectiveStr,
|
|
9683
|
-
loadConfig: () => loadConfig,
|
|
9684
|
-
rawDbValue: () => rawDbValue,
|
|
9685
|
-
setConfig: () => setConfig,
|
|
9686
|
-
setConfigsAtomic: () => setConfigsAtomic,
|
|
9687
|
-
sourceOf: () => sourceOf
|
|
9688
|
-
});
|
|
9689
|
-
async function loadConfig() {
|
|
9690
|
-
const rows = await prisma.runtimeConfig.findMany();
|
|
9691
|
-
const next = /* @__PURE__ */ new Map();
|
|
9692
|
-
for (const r of rows) {
|
|
9693
|
-
if (!isSecret(r.key)) {
|
|
9694
|
-
next.set(r.key, r.value);
|
|
9695
|
-
continue;
|
|
9696
|
-
}
|
|
9697
|
-
const plain = decryptSecret(r.value);
|
|
9698
|
-
if (plain === null) {
|
|
9699
|
-
console.error(`config: nilai '${r.key}' tak bisa didekripsi \u2014 diabaikan`);
|
|
9700
|
-
continue;
|
|
9701
|
-
}
|
|
9702
|
-
next.set(r.key, plain);
|
|
9703
|
-
}
|
|
9704
|
-
cache = next;
|
|
9705
|
-
}
|
|
9706
|
-
function rawDbValue(key) {
|
|
9707
|
-
return cache.get(key);
|
|
9708
|
-
}
|
|
9709
|
-
function effectiveStr(key) {
|
|
9710
|
-
return cache.get(key) ?? process.env[key] ?? configEntry(key)?.default;
|
|
9711
|
-
}
|
|
9712
|
-
function effectiveInt(key) {
|
|
9713
|
-
const v = effectiveStr(key);
|
|
9714
|
-
return v === void 0 ? void 0 : Number(v);
|
|
9715
|
-
}
|
|
9716
|
-
function effectiveBool(key) {
|
|
9717
|
-
const v = effectiveStr(key);
|
|
9718
|
-
return v === "1" || v === "true";
|
|
9719
|
-
}
|
|
9720
|
-
function sourceOf(key) {
|
|
9721
|
-
if (cache.has(key)) return "db";
|
|
9722
|
-
if (process.env[key] !== void 0) return "env";
|
|
9723
|
-
return "default";
|
|
9724
|
-
}
|
|
9725
|
-
async function setConfig(key, value) {
|
|
9726
|
-
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9727
|
-
await prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9728
|
-
cache.set(key, value);
|
|
9729
|
-
}
|
|
9730
|
-
async function setConfigsAtomic(values) {
|
|
9731
|
-
await prisma.$transaction(Object.entries(values).map(([key, value]) => {
|
|
9732
|
-
const stored = isSecret(key) ? encryptSecret(value) : value;
|
|
9733
|
-
return prisma.runtimeConfig.upsert({ where: { key }, create: { key, value: stored }, update: { value: stored } });
|
|
9734
|
-
}));
|
|
9735
|
-
for (const [key, value] of Object.entries(values)) cache.set(key, value);
|
|
9736
|
-
}
|
|
9737
|
-
async function clearConfig(key) {
|
|
9738
|
-
await prisma.runtimeConfig.deleteMany({ where: { key } });
|
|
9739
|
-
cache.delete(key);
|
|
9740
|
-
}
|
|
9741
|
-
var cache, isSecret;
|
|
9742
|
-
var init_config2 = __esm({
|
|
9743
|
-
"src/config.ts"() {
|
|
9744
|
-
"use strict";
|
|
9745
|
-
init_db();
|
|
9746
|
-
init_src();
|
|
9747
|
-
init_secret_box();
|
|
9748
|
-
cache = /* @__PURE__ */ new Map();
|
|
9749
|
-
isSecret = (key) => configEntry(key)?.kind === "secret";
|
|
9750
|
-
}
|
|
9751
|
-
});
|
|
9752
|
-
|
|
9753
9994
|
// src/services/session-sandbox.ts
|
|
9754
9995
|
function assertRuntimeBoundary(env, runtime) {
|
|
9755
9996
|
if (env.NODE_ENV !== "production") return;
|
|
@@ -9836,7 +10077,7 @@ var init_session_sandbox = __esm({
|
|
|
9836
10077
|
import { spawn } from "node-pty";
|
|
9837
10078
|
import { execFileSync } from "node:child_process";
|
|
9838
10079
|
import { createRequire } from "node:module";
|
|
9839
|
-
import { randomUUID } from "node:crypto";
|
|
10080
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9840
10081
|
import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9841
10082
|
import { dirname as dirname7 } from "node:path";
|
|
9842
10083
|
import { tmpdir } from "node:os";
|
|
@@ -10283,6 +10524,8 @@ function killSession(id) {
|
|
|
10283
10524
|
drop(id);
|
|
10284
10525
|
tmux("kill-session", "-t", name(id));
|
|
10285
10526
|
emitDeath({ sessionId: id, exitCode: p.exited ? p.code : null, transcript });
|
|
10527
|
+
void dropSessionUploads(id).catch(() => {
|
|
10528
|
+
});
|
|
10286
10529
|
return true;
|
|
10287
10530
|
}
|
|
10288
10531
|
function detachAll() {
|
|
@@ -10313,6 +10556,7 @@ var init_pty = __esm({
|
|
|
10313
10556
|
init_src();
|
|
10314
10557
|
init_session_phases();
|
|
10315
10558
|
init_session_id();
|
|
10559
|
+
init_uploads();
|
|
10316
10560
|
init_tui_dialog();
|
|
10317
10561
|
init_config2();
|
|
10318
10562
|
init_session_sandbox();
|
|
@@ -10348,7 +10592,7 @@ var init_pty = __esm({
|
|
|
10348
10592
|
}
|
|
10349
10593
|
};
|
|
10350
10594
|
sq = (s2) => `'${s2.split("'").join("'\\''")}'`;
|
|
10351
|
-
idFor = (specId) => specId ? sessionIdForSpec(specId) :
|
|
10595
|
+
idFor = (specId) => specId ? sessionIdForSpec(specId) : randomUUID2().slice(0, 8);
|
|
10352
10596
|
FMT = [
|
|
10353
10597
|
"#{session_name}",
|
|
10354
10598
|
"#{@hanoman_project}",
|
|
@@ -12584,161 +12828,6 @@ var init_conflicts = __esm({
|
|
|
12584
12828
|
}
|
|
12585
12829
|
});
|
|
12586
12830
|
|
|
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
12831
|
// ../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
|
|
12743
12832
|
var require_constants = __commonJS({
|
|
12744
12833
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -24824,13 +24913,13 @@ init_src();
|
|
|
24824
24913
|
import { execFile } from "node:child_process";
|
|
24825
24914
|
import { promisify } from "node:util";
|
|
24826
24915
|
import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
|
|
24827
|
-
import { resolve as
|
|
24916
|
+
import { resolve as resolve10 } from "node:path";
|
|
24828
24917
|
|
|
24829
24918
|
// src/services/safe-repo-path.ts
|
|
24830
24919
|
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
|
|
24920
|
+
import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
|
|
24921
|
+
import { isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve9, sep as sep2 } from "node:path";
|
|
24922
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24834
24923
|
import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
24835
24924
|
var PathContainmentError = class extends Error {
|
|
24836
24925
|
code = "PATH_CONTAINMENT";
|
|
@@ -24859,9 +24948,9 @@ async function ensureRepoParents(root, rel) {
|
|
|
24859
24948
|
const parts = components(rel).slice(0, -1);
|
|
24860
24949
|
let current = base2;
|
|
24861
24950
|
for (const part of parts) {
|
|
24862
|
-
current =
|
|
24951
|
+
current = join6(current, part);
|
|
24863
24952
|
try {
|
|
24864
|
-
await
|
|
24953
|
+
await mkdir2(current, { mode: 448 });
|
|
24865
24954
|
} catch (error) {
|
|
24866
24955
|
if (error.code !== "EEXIST") throw error;
|
|
24867
24956
|
}
|
|
@@ -24876,7 +24965,7 @@ function ensureRepoParentsSync(root, rel) {
|
|
|
24876
24965
|
const parts = components(rel).slice(0, -1);
|
|
24877
24966
|
let current = base2;
|
|
24878
24967
|
for (const part of parts) {
|
|
24879
|
-
current =
|
|
24968
|
+
current = join6(current, part);
|
|
24880
24969
|
try {
|
|
24881
24970
|
mkdirSync5(current, { mode: 448 });
|
|
24882
24971
|
} catch (error) {
|
|
@@ -24892,7 +24981,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24892
24981
|
const parts = components(rel);
|
|
24893
24982
|
let current = base2;
|
|
24894
24983
|
for (let i = 0; i < parts.length; i++) {
|
|
24895
|
-
current =
|
|
24984
|
+
current = join6(current, parts[i]);
|
|
24896
24985
|
const stat2 = await lstat(current).catch((error) => {
|
|
24897
24986
|
if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
|
|
24898
24987
|
return denied("komponen tidak ada");
|
|
@@ -24901,8 +24990,8 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
24901
24990
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
24902
24991
|
if (i < parts.length - 1 && !stat2.isDirectory()) denied("parent bukan direktori");
|
|
24903
24992
|
}
|
|
24904
|
-
if (!beneath(base2,
|
|
24905
|
-
const parent =
|
|
24993
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24994
|
+
const parent = resolve9(current, "..");
|
|
24906
24995
|
const parentReal = await realpath(parent).catch(() => denied("parent tidak ada"));
|
|
24907
24996
|
if (!beneath(base2, parentReal)) denied("parent keluar root");
|
|
24908
24997
|
return { root: base2, path: current, parent: parentReal };
|
|
@@ -24912,7 +25001,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24912
25001
|
const parts = components(rel);
|
|
24913
25002
|
let current = base2;
|
|
24914
25003
|
for (let i = 0; i < parts.length; i++) {
|
|
24915
|
-
current =
|
|
25004
|
+
current = join6(current, parts[i]);
|
|
24916
25005
|
try {
|
|
24917
25006
|
const stat2 = lstatSync(current);
|
|
24918
25007
|
if (stat2.isSymbolicLink()) denied("symlink");
|
|
@@ -24923,7 +25012,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
24923
25012
|
throw error;
|
|
24924
25013
|
}
|
|
24925
25014
|
}
|
|
24926
|
-
if (!beneath(base2,
|
|
25015
|
+
if (!beneath(base2, resolve9(current))) denied("keluar root");
|
|
24927
25016
|
return current;
|
|
24928
25017
|
}
|
|
24929
25018
|
function readRepoFileSync(root, rel) {
|
|
@@ -24939,10 +25028,10 @@ function readRepoFileSync(root, rel) {
|
|
|
24939
25028
|
function writeRepoFileAtomicSync(root, rel, data) {
|
|
24940
25029
|
ensureRepoParentsSync(root, rel);
|
|
24941
25030
|
const path2 = assertSafeRepoPathSync(root, rel, true);
|
|
24942
|
-
const parent =
|
|
25031
|
+
const parent = resolve9(path2, "..");
|
|
24943
25032
|
const parentRel = relative(realpathSync2(root), parent);
|
|
24944
25033
|
if (parentRel) assertSafeRepoPathSync(root, parentRel);
|
|
24945
|
-
const temp =
|
|
25034
|
+
const temp = join6(parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24946
25035
|
const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24947
25036
|
try {
|
|
24948
25037
|
writeFileSync3(fd, data);
|
|
@@ -24986,10 +25075,10 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
24986
25075
|
return denied("final component tak terbaca");
|
|
24987
25076
|
});
|
|
24988
25077
|
if (current?.isSymbolicLink() || current && !current.isFile()) denied("target bukan file regular");
|
|
24989
|
-
await
|
|
25078
|
+
await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
|
|
24990
25079
|
if (error.code !== "EEXIST") throw error;
|
|
24991
25080
|
});
|
|
24992
|
-
const temp =
|
|
25081
|
+
const temp = join6(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
24993
25082
|
const handle = await open2(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
24994
25083
|
try {
|
|
24995
25084
|
await handle.writeFile(data);
|
|
@@ -25004,7 +25093,7 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
25004
25093
|
if (finalAgain?.isSymbolicLink() || finalAgain && !finalAgain.isFile()) denied("target berubah");
|
|
25005
25094
|
await rename(temp, entry.path);
|
|
25006
25095
|
} catch (error) {
|
|
25007
|
-
await
|
|
25096
|
+
await unlink2(temp).catch(() => {
|
|
25008
25097
|
});
|
|
25009
25098
|
throw error;
|
|
25010
25099
|
}
|
|
@@ -25034,7 +25123,7 @@ function docsDirOf(repoDir) {
|
|
|
25034
25123
|
}
|
|
25035
25124
|
function resolveIndex(repoDir, docsDir) {
|
|
25036
25125
|
const rel = `${docsDir}/README.md`;
|
|
25037
|
-
return existsSync6(
|
|
25126
|
+
return existsSync6(resolve10(repoDir, rel)) ? rel : "";
|
|
25038
25127
|
}
|
|
25039
25128
|
var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
|
|
25040
25129
|
var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
@@ -25411,7 +25500,7 @@ import { existsSync as existsSync7 } from "node:fs";
|
|
|
25411
25500
|
import { execFile as execFile3 } from "node:child_process";
|
|
25412
25501
|
import { promisify as promisify3 } from "node:util";
|
|
25413
25502
|
import { rmSync as rmSync3 } from "node:fs";
|
|
25414
|
-
import { join as
|
|
25503
|
+
import { join as join7 } from "node:path";
|
|
25415
25504
|
var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25416
25505
|
var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
|
|
25417
25506
|
var exec3 = promisify3(execFile3);
|
|
@@ -25455,7 +25544,7 @@ async function integrateBranch(repoDir, src, op, target2) {
|
|
|
25455
25544
|
const tgt = await resolveTarget(repoDir, target2);
|
|
25456
25545
|
if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
|
|
25457
25546
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25458
|
-
const wt =
|
|
25547
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
|
|
25459
25548
|
await reclaim(repoDir, wt);
|
|
25460
25549
|
const baseRef = op === "merge" ? tgt.ref : source;
|
|
25461
25550
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
|
|
@@ -25516,7 +25605,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
|
|
|
25516
25605
|
const src = await resolveGraphSource(repoDir, source);
|
|
25517
25606
|
if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
|
|
25518
25607
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25519
|
-
const wt =
|
|
25608
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25520
25609
|
await reclaim(repoDir, wt);
|
|
25521
25610
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25522
25611
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25554,7 +25643,7 @@ async function replayCurrent(repoDir, source, cmd) {
|
|
|
25554
25643
|
const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
25555
25644
|
if (!current || current === "HEAD")
|
|
25556
25645
|
return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
|
|
25557
|
-
const wt =
|
|
25646
|
+
const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25558
25647
|
await reclaim(repoDir, wt);
|
|
25559
25648
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25560
25649
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
@@ -25601,24 +25690,24 @@ init_db();
|
|
|
25601
25690
|
// src/services/spec-review.ts
|
|
25602
25691
|
import { execFile as execFile4 } from "node:child_process";
|
|
25603
25692
|
import { promisify as promisify4 } from "node:util";
|
|
25604
|
-
import { mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
25693
|
+
import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
|
|
25605
25694
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
25606
|
-
import { join as
|
|
25695
|
+
import { join as join8, resolve as resolve11 } from "node:path";
|
|
25607
25696
|
var exec4 = promisify4(execFile4);
|
|
25608
25697
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
25609
25698
|
var MAX = 256 * 1024;
|
|
25610
|
-
var worktreeDir = (repoDir, specId) =>
|
|
25699
|
+
var worktreeDir = (repoDir, specId) => join8(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
|
|
25611
25700
|
async function withTempIndex(wt, fn2) {
|
|
25612
25701
|
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(
|
|
25702
|
+
const dir2 = await mkdtemp(join8(tmpdir2(), "hanoman-idx-"));
|
|
25703
|
+
const tmp = join8(dir2, "index");
|
|
25704
|
+
await copyFile(resolve11(wt, idx), tmp);
|
|
25616
25705
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
25617
25706
|
try {
|
|
25618
25707
|
await exec4("git", ["add", "-A", "-N"], { cwd: wt, env, ...GIT3 });
|
|
25619
25708
|
return await fn2(env);
|
|
25620
25709
|
} finally {
|
|
25621
|
-
await
|
|
25710
|
+
await rm2(dir2, { recursive: true, force: true });
|
|
25622
25711
|
}
|
|
25623
25712
|
}
|
|
25624
25713
|
var splitZ = (s2) => s2.split("\0").filter(Boolean);
|
|
@@ -25822,7 +25911,7 @@ init_notifications();
|
|
|
25822
25911
|
|
|
25823
25912
|
// src/services/session-result.ts
|
|
25824
25913
|
init_db();
|
|
25825
|
-
import { randomUUID as
|
|
25914
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25826
25915
|
var WHITELIST = [
|
|
25827
25916
|
"projectId",
|
|
25828
25917
|
"specId",
|
|
@@ -25836,7 +25925,7 @@ var WHITELIST = [
|
|
|
25836
25925
|
"author"
|
|
25837
25926
|
];
|
|
25838
25927
|
async function recordSessionResult(input) {
|
|
25839
|
-
const id =
|
|
25928
|
+
const id = randomUUID4();
|
|
25840
25929
|
const data = { id };
|
|
25841
25930
|
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25842
25931
|
if (data.status === void 0) data.status = "done";
|
|
@@ -29185,7 +29274,7 @@ import { promisify as promisify7 } from "node:util";
|
|
|
29185
29274
|
import { execFile as execFile6 } from "node:child_process";
|
|
29186
29275
|
import { promisify as promisify6 } from "node:util";
|
|
29187
29276
|
import { existsSync as existsSync9 } from "node:fs";
|
|
29188
|
-
import { resolve as
|
|
29277
|
+
import { resolve as resolve12 } from "node:path";
|
|
29189
29278
|
var exec6 = promisify6(execFile6);
|
|
29190
29279
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
29191
29280
|
var MAX2 = 256 * 1024;
|
|
@@ -29193,7 +29282,7 @@ var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
|
29193
29282
|
function repoAbsPath(repoDir, rel, allowMissingFinal = false) {
|
|
29194
29283
|
if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
29195
29284
|
assertSafeRepoPathSync(repoDir, rel, allowMissingFinal, true);
|
|
29196
|
-
return
|
|
29285
|
+
return resolve12(repoDir, rel);
|
|
29197
29286
|
}
|
|
29198
29287
|
async function listRepoTree(repoDir, ref = "") {
|
|
29199
29288
|
if (!repoDir || !existsSync9(repoDir)) return [];
|
|
@@ -30083,14 +30172,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
30083
30172
|
// src/routes/fs.ts
|
|
30084
30173
|
import { readdir } from "node:fs/promises";
|
|
30085
30174
|
import { homedir as homedir4 } from "node:os";
|
|
30086
|
-
import { resolve as
|
|
30175
|
+
import { resolve as resolve13, dirname as dirname9, join as join9 } from "node:path";
|
|
30087
30176
|
async function fs_default(app2) {
|
|
30088
30177
|
app2.get("/fs/browse", async (req, reply) => {
|
|
30089
30178
|
const q = req.query.path;
|
|
30090
|
-
const dir2 = q && q.trim() ?
|
|
30179
|
+
const dir2 = q && q.trim() ? resolve13(q.trim()) : homedir4();
|
|
30091
30180
|
try {
|
|
30092
30181
|
const ents = await readdir(dir2, { withFileTypes: true });
|
|
30093
|
-
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path:
|
|
30182
|
+
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join9(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
30094
30183
|
const parent = dirname9(dir2);
|
|
30095
30184
|
return { path: dir2, parent: parent === dir2 ? null : parent, entries };
|
|
30096
30185
|
} catch {
|
|
@@ -30524,11 +30613,11 @@ function installCommand(m, agent, shell = shellBin()) {
|
|
|
30524
30613
|
}
|
|
30525
30614
|
|
|
30526
30615
|
// src/services/session-worktree.ts
|
|
30527
|
-
import { resolve as
|
|
30616
|
+
import { resolve as resolve14, sep as sep3 } from "node:path";
|
|
30528
30617
|
function ownsWorktree(repoDir, cwd) {
|
|
30529
|
-
const base2 =
|
|
30530
|
-
const dir2 =
|
|
30531
|
-
const container =
|
|
30618
|
+
const base2 = resolve14(repoDir);
|
|
30619
|
+
const dir2 = resolve14(cwd);
|
|
30620
|
+
const container = resolve14(base2, ".worktrees");
|
|
30532
30621
|
return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
|
|
30533
30622
|
}
|
|
30534
30623
|
|
|
@@ -30536,15 +30625,15 @@ function ownsWorktree(repoDir, cwd) {
|
|
|
30536
30625
|
init_src2();
|
|
30537
30626
|
init_db();
|
|
30538
30627
|
import { execFile as execFile8 } from "node:child_process";
|
|
30539
|
-
import { readdir as readdir2, rm as
|
|
30628
|
+
import { readdir as readdir2, rm as rm3 } from "node:fs/promises";
|
|
30540
30629
|
import { promisify as promisify9 } from "node:util";
|
|
30541
|
-
import { join as
|
|
30630
|
+
import { join as join10, resolve as resolve15 } from "node:path";
|
|
30542
30631
|
init_notifications();
|
|
30543
30632
|
var TICK_MS = 6e4;
|
|
30544
|
-
var trashDirOf = (repoDir) =>
|
|
30633
|
+
var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
|
|
30545
30634
|
var exec8 = promisify9(execFile8);
|
|
30546
30635
|
var prodReaperDeps = {
|
|
30547
|
-
rm: (path2) =>
|
|
30636
|
+
rm: (path2) => rm3(path2, { recursive: true, force: true }),
|
|
30548
30637
|
prune: async (repoDir) => {
|
|
30549
30638
|
try {
|
|
30550
30639
|
await exec8("git", ["worktree", "prune"], { cwd: repoDir, timeout: 3e4 });
|
|
@@ -30598,7 +30687,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
|
|
|
30598
30687
|
}
|
|
30599
30688
|
let removed = 0;
|
|
30600
30689
|
for (const entry of entries) {
|
|
30601
|
-
const path2 =
|
|
30690
|
+
const path2 = join10(dir2, entry);
|
|
30602
30691
|
const known = pending.get(path2);
|
|
30603
30692
|
const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
|
|
30604
30693
|
pending.set(path2, row);
|
|
@@ -30642,6 +30731,8 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30642
30731
|
init_notifications();
|
|
30643
30732
|
init_stage_machine();
|
|
30644
30733
|
init_pty();
|
|
30734
|
+
init_uploads();
|
|
30735
|
+
var ATTACHMENT_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
30645
30736
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
30646
30737
|
async function advanceStage(specId, repoDir, sessionId2, flow, worktree) {
|
|
30647
30738
|
const next = stageForRun(readPhases(phaseFilePath(repoDir, sessionId2), flow), worktree, specId);
|
|
@@ -30965,6 +31056,18 @@ async function terminal_default(app2, opts) {
|
|
|
30965
31056
|
killSession(id);
|
|
30966
31057
|
return reply.code(202).send({ cleanup: null });
|
|
30967
31058
|
});
|
|
31059
|
+
app2.post("/terminal/sessions/:id/attachments", async (req, reply) => {
|
|
31060
|
+
const { id } = req.params;
|
|
31061
|
+
if (!getSession(id)) return reply.code(404).send({ error: "not found" });
|
|
31062
|
+
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
31063
|
+
const part = await req.file?.();
|
|
31064
|
+
if (!part) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
31065
|
+
const buf = await part.toBuffer();
|
|
31066
|
+
if (part.file?.truncated) return reply.code(413).send({ error: "berkas melebihi 5 MB" });
|
|
31067
|
+
if (!ATTACHMENT_MIME.has(part.mimetype)) return reply.code(415).send({ error: "tipe berkas tak didukung" });
|
|
31068
|
+
const { path: path2 } = await saveSessionUpload(id, buf, part.mimetype);
|
|
31069
|
+
return { path: path2 };
|
|
31070
|
+
});
|
|
30968
31071
|
app2.get("/terminal/sessions/:id/ws", {
|
|
30969
31072
|
websocket: true,
|
|
30970
31073
|
preValidation: async (req, reply) => {
|
|
@@ -31203,7 +31306,7 @@ init_config2();
|
|
|
31203
31306
|
import { spawn as spawn3 } from "node:child_process";
|
|
31204
31307
|
import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
31205
31308
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
31206
|
-
import { dirname as dirname10, join as
|
|
31309
|
+
import { dirname as dirname10, join as join11 } from "node:path";
|
|
31207
31310
|
var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
|
|
31208
31311
|
function consoleArgv(t) {
|
|
31209
31312
|
return [
|
|
@@ -31218,8 +31321,8 @@ function consoleArgv(t) {
|
|
|
31218
31321
|
];
|
|
31219
31322
|
}
|
|
31220
31323
|
function askpassScript() {
|
|
31221
|
-
const dir2 = mkdtempSync(
|
|
31222
|
-
const path2 =
|
|
31324
|
+
const dir2 = mkdtempSync(join11(tmpdir3(), "hanoman-askpass-"));
|
|
31325
|
+
const path2 = join11(dir2, "askpass.sh");
|
|
31223
31326
|
writeFileSync4(path2, `#!/bin/sh
|
|
31224
31327
|
printf '%s' "$HANOMAN_SSH_PASSWORD"
|
|
31225
31328
|
`, { mode: 448 });
|
|
@@ -31280,15 +31383,15 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
31280
31383
|
// src/services/vps-audit.ts
|
|
31281
31384
|
init_db();
|
|
31282
31385
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
31283
|
-
import { join as
|
|
31386
|
+
import { join as join13 } from "node:path";
|
|
31284
31387
|
|
|
31285
31388
|
// src/runner/deps.ts
|
|
31286
31389
|
import { existsSync as existsSync11 } from "node:fs";
|
|
31287
|
-
import { dirname as dirname11, join as
|
|
31390
|
+
import { dirname as dirname11, join as join12 } from "node:path";
|
|
31288
31391
|
function repoRootFrom(startDir) {
|
|
31289
31392
|
let dir2 = startDir;
|
|
31290
31393
|
for (let i = 0; i < 8; i++) {
|
|
31291
|
-
if (existsSync11(
|
|
31394
|
+
if (existsSync11(join12(dir2, "pnpm-workspace.yaml"))) return dir2;
|
|
31292
31395
|
const parent = dirname11(dir2);
|
|
31293
31396
|
if (parent === dir2) break;
|
|
31294
31397
|
dir2 = parent;
|
|
@@ -31394,7 +31497,7 @@ function parseHealth(out3) {
|
|
|
31394
31497
|
return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
|
|
31395
31498
|
}
|
|
31396
31499
|
var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
|
|
31397
|
-
var scriptPath = (f) =>
|
|
31500
|
+
var scriptPath = (f) => join13(repoRoot(), "server", "scripts", "vps", f);
|
|
31398
31501
|
async function itemStatesOf(vpsId) {
|
|
31399
31502
|
const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
|
|
31400
31503
|
return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
|
|
@@ -31543,11 +31646,11 @@ init_config2();
|
|
|
31543
31646
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
31544
31647
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
31545
31648
|
import { homedir as homedir5 } from "node:os";
|
|
31546
|
-
import { join as
|
|
31547
|
-
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ??
|
|
31649
|
+
import { join as join14 } from "node:path";
|
|
31650
|
+
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join14(homedir5(), ".hanoman");
|
|
31548
31651
|
function ensureHanomanKey() {
|
|
31549
31652
|
const dir2 = keyDir();
|
|
31550
|
-
const privPath =
|
|
31653
|
+
const privPath = join14(dir2, "id_ed25519");
|
|
31551
31654
|
const pubPath = `${privPath}.pub`;
|
|
31552
31655
|
if (!existsSync12(privPath)) {
|
|
31553
31656
|
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
@@ -31785,7 +31888,7 @@ init_config2();
|
|
|
31785
31888
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
31786
31889
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
31787
31890
|
import { homedir as homedir7 } from "node:os";
|
|
31788
|
-
import { join as
|
|
31891
|
+
import { join as join15 } from "node:path";
|
|
31789
31892
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
31790
31893
|
var TTL_MS2 = 3e4;
|
|
31791
31894
|
var lastOk = null;
|
|
@@ -31798,7 +31901,7 @@ var LABELS = {
|
|
|
31798
31901
|
var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
31799
31902
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
31800
31903
|
function credsFile() {
|
|
31801
|
-
return
|
|
31904
|
+
return join15(effectiveStr("CLAUDE_CONFIG_DIR") ?? join15(homedir7(), ".claude"), ".credentials.json");
|
|
31802
31905
|
}
|
|
31803
31906
|
function readAccessToken() {
|
|
31804
31907
|
if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
|
|
@@ -31872,14 +31975,14 @@ async function getLimits() {
|
|
|
31872
31975
|
// src/services/codex-limits.ts
|
|
31873
31976
|
import { open as open3, readdir as readdir3, stat } from "node:fs/promises";
|
|
31874
31977
|
import { homedir as homedir8 } from "node:os";
|
|
31875
|
-
import { join as
|
|
31978
|
+
import { join as join16 } from "node:path";
|
|
31876
31979
|
var TTL_MS3 = 3e4;
|
|
31877
31980
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
31878
31981
|
var TAIL_BYTES = 512 * 1024;
|
|
31879
31982
|
var MAX_FILES = 8;
|
|
31880
31983
|
var cache2 = null;
|
|
31881
31984
|
var freshUntil2 = 0;
|
|
31882
|
-
var codexSessionsDir = () =>
|
|
31985
|
+
var codexSessionsDir = () => join16(process.env.CODEX_HOME ?? join16(homedir8(), ".codex"), "sessions");
|
|
31883
31986
|
var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
|
|
31884
31987
|
function windowLabel(minutes) {
|
|
31885
31988
|
if (minutes === 300) return "Sesi 5 jam";
|
|
@@ -31912,7 +32015,7 @@ async function recentRollouts(dir2) {
|
|
|
31912
32015
|
return [];
|
|
31913
32016
|
}
|
|
31914
32017
|
const stamped = await Promise.all(entries.map(async (rel) => {
|
|
31915
|
-
const full =
|
|
32018
|
+
const full = join16(dir2, rel);
|
|
31916
32019
|
try {
|
|
31917
32020
|
return { full, mtime: (await stat(full)).mtimeMs };
|
|
31918
32021
|
} catch {
|
|
@@ -32040,7 +32143,7 @@ init_src();
|
|
|
32040
32143
|
init_src();
|
|
32041
32144
|
init_config2();
|
|
32042
32145
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
32043
|
-
import { resolve as
|
|
32146
|
+
import { resolve as resolve16, dirname as dirname12 } from "node:path";
|
|
32044
32147
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32045
32148
|
var UPDATE_COMMAND = "npm i -g hanoman@latest";
|
|
32046
32149
|
function composeUpdate(x) {
|
|
@@ -32066,7 +32169,7 @@ var lastLatest = null;
|
|
|
32066
32169
|
var lastStatus = "unavailable";
|
|
32067
32170
|
function runningVersion() {
|
|
32068
32171
|
const here = dirname12(fileURLToPath3(import.meta.url));
|
|
32069
|
-
for (const p of [
|
|
32172
|
+
for (const p of [resolve16(here, "build-info.json"), resolve16(here, "../package.json")]) {
|
|
32070
32173
|
try {
|
|
32071
32174
|
const v = JSON.parse(readFileSync12(p, "utf8")).version;
|
|
32072
32175
|
if (typeof v === "string" && v) return v;
|
|
@@ -32417,54 +32520,7 @@ setAcceptedHook((row) => broadcastSyncLog(row));
|
|
|
32417
32520
|
init_sync();
|
|
32418
32521
|
init_sync_client();
|
|
32419
32522
|
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
|
|
32523
|
+
init_uploads();
|
|
32468
32524
|
init_config2();
|
|
32469
32525
|
var zPush = external_exports.object({
|
|
32470
32526
|
records: external_exports.array(external_exports.object({
|
|
@@ -32615,12 +32671,12 @@ async function session_results_default(app2) {
|
|
|
32615
32671
|
// src/services/session-history.ts
|
|
32616
32672
|
init_db();
|
|
32617
32673
|
init_pty();
|
|
32618
|
-
import { randomUUID as
|
|
32674
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
32619
32675
|
|
|
32620
32676
|
// src/services/transcript-store.ts
|
|
32621
32677
|
init_config2();
|
|
32622
32678
|
init_src2();
|
|
32623
|
-
import { randomUUID as
|
|
32679
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
32624
32680
|
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
|
|
32625
32681
|
import { join as join17, resolve as resolve17, basename as basename4 } from "node:path";
|
|
32626
32682
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
@@ -32642,7 +32698,7 @@ async function saveTranscript(text) {
|
|
|
32642
32698
|
const { body, truncated } = clamp(text);
|
|
32643
32699
|
const dir2 = transcriptDir();
|
|
32644
32700
|
await mkdir3(dir2, { recursive: true, mode: 448 });
|
|
32645
|
-
const key = `${
|
|
32701
|
+
const key = `${randomUUID5()}.log`;
|
|
32646
32702
|
await writeFile2(join17(dir2, key), body, { encoding: "utf8", mode: 384 });
|
|
32647
32703
|
return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
|
|
32648
32704
|
}
|
|
@@ -32687,7 +32743,7 @@ async function titleFor(specId) {
|
|
|
32687
32743
|
async function beginSession(b) {
|
|
32688
32744
|
await prisma.sessionHistory.create({
|
|
32689
32745
|
data: {
|
|
32690
|
-
id:
|
|
32746
|
+
id: randomUUID6(),
|
|
32691
32747
|
sessionId: b.sessionId,
|
|
32692
32748
|
projectId: b.projectId,
|
|
32693
32749
|
specId: b.specId ?? null,
|
|
@@ -32892,8 +32948,9 @@ init_db();
|
|
|
32892
32948
|
|
|
32893
32949
|
// src/services/ticket.ts
|
|
32894
32950
|
init_db();
|
|
32895
|
-
|
|
32951
|
+
init_uploads();
|
|
32896
32952
|
init_src();
|
|
32953
|
+
import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
|
|
32897
32954
|
function hashAccessKey(key) {
|
|
32898
32955
|
return createHash7("sha256").update(key).digest("hex");
|
|
32899
32956
|
}
|
|
@@ -32938,9 +32995,10 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32938
32995
|
// src/services/ticket-intake.ts
|
|
32939
32996
|
init_db();
|
|
32940
32997
|
init_notifications();
|
|
32998
|
+
init_uploads();
|
|
32941
32999
|
|
|
32942
33000
|
// src/services/upload-pipeline.ts
|
|
32943
|
-
import { randomUUID as
|
|
33001
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
32944
33002
|
import { spawn as spawn4 } from "node:child_process";
|
|
32945
33003
|
import { mkdir as mkdir4, rename as rename2, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises";
|
|
32946
33004
|
import { basename as basename5, isAbsolute as isAbsolute4, join as join18 } from "node:path";
|
|
@@ -40306,6 +40364,7 @@ var dist_default = constructor_default;
|
|
|
40306
40364
|
|
|
40307
40365
|
// src/services/upload-pipeline.ts
|
|
40308
40366
|
init_db();
|
|
40367
|
+
init_uploads();
|
|
40309
40368
|
var UPLOAD_LIMITS = {
|
|
40310
40369
|
fileBytes: 5 * 1024 * 1024,
|
|
40311
40370
|
ticketBytes: 10 * 1024 * 1024,
|
|
@@ -40409,8 +40468,8 @@ async function processUpload(input, deps = {}) {
|
|
|
40409
40468
|
const quarantineDir = join18(storageDir, ".quarantine");
|
|
40410
40469
|
await mkdir4(quarantineDir, { recursive: true, mode: 448 });
|
|
40411
40470
|
await mkdir4(storageDir, { recursive: true, mode: 448 });
|
|
40412
|
-
const quarantine = join18(quarantineDir, `${
|
|
40413
|
-
const storageKey = `${
|
|
40471
|
+
const quarantine = join18(quarantineDir, `${randomUUID7()}.upload`);
|
|
40472
|
+
const storageKey = `${randomUUID7()}${type.extension}`;
|
|
40414
40473
|
await writeFile3(quarantine, normalized, { mode: 384, flag: "wx" });
|
|
40415
40474
|
try {
|
|
40416
40475
|
await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
|
|
@@ -40598,6 +40657,7 @@ async function help_default(app2) {
|
|
|
40598
40657
|
|
|
40599
40658
|
// src/routes/tickets.ts
|
|
40600
40659
|
init_db();
|
|
40660
|
+
init_uploads();
|
|
40601
40661
|
|
|
40602
40662
|
// src/services/ticket-accept.ts
|
|
40603
40663
|
init_db();
|
|
@@ -41534,7 +41594,7 @@ init_config2();
|
|
|
41534
41594
|
init_pty();
|
|
41535
41595
|
init_session_sandbox();
|
|
41536
41596
|
import { execFile as execFile10 } from "node:child_process";
|
|
41537
|
-
import { randomUUID as
|
|
41597
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
41538
41598
|
import { mkdirSync as mkdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
41539
41599
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
41540
41600
|
import { join as join19 } from "node:path";
|
|
@@ -41567,9 +41627,9 @@ function leadProcess(prompt, o, env = process.env) {
|
|
|
41567
41627
|
} };
|
|
41568
41628
|
const promptDir = join19(tmpdir4(), "hanoman-prompts");
|
|
41569
41629
|
mkdirSync9(promptDir, { recursive: true, mode: 448 });
|
|
41570
|
-
const promptFile = join19(promptDir, `oneshot-${
|
|
41630
|
+
const promptFile = join19(promptDir, `oneshot-${randomUUID8()}`);
|
|
41571
41631
|
writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
|
|
41572
|
-
const workspace = o.cwd ?? join19(tmpdir4(), `hanoman-oneshot-${
|
|
41632
|
+
const workspace = o.cwd ?? join19(tmpdir4(), `hanoman-oneshot-${randomUUID8()}`);
|
|
41573
41633
|
if (!o.cwd) mkdirSync9(workspace, { recursive: false, mode: 448 });
|
|
41574
41634
|
try {
|
|
41575
41635
|
const argsWithoutPrompt = directArgs.slice(0, -1);
|
|
@@ -45768,7 +45828,7 @@ init_tap();
|
|
|
45768
45828
|
// src/services/webhooks/emit.ts
|
|
45769
45829
|
init_src();
|
|
45770
45830
|
init_db();
|
|
45771
|
-
import { randomUUID as
|
|
45831
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
45772
45832
|
function skipped(def2, row) {
|
|
45773
45833
|
if (!def2.skipWhen || !row) return false;
|
|
45774
45834
|
return row[def2.skipWhen.field] === def2.skipWhen.equals;
|
|
@@ -45834,7 +45894,7 @@ async function emitWebhook(i) {
|
|
|
45834
45894
|
const targets = matchingEndpoints(type, projectId);
|
|
45835
45895
|
if (!targets.length) return;
|
|
45836
45896
|
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_${
|
|
45897
|
+
const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID9().replace(/-/g, "")}`);
|
|
45838
45898
|
if (!env) return;
|
|
45839
45899
|
await enqueueEnvelope(env, targets);
|
|
45840
45900
|
} catch (e) {
|
|
@@ -46184,6 +46244,7 @@ async function secureHanomanHome(opts) {
|
|
|
46184
46244
|
|
|
46185
46245
|
// src/services/retention.ts
|
|
46186
46246
|
init_db();
|
|
46247
|
+
init_uploads();
|
|
46187
46248
|
var RETENTION_DAYS = {
|
|
46188
46249
|
tickets: 90,
|
|
46189
46250
|
newTickets: 180,
|
|
@@ -46298,6 +46359,7 @@ function startRetentionSweep() {
|
|
|
46298
46359
|
}
|
|
46299
46360
|
|
|
46300
46361
|
// src/server.ts
|
|
46362
|
+
init_uploads();
|
|
46301
46363
|
var app = buildApp();
|
|
46302
46364
|
var port = Number(process.env.PORT ?? 8787);
|
|
46303
46365
|
var host = process.env.HOST ?? "127.0.0.1";
|