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/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) : randomUUID().slice(0, 8);
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 resolve9 } from "node:path";
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 join5, relative, resolve as resolve8, sep as sep2 } from "node:path";
24833
- import { randomUUID as randomUUID2 } from "node:crypto";
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 = join5(current, part);
24951
+ current = join6(current, part);
24863
24952
  try {
24864
- await mkdir(current, { mode: 448 });
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 = join5(current, part);
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 = join5(current, parts[i]);
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, resolve8(current))) denied("keluar root");
24905
- const parent = resolve8(current, "..");
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 = join5(current, parts[i]);
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, resolve8(current))) denied("keluar root");
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 = resolve8(path2, "..");
25031
+ const parent = resolve9(path2, "..");
24943
25032
  const parentRel = relative(realpathSync2(root), parent);
24944
25033
  if (parentRel) assertSafeRepoPathSync(root, parentRel);
24945
- const temp = join5(parent, `.hanoman-${randomUUID2()}.tmp`);
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 mkdir(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
25078
+ await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
24990
25079
  if (error.code !== "EEXIST") throw error;
24991
25080
  });
24992
- const temp = join5(entry.parent, `.hanoman-${randomUUID2()}.tmp`);
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 unlink(temp).catch(() => {
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(resolve9(repoDir, rel)) ? rel : "";
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 join6 } from "node:path";
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 = join6(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
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 = join6(repoDir, ".worktrees", `merge-${sanitize(current)}`);
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 = join6(repoDir, ".worktrees", `merge-${sanitize(current)}`);
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 join7, resolve as resolve10 } from "node:path";
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) => join7(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
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(join7(tmpdir2(), "hanoman-idx-"));
25614
- const tmp = join7(dir2, "index");
25615
- await copyFile(resolve10(wt, idx), tmp);
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 rm(dir2, { recursive: true, force: true });
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 randomUUID3 } from "node:crypto";
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 = randomUUID3();
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 resolve11 } from "node:path";
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 resolve11(repoDir, rel);
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 resolve12, dirname as dirname9, join as join8 } from "node:path";
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() ? resolve12(q.trim()) : homedir4();
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: join8(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
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 resolve13, sep as sep3 } from "node:path";
30616
+ import { resolve as resolve14, sep as sep3 } from "node:path";
30528
30617
  function ownsWorktree(repoDir, cwd) {
30529
- const base2 = resolve13(repoDir);
30530
- const dir2 = resolve13(cwd);
30531
- const container = resolve13(base2, ".worktrees");
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 rm2 } from "node:fs/promises";
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 join9, resolve as resolve14 } from "node:path";
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) => resolve14(repoDir, ".worktrees", ".trash");
30633
+ var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
30545
30634
  var exec8 = promisify9(execFile8);
30546
30635
  var prodReaperDeps = {
30547
- rm: (path2) => rm2(path2, { recursive: true, force: true }),
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 = join9(dir2, entry);
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 join10 } from "node:path";
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(join10(tmpdir3(), "hanoman-askpass-"));
31222
- const path2 = join10(dir2, "askpass.sh");
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 join12 } from "node:path";
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 join11 } from "node:path";
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(join11(dir2, "pnpm-workspace.yaml"))) return dir2;
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) => join12(repoRoot(), "server", "scripts", "vps", 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 join13 } from "node:path";
31547
- var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join13(homedir5(), ".hanoman");
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 = join13(dir2, "id_ed25519");
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 join14 } from "node:path";
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 join14(effectiveStr("CLAUDE_CONFIG_DIR") ?? join14(homedir7(), ".claude"), ".credentials.json");
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 join15 } from "node:path";
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 = () => join15(process.env.CODEX_HOME ?? join15(homedir8(), ".codex"), "sessions");
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 = join15(dir2, rel);
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 resolve15, dirname as dirname12 } from "node:path";
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 [resolve15(here, "build-info.json"), resolve15(here, "../package.json")]) {
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 randomUUID5 } from "node:crypto";
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 randomUUID4 } from "node:crypto";
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 = `${randomUUID4()}.log`;
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: randomUUID5(),
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
- import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
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 randomUUID6 } from "node:crypto";
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, `${randomUUID6()}.upload`);
40413
- const storageKey = `${randomUUID6()}${type.extension}`;
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 randomUUID7 } from "node:crypto";
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-${randomUUID7()}`);
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-${randomUUID7()}`);
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 randomUUID8 } from "node:crypto";
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_${randomUUID8().replace(/-/g, "")}`);
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";