@rubytech/create-maxy-code 0.1.353 → 0.1.355

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.
Files changed (52) hide show
  1. package/dist/__tests__/preflight-port-classifier.test.js +52 -0
  2. package/dist/preflight-port-classifier.js +31 -2
  3. package/package.json +1 -1
  4. package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
  5. package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
  6. package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
  7. package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
  8. package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
  9. package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
  10. package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
  11. package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
  12. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
  13. package/payload/platform/lib/admins-write/dist/index.js +93 -31
  14. package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
  15. package/payload/platform/lib/admins-write/src/index.ts +133 -36
  16. package/payload/platform/plugins/admin/PLUGIN.md +4 -0
  17. package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
  18. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  19. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
  20. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  21. package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
  22. package/payload/platform/scripts/check-no-esm-require.mjs +4 -0
  23. package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
  24. package/payload/server/maxy-edge.js +1 -1
  25. package/payload/server/public/assets/AdminLoginScreens-4I5CdIWl.js +1 -0
  26. package/payload/server/public/assets/{AdminShell-CC-CuN6Y.js → AdminShell-Bc9F7X_N.js} +1 -1
  27. package/payload/server/public/assets/{Checkbox-sQBc9xSy.js → Checkbox-DoQV18Ow.js} +1 -1
  28. package/payload/server/public/assets/OperatorConversations-BJY9FZbr.js +9 -0
  29. package/payload/server/public/assets/OperatorConversations-C1I78hON.css +1 -0
  30. package/payload/server/public/assets/{admin-qW6jM6l0.js → admin-DLsMA09F.js} +1 -1
  31. package/payload/server/public/assets/{browser-BsW6KaxZ.js → browser-w6rY_KAO.js} +1 -1
  32. package/payload/server/public/assets/{calendar-BPAiN1cL.js → calendar-CvEdDFYd.js} +1 -1
  33. package/payload/server/public/assets/chat-BlOWMUVI.js +1 -0
  34. package/payload/server/public/assets/{data-D_cR_NCZ.js → data-p3T_EZFq.js} +1 -1
  35. package/payload/server/public/assets/{graph-CmpAy2WP.js → graph-BrwfYOr3.js} +1 -1
  36. package/payload/server/public/assets/{graph-labels-C0SLq_VP.js → graph-labels-BNZ8u8TE.js} +1 -1
  37. package/payload/server/public/assets/{operator-CrjtJcEY.js → operator-BNY1P_mH.js} +1 -1
  38. package/payload/server/public/assets/{page-Cf3fVDSo.js → page-CtygoIXt.js} +3 -3
  39. package/payload/server/public/assets/{public-BkNtvSYp.js → public-CDzw6yjH.js} +1 -1
  40. package/payload/server/public/browser.html +4 -4
  41. package/payload/server/public/calendar.html +4 -4
  42. package/payload/server/public/chat.html +5 -5
  43. package/payload/server/public/data.html +4 -4
  44. package/payload/server/public/graph.html +6 -6
  45. package/payload/server/public/index.html +6 -6
  46. package/payload/server/public/operator.html +7 -7
  47. package/payload/server/public/public.html +5 -5
  48. package/payload/server/server.js +1289 -1150
  49. package/payload/server/public/assets/AdminLoginScreens-CzdOQKyO.js +0 -1
  50. package/payload/server/public/assets/OperatorConversations-CA_2c3sp.js +0 -9
  51. package/payload/server/public/assets/OperatorConversations-DPVKGsRS.css +0 -1
  52. package/payload/server/public/assets/chat-DVD9yoVL.js +0 -1
@@ -21,6 +21,7 @@ import {
21
21
  REMOTE_PASSWORD_FILE,
22
22
  RFB_PORT,
23
23
  SYSTEM_PATH_SLUGS,
24
+ USERS_AUDIT_LOG,
24
25
  USERS_FILE,
25
26
  VISITOR_TOKEN_SECRET_FILE,
26
27
  VNC_DISPLAY,
@@ -105,7 +106,7 @@ import {
105
106
  vncLog,
106
107
  walkPremiumBundles,
107
108
  writeAdminUserAndPerson
108
- } from "./chunk-QAKVFSEJ.js";
109
+ } from "./chunk-7GNXBVGY.js";
109
110
  import {
110
111
  __commonJS,
111
112
  __toESM
@@ -361,9 +362,9 @@ var require_dist = __commonJS({
361
362
  try {
362
363
  await onEmpty(c);
363
364
  } catch (err) {
364
- const id8 = c.elementId.slice(0, 8);
365
+ const id82 = c.elementId.slice(0, 8);
365
366
  const message = err instanceof Error ? err.message : String(err);
366
- process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id8} labels=${c.labels.join(",")} message=${message}
367
+ process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id82} labels=${c.labels.join(",")} message=${message}
367
368
  `);
368
369
  continue;
369
370
  }
@@ -1303,7 +1304,7 @@ var serveStatic = (options = { root: "" }) => {
1303
1304
  // server/index.ts
1304
1305
  import { readFileSync as readFileSync33, existsSync as existsSync32, watchFile } from "fs";
1305
1306
  import { spawn as spawn3 } from "child_process";
1306
- import { resolve as resolve34, join as join32, basename as basename10 } from "path";
1307
+ import { resolve as resolve32, join as join32, basename as basename10 } from "path";
1307
1308
  import { homedir as homedir3 } from "os";
1308
1309
  import { monitorEventLoopDelay } from "perf_hooks";
1309
1310
 
@@ -1961,7 +1962,7 @@ var credsSaveQueue = Promise.resolve();
1961
1962
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
1962
1963
  console.error(`${TAG2} draining credential save queue\u2026`);
1963
1964
  const timer2 = new Promise(
1964
- (resolve35) => setTimeout(() => resolve35("timeout"), timeoutMs)
1965
+ (resolve33) => setTimeout(() => resolve33("timeout"), timeoutMs)
1965
1966
  );
1966
1967
  const result = await Promise.race([
1967
1968
  credsSaveQueue.then(() => "drained"),
@@ -2089,11 +2090,11 @@ async function createWaSocket(opts) {
2089
2090
  return sock;
2090
2091
  }
2091
2092
  async function waitForConnection(sock) {
2092
- return new Promise((resolve35, reject) => {
2093
+ return new Promise((resolve33, reject) => {
2093
2094
  const handler = (update) => {
2094
2095
  if (update.connection === "open") {
2095
2096
  sock.ev.off("connection.update", handler);
2096
- resolve35();
2097
+ resolve33();
2097
2098
  }
2098
2099
  if (update.connection === "close") {
2099
2100
  sock.ev.off("connection.update", handler);
@@ -2207,14 +2208,14 @@ ${inspected}`;
2207
2208
  return inspect2(err, INSPECT_OPTS2);
2208
2209
  }
2209
2210
  function withTimeout(label, promise, timeoutMs) {
2210
- return new Promise((resolve35, reject) => {
2211
+ return new Promise((resolve33, reject) => {
2211
2212
  const timer2 = setTimeout(() => {
2212
2213
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2213
2214
  }, timeoutMs);
2214
2215
  promise.then(
2215
2216
  (value) => {
2216
2217
  clearTimeout(timer2);
2217
- resolve35(value);
2218
+ resolve33(value);
2218
2219
  },
2219
2220
  (err) => {
2220
2221
  clearTimeout(timer2);
@@ -3098,8 +3099,8 @@ async function persistWhatsAppMessage(input) {
3098
3099
  const { givenName, familyName } = splitName(input.pushName);
3099
3100
  const prev = sessionWriteLocks.get(input.cacheKey);
3100
3101
  let release;
3101
- const mine = new Promise((resolve35) => {
3102
- release = resolve35;
3102
+ const mine = new Promise((resolve33) => {
3103
+ release = resolve33;
3103
3104
  });
3104
3105
  const chained = (prev ?? Promise.resolve()).then(() => mine);
3105
3106
  sessionWriteLocks.set(input.cacheKey, chained);
@@ -4209,11 +4210,11 @@ async function connectWithReconnect(conn) {
4209
4210
  console.error(
4210
4211
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4211
4212
  );
4212
- await new Promise((resolve35) => {
4213
- const timer2 = setTimeout(resolve35, delay);
4213
+ await new Promise((resolve33) => {
4214
+ const timer2 = setTimeout(resolve33, delay);
4214
4215
  conn.abortController.signal.addEventListener("abort", () => {
4215
4216
  clearTimeout(timer2);
4216
- resolve35();
4217
+ resolve33();
4217
4218
  }, { once: true });
4218
4219
  });
4219
4220
  }
@@ -4221,16 +4222,16 @@ async function connectWithReconnect(conn) {
4221
4222
  }
4222
4223
  }
4223
4224
  function waitForDisconnectEvent(conn) {
4224
- return new Promise((resolve35) => {
4225
+ return new Promise((resolve33) => {
4225
4226
  if (!conn.sock) {
4226
- resolve35();
4227
+ resolve33();
4227
4228
  return;
4228
4229
  }
4229
4230
  const sock = conn.sock;
4230
4231
  const handler = (update) => {
4231
4232
  if (update.connection === "close") {
4232
4233
  sock.ev.off("connection.update", handler);
4233
- resolve35();
4234
+ resolve33();
4234
4235
  }
4235
4236
  };
4236
4237
  sock.ev.on("connection.update", handler);
@@ -4490,8 +4491,8 @@ async function handleInboundMessage(conn, msg) {
4490
4491
  const conversationKey = isGroup ? remoteJid : senderPhone;
4491
4492
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4492
4493
  let resolvePending;
4493
- const sttPending = new Promise((resolve35) => {
4494
- resolvePending = resolve35;
4494
+ const sttPending = new Promise((resolve33) => {
4495
+ resolvePending = resolve33;
4495
4496
  });
4496
4497
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4497
4498
  try {
@@ -4891,20 +4892,20 @@ function buildX11Env(chromiumWrapperPath, transport = "vnc") {
4891
4892
 
4892
4893
  // server/routes/health.ts
4893
4894
  function checkPort(port2, timeoutMs = 500) {
4894
- return new Promise((resolve35) => {
4895
+ return new Promise((resolve33) => {
4895
4896
  const socket = createConnection2(port2, "127.0.0.1");
4896
4897
  socket.setTimeout(timeoutMs);
4897
4898
  socket.once("connect", () => {
4898
4899
  socket.destroy();
4899
- resolve35(true);
4900
+ resolve33(true);
4900
4901
  });
4901
4902
  socket.once("error", () => {
4902
4903
  socket.destroy();
4903
- resolve35(false);
4904
+ resolve33(false);
4904
4905
  });
4905
4906
  socket.once("timeout", () => {
4906
4907
  socket.destroy();
4907
- resolve35(false);
4908
+ resolve33(false);
4908
4909
  });
4909
4910
  });
4910
4911
  }
@@ -6044,8 +6045,8 @@ async function startLogin(opts) {
6044
6045
  await clearAuth(authDir);
6045
6046
  let resolveCode = null;
6046
6047
  let rejectCode = null;
6047
- const codePromise = new Promise((resolve35, reject) => {
6048
- resolveCode = resolve35;
6048
+ const codePromise = new Promise((resolve33, reject) => {
6049
+ resolveCode = resolve33;
6049
6050
  rejectCode = reject;
6050
6051
  });
6051
6052
  const codeTimer = setTimeout(
@@ -9471,8 +9472,31 @@ import { openSync as openSync3, closeSync as closeSync3, writeFileSync as writeF
9471
9472
  import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
9472
9473
 
9473
9474
  // ../lib/admins-write/src/index.ts
9474
- import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7 } from "fs";
9475
+ import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7, appendFileSync } from "fs";
9475
9476
  import { dirname as dirname4, join as join20 } from "path";
9477
+ function id8(value) {
9478
+ return value.slice(0, 8);
9479
+ }
9480
+ function formatAuditRowIds(userIds) {
9481
+ return userIds.map(id8).join(",");
9482
+ }
9483
+ function appendUsersAuditLine(audit, fields) {
9484
+ const sess = audit.session ? ` session=${id8(audit.session)}` : "";
9485
+ const actor = audit.actor.includes("-") ? id8(audit.actor) : audit.actor;
9486
+ const line = `[users-audit] action=${fields.action} actor=${actor}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
9487
+ `;
9488
+ try {
9489
+ mkdirSync4(dirname4(audit.logFile), { recursive: true, mode: 448 });
9490
+ appendFileSync(audit.logFile, line, { mode: 384 });
9491
+ } catch (err) {
9492
+ console.error(
9493
+ `[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`
9494
+ );
9495
+ }
9496
+ }
9497
+ function logUsersAudit(audit, fields) {
9498
+ appendUsersAuditLine(audit, fields);
9499
+ }
9476
9500
  function logLine(input, result) {
9477
9501
  const userIdShort = input.userId.slice(0, 8);
9478
9502
  console.error(
@@ -9499,6 +9523,7 @@ function writeAdminEntry(input) {
9499
9523
  users = parsed;
9500
9524
  }
9501
9525
  }
9526
+ const rowsBefore = users.map((u) => id8(u.userId)).join(",");
9502
9527
  const existing = users.findIndex((u) => u.userId === input.userId);
9503
9528
  if (existing === -1) {
9504
9529
  users.push({ userId: input.userId, pin: input.pin });
@@ -9507,6 +9532,12 @@ function writeAdminEntry(input) {
9507
9532
  }
9508
9533
  writeFileAtomic(input.usersFile, JSON.stringify(users, null, 2) + "\n", 384);
9509
9534
  result.usersJsonResult = "ok";
9535
+ appendUsersAuditLine(input.audit, {
9536
+ action: existing === -1 ? "add" : "set-pin",
9537
+ field: existing === -1 ? "row" : "pin",
9538
+ rowsBefore,
9539
+ rowsAfter: users.map((u) => id8(u.userId)).join(",")
9540
+ });
9510
9541
  } catch (err) {
9511
9542
  result.usersJsonResult = "fail";
9512
9543
  result.usersError = err instanceof Error ? err.message : String(err);
@@ -9535,12 +9566,8 @@ function writeAdminEntry(input) {
9535
9566
  logLine(input, result);
9536
9567
  return result;
9537
9568
  }
9538
- function checkAdminAuthInvariant(input) {
9539
- const result = {
9540
- divergences: 0,
9541
- accountWithoutUsers: [],
9542
- usersWithoutAccount: []
9543
- };
9569
+ function computeAdminStoreDivergence(input) {
9570
+ const result = { divergences: 0, accountWithoutUsers: [], usersWithoutAccount: [], errors: [] };
9544
9571
  const usersUserIds = /* @__PURE__ */ new Set();
9545
9572
  if (existsSync13(input.usersFile)) {
9546
9573
  try {
@@ -9552,8 +9579,7 @@ function checkAdminAuthInvariant(input) {
9552
9579
  }
9553
9580
  }
9554
9581
  } catch (err) {
9555
- const msg = err instanceof Error ? err.message : String(err);
9556
- console.error(`[${input.tag}] users.json unreadable usersFile=${input.usersFile} error=${msg}`);
9582
+ result.errors.push({ source: input.usersFile, detail: err instanceof Error ? err.message : String(err) });
9557
9583
  }
9558
9584
  }
9559
9585
  const accountUserIds = /* @__PURE__ */ new Set();
@@ -9562,9 +9588,7 @@ function checkAdminAuthInvariant(input) {
9562
9588
  try {
9563
9589
  entries = readdirSync12(input.accountsDir);
9564
9590
  } catch (err) {
9565
- const msg = err instanceof Error ? err.message : String(err);
9566
- console.error(`[${input.tag}] accounts-dir unreadable accountsDir=${input.accountsDir} error=${msg}`);
9567
- console.error(`[${input.tag}] check complete divergences=0 (accounts-dir unreadable)`);
9591
+ result.errors.push({ source: input.accountsDir, detail: err instanceof Error ? err.message : String(err) });
9568
9592
  return result;
9569
9593
  }
9570
9594
  for (const entry of entries) {
@@ -9582,18 +9606,13 @@ function checkAdminAuthInvariant(input) {
9582
9606
  const config = JSON.parse(readFileSync21(accountJsonPath, "utf-8"));
9583
9607
  admins = config.admins ?? [];
9584
9608
  } catch (err) {
9585
- const msg = err instanceof Error ? err.message : String(err);
9586
- console.error(`[${input.tag}] account.json unreadable source=${accountJsonPath} error=${msg}`);
9609
+ result.errors.push({ source: accountJsonPath, detail: err instanceof Error ? err.message : String(err) });
9587
9610
  continue;
9588
9611
  }
9589
9612
  for (const a of admins) {
9590
9613
  if (typeof a.userId !== "string") continue;
9591
9614
  accountUserIds.add(a.userId);
9592
9615
  if (!usersUserIds.has(a.userId)) {
9593
- const userIdShort = a.userId.slice(0, 8);
9594
- console.error(
9595
- `[${input.tag}] direction=account-without-users userId=${userIdShort} source=${accountJsonPath}`
9596
- );
9597
9616
  result.accountWithoutUsers.push({ userId: a.userId, source: accountJsonPath });
9598
9617
  result.divergences += 1;
9599
9618
  }
@@ -9602,17 +9621,30 @@ function checkAdminAuthInvariant(input) {
9602
9621
  }
9603
9622
  for (const uid of usersUserIds) {
9604
9623
  if (!accountUserIds.has(uid)) {
9605
- const userIdShort = uid.slice(0, 8);
9606
- console.error(
9607
- `[${input.tag}] direction=users-without-account userId=${userIdShort} source=${input.usersFile}`
9608
- );
9609
9624
  result.usersWithoutAccount.push({ userId: uid });
9610
9625
  result.divergences += 1;
9611
9626
  }
9612
9627
  }
9613
- console.error(`[${input.tag}] check complete divergences=${result.divergences}`);
9614
9628
  return result;
9615
9629
  }
9630
+ function checkAdminAuthInvariant(input) {
9631
+ const d = computeAdminStoreDivergence({ usersFile: input.usersFile, accountsDir: input.accountsDir });
9632
+ for (const e of d.errors) {
9633
+ console.error(`[${input.tag}] unreadable source=${e.source} error=${e.detail}`);
9634
+ }
9635
+ for (const a of d.accountWithoutUsers) {
9636
+ console.error(`[${input.tag}] direction=account-without-users userId=${a.userId.slice(0, 8)} source=${a.source}`);
9637
+ }
9638
+ for (const u of d.usersWithoutAccount) {
9639
+ console.error(`[${input.tag}] direction=users-without-account userId=${u.userId.slice(0, 8)} source=${input.usersFile}`);
9640
+ }
9641
+ console.error(`[${input.tag}] check complete divergences=${d.divergences}`);
9642
+ return {
9643
+ divergences: d.divergences,
9644
+ accountWithoutUsers: d.accountWithoutUsers,
9645
+ usersWithoutAccount: d.usersWithoutAccount
9646
+ };
9647
+ }
9616
9648
 
9617
9649
  // server/routes/onboarding.ts
9618
9650
  function hashPin2(pin) {
@@ -9767,7 +9799,8 @@ app9.post("/set-pin", async (c) => {
9767
9799
  role: "owner",
9768
9800
  usersFile: USERS_FILE,
9769
9801
  accountDir: account.accountDir,
9770
- caller: "set-pin"
9802
+ caller: "set-pin",
9803
+ audit: { actor: userId, logFile: USERS_AUDIT_LOG }
9771
9804
  });
9772
9805
  if (result.usersJsonResult !== "ok") {
9773
9806
  console.error(`[set-pin] users.json write failed: ${result.usersError ?? "unknown"}`);
@@ -9826,7 +9859,12 @@ ${body.pin}
9826
9859
  }
9827
9860
  return c.json({ ok: true });
9828
9861
  });
9829
- app9.delete("/set-pin", async (c) => {
9862
+ app9.delete("/set-pin", requireAdminSession, async (c) => {
9863
+ const cacheKey = c.get("cacheKey");
9864
+ const sessionUserId = cacheKey ? getUserIdForSession(cacheKey) : void 0;
9865
+ if (!sessionUserId) {
9866
+ return c.json({ error: "Session has no user binding." }, 401);
9867
+ }
9830
9868
  let users = null;
9831
9869
  try {
9832
9870
  users = readUsersFile2();
@@ -9844,26 +9882,120 @@ app9.delete("/set-pin", async (c) => {
9844
9882
  return c.json({ error: "Invalid request" }, 400);
9845
9883
  }
9846
9884
  if (!body.currentPin) return c.json({ error: "Current PIN required." }, 400);
9847
- const inputHash = hashPin2(body.currentPin);
9848
- const matchedUser = users.find((u) => u.pin === inputHash);
9849
- if (!matchedUser) {
9850
- console.log(`[set-pin] DELETE PIN mismatch: input=${inputHash.slice(0, 8)}\u2026`);
9885
+ const ownRow = users.find((u) => u.userId === sessionUserId);
9886
+ if (!ownRow) {
9887
+ console.log(`[set-pin] DELETE no row for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
9888
+ return c.json({ error: "No PIN configured for this user." }, 404);
9889
+ }
9890
+ if (ownRow.pin !== hashPin2(body.currentPin)) {
9891
+ console.log(`[set-pin] DELETE PIN mismatch for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
9851
9892
  return c.json({ error: "Current PIN is incorrect." }, 401);
9852
9893
  }
9853
- const remaining = users.filter((u) => u.userId !== matchedUser.userId);
9894
+ const remaining = users.filter((u) => u.userId !== sessionUserId);
9854
9895
  if (remaining.length === 0) {
9855
9896
  unlinkSync2(USERS_FILE);
9856
- console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
9897
+ console.log(`[set-pin] cleared users.json (last entry removed): userId=${sessionUserId.slice(0, 8)}\u2026`);
9857
9898
  } else {
9858
9899
  writeFileSync10(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
9859
- console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
9900
+ console.log(`[set-pin] removed entry from users.json: userId=${sessionUserId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
9901
+ }
9902
+ logUsersAudit(
9903
+ { actor: sessionUserId, logFile: USERS_AUDIT_LOG },
9904
+ { action: "remove", field: "row", rowsBefore: formatAuditRowIds(users.map((u) => u.userId)), rowsAfter: formatAuditRowIds(remaining.map((u) => u.userId)) }
9905
+ );
9906
+ return c.json({ ok: true });
9907
+ });
9908
+ app9.put("/set-pin", requireAdminSession, async (c) => {
9909
+ const cacheKey = c.get("cacheKey");
9910
+ const sessionUserId = cacheKey ? getUserIdForSession(cacheKey) : void 0;
9911
+ if (!sessionUserId) {
9912
+ return c.json({ error: "Session has no user binding." }, 401);
9913
+ }
9914
+ let body;
9915
+ try {
9916
+ body = await c.req.json();
9917
+ } catch {
9918
+ return c.json({ error: "Invalid request" }, 400);
9919
+ }
9920
+ if (!body.currentPin) return c.json({ error: "Current PIN required." }, 400);
9921
+ if (!body.newPin || body.newPin.length < 4) {
9922
+ return c.json({ error: "New PIN must be at least 4 characters." }, 400);
9923
+ }
9924
+ let users = null;
9925
+ try {
9926
+ users = readUsersFile2();
9927
+ } catch (err) {
9928
+ console.error(`[set-pin] users.json corrupt on PUT: ${err instanceof Error ? err.message : String(err)}`);
9929
+ return c.json({ error: "User configuration is corrupt." }, 400);
9930
+ }
9931
+ if (users === null || users.length === 0) {
9932
+ return c.json({ error: "No PIN configured." }, 404);
9933
+ }
9934
+ const ownRow = users.find((u) => u.userId === sessionUserId);
9935
+ if (!ownRow) {
9936
+ return c.json({ error: "No PIN configured for this user." }, 404);
9937
+ }
9938
+ if (ownRow.pin !== hashPin2(body.currentPin)) {
9939
+ console.log(`[set-pin] PUT PIN mismatch for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
9940
+ return c.json({ error: "Current PIN is incorrect." }, 401);
9941
+ }
9942
+ const account = resolveAccount();
9943
+ if (!account) {
9944
+ console.error(`[set-pin] PUT no account resolvable, refusing in-place PIN write`);
9945
+ return c.json({ error: "No account is configured on this device. Re-run the installer." }, 503);
9946
+ }
9947
+ const role = cacheKey && getRoleForSession(cacheKey) === "owner" ? "owner" : "admin";
9948
+ const result = writeAdminEntry({
9949
+ userId: sessionUserId,
9950
+ pin: hashPin2(body.newPin),
9951
+ role,
9952
+ usersFile: USERS_FILE,
9953
+ accountDir: account.accountDir,
9954
+ caller: "change-pin",
9955
+ audit: { actor: sessionUserId, logFile: USERS_AUDIT_LOG }
9956
+ });
9957
+ if (result.usersJsonResult !== "ok") {
9958
+ console.error(`[set-pin] PUT users.json write failed: ${result.usersError ?? "unknown"}`);
9959
+ return c.json({ error: "Failed to write user configuration. See server log." }, 500);
9960
+ }
9961
+ console.log(`[set-pin] changed PIN in place: userId=${sessionUserId.slice(0, 8)}\u2026`);
9962
+ if (process.platform === "linux") {
9963
+ let installOwner = null;
9964
+ if (existsSync14(INSTALL_OWNER_FILE)) {
9965
+ try {
9966
+ const raw = readFileSync22(INSTALL_OWNER_FILE, "utf-8").trim();
9967
+ if (raw.length > 0) installOwner = raw;
9968
+ } catch (err) {
9969
+ console.error(`[set-pin] install-owner-read-failed path=${INSTALL_OWNER_FILE} error=${err instanceof Error ? err.message : String(err)}`);
9970
+ }
9971
+ }
9972
+ if (!installOwner) {
9973
+ console.error(`[set-pin] smbpasswd sync skipped owner=<unknown> reason=install-owner-file-missing path=${INSTALL_OWNER_FILE}`);
9974
+ } else {
9975
+ const smbProc = spawnSync2("sudo", ["-n", "smbpasswd", "-a", "-s", installOwner], {
9976
+ input: `${body.newPin}
9977
+ ${body.newPin}
9978
+ `,
9979
+ stdio: ["pipe", "pipe", "pipe"],
9980
+ encoding: "utf-8",
9981
+ timeout: 1e4
9982
+ });
9983
+ if (smbProc.status === 0) {
9984
+ console.log(`[set-pin] smbpasswd sync ok owner=${installOwner} userId=${sessionUserId.slice(0, 8)}\u2026`);
9985
+ } else {
9986
+ const stderr = (smbProc.stderr ?? "").trim().slice(0, 200);
9987
+ console.error(`[set-pin] smbpasswd sync failed owner=${installOwner} rc=${smbProc.status} stderr=${JSON.stringify(stderr)}`);
9988
+ }
9989
+ }
9990
+ } else {
9991
+ console.log(`[set-pin] smb-password-sync-skipped reason=non-linux platform=${process.platform}`);
9860
9992
  }
9861
9993
  return c.json({ ok: true });
9862
9994
  });
9863
9995
  var onboarding_default = app9;
9864
9996
 
9865
9997
  // server/routes/client-error.ts
9866
- import { appendFileSync, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
9998
+ import { appendFileSync as appendFileSync2, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
9867
9999
  import { join as join21 } from "path";
9868
10000
  var CLIENT_ERRORS_LOG = join21(LOG_DIR, "client-errors.log");
9869
10001
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
@@ -10012,7 +10144,7 @@ app10.post("/", async (c) => {
10012
10144
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
10013
10145
  status: typeof body.status === "number" ? body.status : void 0
10014
10146
  };
10015
- appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
10147
+ appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
10016
10148
  } catch (err) {
10017
10149
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
10018
10150
  }
@@ -10511,7 +10643,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync5, createWriteStream
10511
10643
  async function purgeAdminConversationJsonls(args) {
10512
10644
  const fetchImpl = args.fetchImpl ?? fetch;
10513
10645
  const log = args.logger ?? ((line) => console.log(line));
10514
- const id8 = args.sessionId.slice(0, 8);
10646
+ const id82 = args.sessionId.slice(0, 8);
10515
10647
  const purged = [];
10516
10648
  for (const sessionId of args.claudeSessionIds) {
10517
10649
  const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
@@ -10521,7 +10653,7 @@ async function purgeAdminConversationJsonls(args) {
10521
10653
  } catch (err) {
10522
10654
  const message2 = err instanceof Error ? err.message : String(err);
10523
10655
  log(
10524
- `[admin-conversation-delete] sessionId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
10656
+ `[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
10525
10657
  );
10526
10658
  return {
10527
10659
  ok: false,
@@ -10538,7 +10670,7 @@ async function purgeAdminConversationJsonls(args) {
10538
10670
  }
10539
10671
  if (res.status === 409) {
10540
10672
  log(
10541
- `[admin-conversation-delete] sessionId=${id8} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
10673
+ `[admin-conversation-delete] sessionId=${id82} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
10542
10674
  );
10543
10675
  return {
10544
10676
  ok: false,
@@ -10555,7 +10687,7 @@ async function purgeAdminConversationJsonls(args) {
10555
10687
  } catch {
10556
10688
  }
10557
10689
  log(
10558
- `[admin-conversation-delete] sessionId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
10690
+ `[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
10559
10691
  );
10560
10692
  return {
10561
10693
  ok: false,
@@ -10574,13 +10706,13 @@ async function cascadeAdminConversationDelete(args) {
10574
10706
  const started = Date.now();
10575
10707
  const fetchImpl = args.fetchImpl ?? fetch;
10576
10708
  const log = args.logger ?? ((line) => console.log(line));
10577
- const id8 = args.sessionId.slice(0, 8);
10709
+ const id82 = args.sessionId.slice(0, 8);
10578
10710
  let claudeSessionIds;
10579
10711
  try {
10580
10712
  claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
10581
10713
  } catch (err) {
10582
10714
  const message = err instanceof Error ? err.message : String(err);
10583
- log(`[admin-conversation-delete] sessionId=${id8} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
10715
+ log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
10584
10716
  return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
10585
10717
  }
10586
10718
  const purgeOutcome = await purgeAdminConversationJsonls({
@@ -10611,7 +10743,7 @@ async function cascadeAdminConversationDelete(args) {
10611
10743
  } catch (err) {
10612
10744
  const message = err instanceof Error ? err.message : String(err);
10613
10745
  const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
10614
- log(`[admin-conversation-delete] sessionId=${id8} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
10746
+ log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
10615
10747
  return {
10616
10748
  ok: false,
10617
10749
  reason: "graph-delete-failed",
@@ -10624,7 +10756,7 @@ async function cascadeAdminConversationDelete(args) {
10624
10756
  }
10625
10757
  const ms = Date.now() - started;
10626
10758
  const csv = purged.map((s) => s.slice(0, 8)).join(",");
10627
- log(`[admin-conversation-delete] sessionId=${id8} claudeSessionIds=${csv} ms=${ms}`);
10759
+ log(`[admin-conversation-delete] sessionId=${id82} claudeSessionIds=${csv} ms=${ms}`);
10628
10760
  return { ok: true, claudeSessionIds: purged, ms };
10629
10761
  }
10630
10762
 
@@ -16607,955 +16739,336 @@ app38.get("/", async (c) => {
16607
16739
  });
16608
16740
  var public_session_context_default = app38;
16609
16741
 
16610
- // app/lib/channel-pty-bridge/bridge.ts
16611
- import { resolve as resolve26 } from "path";
16612
-
16613
- // app/lib/channel-pty-bridge/public-session-end-review.ts
16614
- import { randomUUID as randomUUID13 } from "crypto";
16615
- var TAG29 = "[public-session-review]";
16616
- var CONSUMED_CAP = 500;
16617
- var consumed = /* @__PURE__ */ new Set();
16618
- function consumeOnce(sessionId) {
16619
- if (consumed.has(sessionId)) return false;
16620
- consumed.add(sessionId);
16621
- if (consumed.size > CONSUMED_CAP) {
16622
- const oldest = consumed.values().next().value;
16623
- if (oldest !== void 0) consumed.delete(oldest);
16624
- }
16625
- return true;
16742
+ // app/lib/webchat/gateway/instance.ts
16743
+ var instance2 = null;
16744
+ function setWebchatGateway(gw) {
16745
+ instance2 = gw;
16626
16746
  }
16627
- function managerBase3() {
16628
- const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
16629
- return `http://127.0.0.1:${port2}`;
16747
+ function getWebchatGateway() {
16748
+ return instance2;
16630
16749
  }
16631
- function uiBase() {
16632
- const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
16633
- return `http://127.0.0.1:${port2}`;
16750
+
16751
+ // server/routes/admin/public-session-exit.ts
16752
+ var TAG29 = "[public-session-exit-route]";
16753
+ function isLoopbackAddr4(addr) {
16754
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16634
16755
  }
16635
- async function fetchJsonl(sessionId) {
16756
+ var app39 = new Hono();
16757
+ app39.post("/", async (c) => {
16758
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16759
+ if (!isLoopbackAddr4(remoteAddr)) {
16760
+ console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16761
+ return c.json({ error: "public-session-exit-loopback-only" }, 403);
16762
+ }
16763
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16764
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16765
+ if (xffRaw.length > 0) {
16766
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16767
+ const offender = tokens.find((t) => !isLoopbackAddr4(t));
16768
+ if (offender !== void 0) {
16769
+ console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16770
+ return c.json({ error: "public-session-exit-loopback-only" }, 403);
16771
+ }
16772
+ }
16773
+ let body;
16636
16774
  try {
16637
- const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
16638
- if (!res.ok) return null;
16639
- return await res.text();
16640
- } catch (err) {
16641
- console.error(
16642
- `${TAG29} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
16643
- );
16644
- return null;
16775
+ body = await c.req.json();
16776
+ } catch {
16777
+ return c.json({ error: "invalid-json" }, 400);
16778
+ }
16779
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
16780
+ if (!sessionId) return c.json({ error: "sessionId required" }, 400);
16781
+ const gateway = getWebchatGateway();
16782
+ if (!gateway) {
16783
+ console.error(`${TAG29} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
16784
+ return c.json({ error: "webchat-gateway-unset" }, 503);
16645
16785
  }
16786
+ gateway.handlePublicSessionExit(sessionId);
16787
+ return c.json({ ok: true });
16788
+ });
16789
+ var public_session_exit_default = app39;
16790
+
16791
+ // server/routes/admin/access-session-evict.ts
16792
+ var TAG30 = "[access-session-evict]";
16793
+ function isLoopbackAddr5(addr) {
16794
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16646
16795
  }
16647
- function countOperatorTurns(jsonl) {
16648
- let count = 0;
16649
- for (const raw of jsonl.split("\n")) {
16650
- const line = raw.trim();
16651
- if (!line) continue;
16652
- try {
16653
- const obj = JSON.parse(line);
16654
- if (obj.type === "user") count += 1;
16655
- } catch {
16796
+ var app40 = new Hono();
16797
+ app40.post("/", async (c) => {
16798
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16799
+ if (!isLoopbackAddr5(remoteAddr)) {
16800
+ console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16801
+ return c.json({ error: "access-session-evict-loopback-only" }, 403);
16802
+ }
16803
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16804
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16805
+ if (xffRaw.length > 0) {
16806
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16807
+ const offender = tokens.find((t) => !isLoopbackAddr5(t));
16808
+ if (offender !== void 0) {
16809
+ console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16810
+ return c.json({ error: "access-session-evict-loopback-only" }, 403);
16656
16811
  }
16657
16812
  }
16658
- return count;
16659
- }
16660
- async function fetchPriorWrites(sliceToken, accountId) {
16661
- const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
16813
+ let body;
16662
16814
  try {
16663
- const res = await fetch(url);
16664
- if (!res.ok) {
16665
- console.error(
16666
- `${TAG29} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
16667
- );
16668
- return [];
16815
+ body = await c.req.json();
16816
+ } catch {
16817
+ return c.json({ error: "invalid-json" }, 400);
16818
+ }
16819
+ const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
16820
+ if (!grantId) return c.json({ error: "grantId required" }, 400);
16821
+ const dropped = evictAccessSessionsByGrant(grantId);
16822
+ console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
16823
+ return c.json({ ok: true, dropped });
16824
+ });
16825
+ var access_session_evict_default = app40;
16826
+
16827
+ // server/routes/admin/enrol-person.ts
16828
+ var TAG31 = "[enrol]";
16829
+ function isLoopbackAddr6(addr) {
16830
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
16831
+ }
16832
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
16833
+ var app41 = new Hono();
16834
+ app41.post("/", async (c) => {
16835
+ const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
16836
+ if (!isLoopbackAddr6(remoteAddr)) {
16837
+ console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
16838
+ return c.json({ error: "enrol-person-loopback-only" }, 403);
16839
+ }
16840
+ const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
16841
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
16842
+ if (xffRaw.length > 0) {
16843
+ const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
16844
+ const offender = tokens.find((t) => !isLoopbackAddr6(t));
16845
+ if (offender !== void 0) {
16846
+ console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
16847
+ return c.json({ error: "enrol-person-loopback-only" }, 403);
16669
16848
  }
16670
- const body = await res.json();
16671
- return Array.isArray(body.writes) ? body.writes : [];
16849
+ }
16850
+ let body;
16851
+ try {
16852
+ body = await c.req.json();
16853
+ } catch {
16854
+ return c.json({ error: "invalid-json" }, 400);
16855
+ }
16856
+ const rawPhone = typeof body.phone === "string" ? body.phone : "";
16857
+ const phone = normalizeE164(rawPhone);
16858
+ if (phone.length <= 1) {
16859
+ return c.json({ error: "invalid-phone" }, 400);
16860
+ }
16861
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
16862
+ if (!EMAIL_RE.test(email)) {
16863
+ return c.json({ error: "invalid-email" }, 400);
16864
+ }
16865
+ const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
16866
+ if (!agentSlug) {
16867
+ return c.json({ error: "agentSlug required" }, 400);
16868
+ }
16869
+ const accountId = process.env.ACCOUNT_ID ?? "";
16870
+ if (!accountId) {
16871
+ console.error(`${TAG31} op=person result=no-account-id`);
16872
+ return c.json({ error: "no-account-id" }, 500);
16873
+ }
16874
+ let personId;
16875
+ try {
16876
+ personId = await enrolPerson({ accountId, phone, email, agentSlug });
16672
16877
  } catch (err) {
16673
16878
  console.error(
16674
- `${TAG29} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
16879
+ `${TAG31} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
16675
16880
  );
16676
- return [];
16881
+ return c.json({ error: "enrol-failed" }, 500);
16677
16882
  }
16678
- }
16679
- function composeInitialMessage(input) {
16680
- const priorJson = JSON.stringify(input.priorWrites, null, 2);
16681
- return [
16682
- `The gated public-agent session for agent "${input.agentSlug}" has just been reaped. Review the transcript and decide what is worth saving to the visitor's per-visitor memory slice. Dispatch \`database-operator\` for each individual write via the Task tool. Do not call memory-write or memory-update directly.`,
16683
- "",
16684
- "<slice-token>",
16685
- input.sliceToken,
16686
- "</slice-token>",
16687
- "",
16688
- "<person-id>",
16689
- input.personId ?? "(unknown)",
16690
- "</person-id>",
16691
- "",
16692
- "<prior-writes>",
16693
- priorJson,
16694
- "</prior-writes>",
16695
- "",
16696
- "<transcript>",
16697
- input.jsonl,
16698
- "</transcript>"
16699
- ].join("\n");
16700
- }
16701
- async function dispatchReviewer(input, initialMessage) {
16702
- const sessionId = randomUUID13();
16703
- console.log(`${TAG29} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
16704
- const spawned = await managerRcSpawn({
16705
- sessionId,
16706
- initialMessage,
16707
- closeAfterTurn: true,
16708
- sliceToken: input.sliceToken,
16709
- personId: input.personId ?? void 0,
16710
- logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
16711
- });
16712
- if ("error" in spawned) {
16713
- return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
16714
- }
16715
- return { ok: true, managerSessionId: spawned.sessionId };
16716
- }
16717
- async function firePublicSessionEndReview(input) {
16718
- const sliceShort = input.sliceToken.slice(0, 8);
16719
- const personField = input.personId ?? "none";
16720
- const dispatchedAt = Date.now();
16721
- if (consumed.has(input.sessionId)) {
16722
- console.log(
16723
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
16883
+ let grant = null;
16884
+ try {
16885
+ const g = await findGrantByContact(email, agentSlug, accountId);
16886
+ if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
16887
+ } catch (err) {
16888
+ console.error(
16889
+ `${TAG31} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
16724
16890
  );
16725
- return;
16726
16891
  }
16727
- const jsonl = await fetchJsonl(input.sessionId);
16728
- if (!jsonl) {
16729
- console.log(
16730
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
16892
+ console.log(
16893
+ `${TAG31} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
16894
+ );
16895
+ return c.json({ personId, grant }, 200);
16896
+ });
16897
+ var enrol_person_default = app41;
16898
+
16899
+ // server/routes/admin/browser.ts
16900
+ var app42 = new Hono();
16901
+ app42.post("/launch", requireAdminSession, async (c) => {
16902
+ try {
16903
+ const transport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
16904
+ console.error(`[admin/browser/launch] op=request transport=${transport}`);
16905
+ if (transport === "vnc") {
16906
+ const vncOk = await ensureVnc();
16907
+ console.error(`[admin/browser/launch] op=vnc-ensured ok=${vncOk}`);
16908
+ if (!vncOk) {
16909
+ console.error("[admin/browser/launch] op=vnc-failed");
16910
+ return c.json({ ok: false, error: "VNC failed to start" }, 502);
16911
+ }
16912
+ }
16913
+ const cdpOk = await ensureCdp(transport);
16914
+ console.error(`[admin/browser/launch] op=cdp-ensured ok=${cdpOk}`);
16915
+ if (!cdpOk) {
16916
+ console.error("[admin/browser/launch] op=cdp-failed");
16917
+ return c.json({ ok: false, error: "Chrome failed to start" }, 502);
16918
+ }
16919
+ console.error(`[admin/browser/launch] op=ready transport=${transport}`);
16920
+ return c.json({ ok: true, transport });
16921
+ } catch (err) {
16922
+ console.error("[admin/browser/launch] Failed to start browser:", err);
16923
+ return c.json(
16924
+ { ok: false, error: err instanceof Error ? err.message : "Unknown error" },
16925
+ 500
16731
16926
  );
16732
- return;
16733
16927
  }
16734
- const operatorTurns = countOperatorTurns(jsonl);
16735
- if (operatorTurns === 0) {
16736
- console.log(
16737
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
16738
- );
16739
- return;
16928
+ });
16929
+ var browser_default = app42;
16930
+
16931
+ // server/routes/admin/calendar.ts
16932
+ import { existsSync as existsSync26 } from "fs";
16933
+ import { join as join28 } from "path";
16934
+
16935
+ // server/lib/calendar-ics.ts
16936
+ var CRLF = "\r\n";
16937
+ var PRODID = "-//Maxy//Calendar//EN";
16938
+ var MAX_LINE_OCTETS = 75;
16939
+ function escapeText(value) {
16940
+ return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
16941
+ }
16942
+ function foldLine(line) {
16943
+ const buf = Buffer.from(line, "utf-8");
16944
+ if (buf.length <= MAX_LINE_OCTETS) return line;
16945
+ const parts = [];
16946
+ let offset = 0;
16947
+ let first = true;
16948
+ while (offset < buf.length) {
16949
+ const maxChunk = first ? MAX_LINE_OCTETS : MAX_LINE_OCTETS - 1;
16950
+ let end = Math.min(offset + maxChunk, buf.length);
16951
+ while (end < buf.length && end > offset && (buf[end] & 192) === 128) end--;
16952
+ const chunk = buf.subarray(offset, end).toString("utf-8");
16953
+ parts.push(first ? chunk : " " + chunk);
16954
+ offset = end;
16955
+ first = false;
16740
16956
  }
16741
- const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
16742
- const initialMessage = composeInitialMessage({
16743
- agentSlug: input.agentSlug,
16744
- sliceToken: input.sliceToken,
16745
- personId: input.personId,
16746
- jsonl,
16747
- priorWrites
16748
- });
16749
- if (!consumeOnce(input.sessionId)) {
16750
- console.log(
16751
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
16752
- );
16753
- return;
16957
+ return parts.join(CRLF);
16958
+ }
16959
+ function toICSDateTime(iso) {
16960
+ const d = new Date(iso);
16961
+ if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
16962
+ const pad = (n) => String(n).padStart(2, "0");
16963
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
16964
+ }
16965
+ function toICSDate(iso) {
16966
+ const d = new Date(iso);
16967
+ if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
16968
+ const pad = (n) => String(n).padStart(2, "0");
16969
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
16970
+ }
16971
+ function attendeeLine(a) {
16972
+ if (!a.email) return null;
16973
+ const cn = a.name ? `;CN=${escapeText(a.name)}` : "";
16974
+ return `ATTENDEE${cn}:mailto:${a.email}`;
16975
+ }
16976
+ function serializeMeetingICS(m, opts) {
16977
+ const now = opts.now ?? /* @__PURE__ */ new Date();
16978
+ const lines = [];
16979
+ lines.push("BEGIN:VCALENDAR");
16980
+ lines.push("VERSION:2.0");
16981
+ lines.push(`PRODID:${PRODID}`);
16982
+ lines.push("CALSCALE:GREGORIAN");
16983
+ lines.push("METHOD:PUBLISH");
16984
+ lines.push("BEGIN:VEVENT");
16985
+ lines.push(`UID:${m.uid ?? m.id}`);
16986
+ lines.push(`DTSTAMP:${toICSDateTime(now.toISOString())}`);
16987
+ if (m.isAllDay) {
16988
+ lines.push(`DTSTART;VALUE=DATE:${toICSDate(m.startsAt)}`);
16989
+ if (m.endsAt) lines.push(`DTEND;VALUE=DATE:${toICSDate(m.endsAt)}`);
16990
+ } else {
16991
+ lines.push(`DTSTART:${toICSDateTime(m.startsAt)}`);
16992
+ if (m.endsAt) lines.push(`DTEND:${toICSDateTime(m.endsAt)}`);
16754
16993
  }
16755
- const dispatched = await dispatchReviewer(input, initialMessage);
16756
- if (!dispatched.ok) {
16757
- console.error(
16758
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
16759
- );
16760
- return;
16994
+ lines.push(`SUMMARY:${escapeText(m.title ?? "")}`);
16995
+ if (m.location) lines.push(`LOCATION:${escapeText(m.location)}`);
16996
+ if (opts.includeAttendees) {
16997
+ for (const a of m.attendees) {
16998
+ const line = attendeeLine(a);
16999
+ if (line) lines.push(line);
17000
+ }
16761
17001
  }
16762
- console.log(
16763
- `${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
16764
- );
17002
+ lines.push("END:VEVENT");
17003
+ lines.push("END:VCALENDAR");
17004
+ return lines.map(foldLine).join(CRLF) + CRLF;
16765
17005
  }
16766
-
16767
- // app/lib/channel-pty-bridge/follower.ts
16768
- function followerPendingMaxMs() {
16769
- return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
17006
+ function countVEvents(ics) {
17007
+ const unfolded = ics.replace(/\r?\n[ \t]/g, "");
17008
+ const lines = unfolded.split(/\r?\n/);
17009
+ let count = 0;
17010
+ let inEvent = false;
17011
+ let nested = 0;
17012
+ for (const raw of lines) {
17013
+ const line = raw.trim();
17014
+ if (line === "BEGIN:VEVENT") {
17015
+ inEvent = true;
17016
+ nested = 0;
17017
+ continue;
17018
+ }
17019
+ if (!inEvent) continue;
17020
+ if (line === "END:VEVENT") {
17021
+ inEvent = false;
17022
+ count++;
17023
+ continue;
17024
+ }
17025
+ if (line.startsWith("BEGIN:")) nested++;
17026
+ else if (line.startsWith("END:") && nested > 0) nested--;
17027
+ }
17028
+ return count;
16770
17029
  }
16771
- function followerRetryMs() {
16772
- return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
16773
- }
16774
- function startFollower(opts) {
16775
- const abort = new AbortController();
16776
- const { entry, tag } = opts;
16777
- void (async () => {
16778
- try {
16779
- const sid = entry.sessionId.slice(0, 8);
16780
- const deadline = Date.now() + followerPendingMaxMs();
16781
- const retryMs = followerRetryMs();
16782
- let res;
16783
- let attempt = 0;
16784
- for (; ; ) {
16785
- res = await fetch(
16786
- managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
16787
- { signal: abort.signal }
16788
- );
16789
- console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
16790
- if (res.status === 202) {
16791
- attempt += 1;
16792
- await res.body?.cancel().catch(() => {
16793
- });
16794
- if (abort.signal.aborted) return;
16795
- if (Date.now() >= deadline) {
16796
- console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
16797
- opts.onError?.("follow-pending-timeout");
16798
- return;
16799
- }
16800
- console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
16801
- await new Promise((r) => setTimeout(r, retryMs));
16802
- if (abort.signal.aborted) return;
16803
- continue;
16804
- }
16805
- break;
16806
- }
16807
- if (!res.ok || !res.body) {
16808
- opts.onError?.(`follow-status-${res.status}`);
16809
- return;
16810
- }
16811
- console.error(`${tag} follower-open sessionId=${sid}`);
16812
- const reader = res.body.getReader();
16813
- const decoder = new TextDecoder("utf8");
16814
- let buffered = "";
16815
- const fileDelivery = opts.fileDelivery ?? null;
16816
- let firedFileTools = [];
16817
- let suppressing = opts.suppressResumeReplay === true;
16818
- let discardedTurns = 0;
16819
- let discardedFileTools = 0;
16820
- while (!abort.signal.aborted) {
16821
- const { value, done } = await reader.read();
16822
- if (done) break;
16823
- buffered += decoder.decode(value, { stream: true });
16824
- let nl = buffered.indexOf("\n");
16825
- while (nl !== -1) {
16826
- const line = buffered.slice(0, nl);
16827
- buffered = buffered.slice(nl + 1);
16828
- nl = buffered.indexOf("\n");
16829
- if (!line) continue;
16830
- let event;
16831
- try {
16832
- event = JSON.parse(line);
16833
- } catch (err) {
16834
- console.error(
16835
- `${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
16836
- );
16837
- continue;
16838
- }
16839
- if (event.type === "__channel_resume_boundary__") {
16840
- if (suppressing) {
16841
- suppressing = false;
16842
- console.error(
16843
- `${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
16844
- );
16845
- }
16846
- continue;
16847
- }
16848
- if (event.type === "user") {
16849
- entry.pendingTurnText = "";
16850
- firedFileTools = [];
16851
- continue;
16852
- }
16853
- if (event.type !== "assistant") continue;
16854
- const msg = event.message;
16855
- if (!msg) continue;
16856
- let sawContent = false;
16857
- if (Array.isArray(msg.content)) {
16858
- for (const block of msg.content) {
16859
- if (block?.type === "text" && typeof block.text === "string") {
16860
- entry.pendingTurnText += block.text;
16861
- sawContent = true;
16862
- } else if (block?.type === "tool_use") {
16863
- sawContent = true;
16864
- if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
16865
- if (suppressing) {
16866
- discardedFileTools += 1;
16867
- continue;
16868
- }
16869
- firedFileTools.push(block.name);
16870
- try {
16871
- await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
16872
- } catch (err) {
16873
- console.error(
16874
- `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
16875
- );
16876
- }
16877
- }
16878
- }
16879
- }
16880
- }
16881
- if (msg.stop_reason === "end_turn") {
16882
- if (suppressing) {
16883
- entry.pendingTurnText = "";
16884
- firedFileTools = [];
16885
- discardedTurns += 1;
16886
- continue;
16887
- }
16888
- const flush = entry.pendingTurnText;
16889
- entry.pendingTurnText = "";
16890
- if (flush.trim()) {
16891
- await fanOut(entry.subscribers, flush, opts.onError, tag);
16892
- }
16893
- if (fileDelivery && firedFileTools.length > 0) {
16894
- const fired = firedFileTools;
16895
- firedFileTools = [];
16896
- try {
16897
- fileDelivery.onTurnEnd(fired);
16898
- } catch (err) {
16899
- console.error(
16900
- `${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
16901
- );
16902
- }
16903
- }
16904
- opts.onTurnComplete?.();
16905
- } else if (sawContent && !suppressing) {
16906
- opts.onActivity?.();
16907
- }
16908
- }
16909
- }
16910
- } catch (err) {
16911
- if (!abort.signal.aborted) {
16912
- opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
16913
- }
16914
- } finally {
16915
- opts.onClose();
16916
- }
16917
- })();
16918
- return abort;
16919
- }
16920
- async function fanOut(subscribers, text, onError, tag) {
16921
- const callbacks = Array.from(subscribers);
16922
- await Promise.all(
16923
- callbacks.map(
16924
- (cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
16925
- const m = err instanceof Error ? err.message : String(err);
16926
- console.error(`${tag} subscriber-error message=${m}`);
16927
- onError?.(`subscriber-error: ${m}`);
16928
- })
16929
- )
16930
- );
16931
- }
16932
-
16933
- // app/lib/channel-pty-bridge/file-delivery.ts
16934
- var SEND_USER_FILE = "SendUserFile";
16935
- function makeFileDelivery(opts) {
16936
- const { entry, tag, channel, sendFile } = opts;
16937
- let failedFiles = [];
16938
- let attempts = 0;
16939
- return {
16940
- isFileDeliveryTool(toolName) {
16941
- return toolName === SEND_USER_FILE;
16942
- },
16943
- async onFileToolUse(use) {
16944
- if (use.toolName !== SEND_USER_FILE) return;
16945
- const input = use.input ?? {};
16946
- const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
16947
- const caption = typeof input.caption === "string" ? input.caption : void 0;
16948
- for (let i = 0; i < files.length; i++) {
16949
- attempts++;
16950
- const result = await sendFile(files[i], i === 0 ? caption : void 0);
16951
- const name = files[i].split("/").pop() ?? files[i];
16952
- console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
16953
- if (!result.ok) failedFiles.push(files[i]);
16954
- }
16955
- },
16956
- onTurnEnd(firedTools) {
16957
- const failed = failedFiles;
16958
- const tried = attempts;
16959
- failedFiles = [];
16960
- attempts = 0;
16961
- const sid = entry.sessionId.slice(0, 8);
16962
- for (const file of failed) {
16963
- console.error(
16964
- `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
16965
- );
16966
- }
16967
- if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
16968
- console.error(
16969
- `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
16970
- );
16971
- }
16972
- }
16973
- };
16974
- }
16975
-
16976
- // app/lib/whatsapp/inbound/file-delivery-bridge.ts
16977
- var TAG30 = "[whatsapp-adaptor]";
16978
- var SEND_USER_FILE2 = "SendUserFile";
16979
- var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
16980
- function platformRoot() {
16981
- return process.env.MAXY_PLATFORM_ROOT || "";
16982
- }
16983
- function makeWhatsAppSendFile(entry) {
16984
- return async (filePath, caption) => {
16985
- let maxyAccountId;
16986
- try {
16987
- maxyAccountId = resolvePlatformAccountId();
16988
- } catch (err) {
16989
- console.error(
16990
- `${TAG30} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
16991
- );
16992
- return { ok: false, error: "account-unresolved" };
16993
- }
16994
- const result = await sendWhatsAppDocument({
16995
- to: entry.senderId,
16996
- filePath,
16997
- caption,
16998
- accountId: entry.accountId,
16999
- maxyAccountId,
17000
- platformRoot: platformRoot()
17001
- });
17002
- if (result.ok) return { ok: true };
17003
- console.error(
17004
- `${TAG30} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
17005
- );
17006
- return { ok: false, error: result.error };
17007
- };
17008
- }
17009
- function makeWhatsAppFileDelivery(entry) {
17010
- const shared = makeFileDelivery({
17011
- entry,
17012
- tag: TAG30,
17013
- channel: "whatsapp",
17014
- sendFile: makeWhatsAppSendFile(entry)
17015
- });
17016
- let turnStartedAt = null;
17017
- let routeCalls = [];
17018
- return {
17019
- isFileDeliveryTool(toolName) {
17020
- return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
17021
- },
17022
- async onFileToolUse(use) {
17023
- if (turnStartedAt === null) turnStartedAt = Date.now();
17024
- if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
17025
- const input = use.input ?? {};
17026
- routeCalls.push({
17027
- to: typeof input.to === "string" ? input.to : void 0,
17028
- filePath: typeof input.filePath === "string" ? input.filePath : void 0
17029
- });
17030
- return;
17031
- }
17032
- await shared.onFileToolUse(use);
17033
- },
17034
- onTurnEnd(firedTools) {
17035
- const startedAt = turnStartedAt ?? 0;
17036
- const routes = routeCalls;
17037
- turnStartedAt = null;
17038
- routeCalls = [];
17039
- const sid = entry.sessionId.slice(0, 8);
17040
- shared.onTurnEnd(firedTools);
17041
- for (const call of routes) {
17042
- const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
17043
- const delivered = routeAt !== void 0 && routeAt >= startedAt;
17044
- if (!delivered) {
17045
- const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
17046
- console.error(
17047
- `${TAG30} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
17048
- );
17049
- }
17050
- }
17051
- }
17052
- };
17053
- }
17054
-
17055
- // app/lib/telegram/outbound/send-document.ts
17056
- import { realpathSync as realpathSync5 } from "fs";
17057
- import { readFile as readFile6, stat as stat6 } from "fs/promises";
17058
- import { resolve as resolve24, basename as basename9 } from "path";
17059
- var TAG31 = "[telegram:outbound]";
17060
- var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
17061
- async function sendTelegramDocument(input) {
17062
- const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
17063
- if (!botToken || !filePath) {
17064
- return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
17065
- }
17066
- if (!maxyAccountId || !platformRoot3) {
17067
- return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
17068
- }
17069
- const accountDir = resolve24(platformRoot3, "..", "data/accounts", maxyAccountId);
17070
- let resolvedPath;
17071
- try {
17072
- resolvedPath = realpathSync5(filePath);
17073
- const accountResolved = realpathSync5(accountDir);
17074
- if (!resolvedPath.startsWith(accountResolved + "/")) {
17075
- console.error(`${TAG31} document REJECTED reason=outside_account_directory`);
17076
- return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
17077
- }
17078
- } catch (err) {
17079
- const code = err.code;
17080
- if (code === "ENOENT") {
17081
- console.error(`${TAG31} document ENOENT path=${filePath}`);
17082
- return { ok: false, status: 404, error: `File not found: ${filePath}` };
17083
- }
17084
- console.error(`${TAG31} document path error: ${String(err)}`);
17085
- return { ok: false, status: 500, error: String(err) };
17086
- }
17087
- const fileStat = await stat6(resolvedPath);
17088
- if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
17089
- return {
17090
- ok: false,
17091
- status: 400,
17092
- error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
17093
- };
17094
- }
17095
- const filename = basename9(resolvedPath);
17096
- const buffer = Buffer.from(await readFile6(resolvedPath));
17097
- const form = new FormData();
17098
- form.append("chat_id", String(chatId));
17099
- if (caption) form.append("caption", caption);
17100
- form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
17101
- let ok = false;
17102
- let messageId;
17103
- let error = "send failed";
17104
- try {
17105
- const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
17106
- method: "POST",
17107
- body: form
17108
- });
17109
- const data = await res.json();
17110
- ok = data.ok;
17111
- messageId = data.result?.message_id;
17112
- if (!data.ok) error = data.description ?? "Unknown Telegram error";
17113
- } catch (e) {
17114
- error = e instanceof Error ? e.message : String(e);
17115
- }
17116
- console.error(
17117
- `${TAG31} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
17118
- );
17119
- return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
17120
- }
17121
-
17122
- // app/lib/telegram/outbound/file-delivery.ts
17123
- var TAG32 = "[telegram:outbound]";
17124
- function platformRoot2() {
17125
- return process.env.MAXY_PLATFORM_ROOT || "";
17126
- }
17127
- function makeTelegramSendFile(entry) {
17128
- let botToken;
17129
- return async (filePath, caption) => {
17130
- if (botToken === void 0) {
17131
- const tg = resolveAccount()?.config.telegram;
17132
- botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
17133
- }
17134
- if (!botToken) {
17135
- console.error(`${TAG32} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
17136
- return { ok: false, error: "no-bot-token" };
17137
- }
17138
- if (entry.replyTarget == null) {
17139
- console.error(`${TAG32} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
17140
- return { ok: false, error: "no-reply-target" };
17141
- }
17142
- let maxyAccountId;
17143
- try {
17144
- maxyAccountId = resolvePlatformAccountId();
17145
- } catch (err) {
17146
- console.error(
17147
- `${TAG32} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
17148
- );
17149
- return { ok: false, error: "account-unresolved" };
17150
- }
17151
- const result = await sendTelegramDocument({
17152
- botToken,
17153
- chatId: Number(entry.replyTarget),
17154
- filePath,
17155
- caption,
17156
- maxyAccountId,
17157
- platformRoot: platformRoot2()
17158
- });
17159
- return result.ok ? { ok: true } : { ok: false, error: result.error };
17160
- };
17161
- }
17162
- function makeTelegramFileDelivery(entry) {
17163
- return makeFileDelivery({
17164
- entry,
17165
- tag: TAG32,
17166
- channel: "telegram",
17167
- sendFile: makeTelegramSendFile(entry)
17168
- });
17169
- }
17170
-
17171
- // app/lib/webchat/file-delivery.ts
17172
- import { realpathSync as realpathSync6 } from "fs";
17173
- import { resolve as resolve25 } from "path";
17174
-
17175
- // app/lib/channel-pty-bridge/bridge.ts
17176
- function tagFor(channel) {
17177
- return `[${channel}-adaptor]`;
17178
- }
17179
- function publicIdleMs() {
17180
- return Number(process.env.CHANNEL_PTY_IDLE_MS ?? String(5 * 6e4));
17181
- }
17182
- var index = /* @__PURE__ */ new Map();
17183
- var reaperHandle = null;
17184
- function startReaper() {
17185
- if (reaperHandle) return;
17186
- reaperHandle = setInterval(() => {
17187
- const now = Date.now();
17188
- const cutoff = now - publicIdleMs();
17189
- for (const [key, entry] of index) {
17190
- if (entry.role !== "public") continue;
17191
- if (entry.subscribers.size > 0) continue;
17192
- if (entry.lastInboundAt > cutoff) continue;
17193
- const idleMs = now - entry.lastInboundAt;
17194
- const tag = tagFor(entry.channel);
17195
- console.error(
17196
- `${tag} reap senderId=${entry.senderId} idle-ms=${idleMs} sessionId=${entry.sessionId.slice(0, 8)}`
17197
- );
17198
- if (entry.followerAbort) entry.followerAbort.abort();
17199
- index.delete(key);
17200
- if (entry.role === "public" && entry.sliceToken.length > 0) {
17201
- void firePublicSessionEndReview({
17202
- sessionId: entry.sessionId,
17203
- sliceToken: entry.sliceToken,
17204
- personId: entry.personId,
17205
- accountId: entry.accountId,
17206
- agentSlug: entry.agentSlug,
17207
- dispatchFor: "eviction"
17208
- });
17209
- }
17210
- void managerDelete(entry.sessionId);
17211
- }
17212
- }, 6e4);
17213
- if (reaperHandle.unref) reaperHandle.unref();
17214
- }
17215
- function handlePublicSessionExit(sessionId) {
17216
- let found = null;
17217
- for (const [key2, entry2] of index) {
17218
- if (entry2.sessionId === sessionId) {
17219
- found = { key: key2, entry: entry2 };
17220
- break;
17221
- }
17222
- }
17223
- if (!found) {
17224
- console.error(`[public-session-exit] sessionId=${sessionId} result=no-entry`);
17225
- return;
17226
- }
17227
- const { key, entry } = found;
17228
- if (entry.role !== "public" || entry.sliceToken.length === 0) {
17229
- return;
17230
- }
17231
- if (entry.followerAbort) entry.followerAbort.abort();
17232
- index.delete(key);
17233
- void firePublicSessionEndReview({
17234
- sessionId: entry.sessionId,
17235
- sliceToken: entry.sliceToken,
17236
- personId: entry.personId,
17237
- accountId: entry.accountId,
17238
- agentSlug: entry.agentSlug,
17239
- dispatchFor: "exit"
17240
- });
17241
- }
17242
-
17243
- // server/routes/admin/public-session-exit.ts
17244
- var TAG33 = "[public-session-exit-route]";
17245
- function isLoopbackAddr4(addr) {
17246
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17247
- }
17248
- var app39 = new Hono();
17249
- app39.post("/", async (c) => {
17250
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17251
- if (!isLoopbackAddr4(remoteAddr)) {
17252
- console.error(`${TAG33} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17253
- return c.json({ error: "public-session-exit-loopback-only" }, 403);
17254
- }
17255
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17256
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17257
- if (xffRaw.length > 0) {
17258
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17259
- const offender = tokens.find((t) => !isLoopbackAddr4(t));
17260
- if (offender !== void 0) {
17261
- console.error(`${TAG33} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17262
- return c.json({ error: "public-session-exit-loopback-only" }, 403);
17263
- }
17264
- }
17265
- let body;
17266
- try {
17267
- body = await c.req.json();
17268
- } catch {
17269
- return c.json({ error: "invalid-json" }, 400);
17270
- }
17271
- const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
17272
- if (!sessionId) return c.json({ error: "sessionId required" }, 400);
17273
- handlePublicSessionExit(sessionId);
17274
- return c.json({ ok: true });
17275
- });
17276
- var public_session_exit_default = app39;
17277
-
17278
- // server/routes/admin/access-session-evict.ts
17279
- var TAG34 = "[access-session-evict]";
17280
- function isLoopbackAddr5(addr) {
17281
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17282
- }
17283
- var app40 = new Hono();
17284
- app40.post("/", async (c) => {
17285
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17286
- if (!isLoopbackAddr5(remoteAddr)) {
17287
- console.error(`${TAG34} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17288
- return c.json({ error: "access-session-evict-loopback-only" }, 403);
17289
- }
17290
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17291
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17292
- if (xffRaw.length > 0) {
17293
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17294
- const offender = tokens.find((t) => !isLoopbackAddr5(t));
17295
- if (offender !== void 0) {
17296
- console.error(`${TAG34} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17297
- return c.json({ error: "access-session-evict-loopback-only" }, 403);
17298
- }
17299
- }
17300
- let body;
17301
- try {
17302
- body = await c.req.json();
17303
- } catch {
17304
- return c.json({ error: "invalid-json" }, 400);
17305
- }
17306
- const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
17307
- if (!grantId) return c.json({ error: "grantId required" }, 400);
17308
- const dropped = evictAccessSessionsByGrant(grantId);
17309
- console.log(`${TAG34} grantId=${grantId} dropped=${dropped}`);
17310
- return c.json({ ok: true, dropped });
17311
- });
17312
- var access_session_evict_default = app40;
17313
-
17314
- // server/routes/admin/enrol-person.ts
17315
- var TAG35 = "[enrol]";
17316
- function isLoopbackAddr6(addr) {
17317
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17318
- }
17319
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17320
- var app41 = new Hono();
17321
- app41.post("/", async (c) => {
17322
- const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17323
- if (!isLoopbackAddr6(remoteAddr)) {
17324
- console.error(`${TAG35} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17325
- return c.json({ error: "enrol-person-loopback-only" }, 403);
17326
- }
17327
- const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17328
- const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17329
- if (xffRaw.length > 0) {
17330
- const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17331
- const offender = tokens.find((t) => !isLoopbackAddr6(t));
17332
- if (offender !== void 0) {
17333
- console.error(`${TAG35} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17334
- return c.json({ error: "enrol-person-loopback-only" }, 403);
17335
- }
17336
- }
17337
- let body;
17338
- try {
17339
- body = await c.req.json();
17340
- } catch {
17341
- return c.json({ error: "invalid-json" }, 400);
17342
- }
17343
- const rawPhone = typeof body.phone === "string" ? body.phone : "";
17344
- const phone = normalizeE164(rawPhone);
17345
- if (phone.length <= 1) {
17346
- return c.json({ error: "invalid-phone" }, 400);
17347
- }
17348
- const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
17349
- if (!EMAIL_RE.test(email)) {
17350
- return c.json({ error: "invalid-email" }, 400);
17351
- }
17352
- const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
17353
- if (!agentSlug) {
17354
- return c.json({ error: "agentSlug required" }, 400);
17355
- }
17356
- const accountId = process.env.ACCOUNT_ID ?? "";
17357
- if (!accountId) {
17358
- console.error(`${TAG35} op=person result=no-account-id`);
17359
- return c.json({ error: "no-account-id" }, 500);
17360
- }
17361
- let personId;
17362
- try {
17363
- personId = await enrolPerson({ accountId, phone, email, agentSlug });
17364
- } catch (err) {
17365
- console.error(
17366
- `${TAG35} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17367
- );
17368
- return c.json({ error: "enrol-failed" }, 500);
17369
- }
17370
- let grant = null;
17371
- try {
17372
- const g = await findGrantByContact(email, agentSlug, accountId);
17373
- if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
17374
- } catch (err) {
17375
- console.error(
17376
- `${TAG35} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17377
- );
17378
- }
17379
- console.log(
17380
- `${TAG35} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
17381
- );
17382
- return c.json({ personId, grant }, 200);
17383
- });
17384
- var enrol_person_default = app41;
17385
-
17386
- // server/routes/admin/browser.ts
17387
- var app42 = new Hono();
17388
- app42.post("/launch", requireAdminSession, async (c) => {
17389
- try {
17390
- const transport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
17391
- console.error(`[admin/browser/launch] op=request transport=${transport}`);
17392
- if (transport === "vnc") {
17393
- const vncOk = await ensureVnc();
17394
- console.error(`[admin/browser/launch] op=vnc-ensured ok=${vncOk}`);
17395
- if (!vncOk) {
17396
- console.error("[admin/browser/launch] op=vnc-failed");
17397
- return c.json({ ok: false, error: "VNC failed to start" }, 502);
17398
- }
17399
- }
17400
- const cdpOk = await ensureCdp(transport);
17401
- console.error(`[admin/browser/launch] op=cdp-ensured ok=${cdpOk}`);
17402
- if (!cdpOk) {
17403
- console.error("[admin/browser/launch] op=cdp-failed");
17404
- return c.json({ ok: false, error: "Chrome failed to start" }, 502);
17405
- }
17406
- console.error(`[admin/browser/launch] op=ready transport=${transport}`);
17407
- return c.json({ ok: true, transport });
17408
- } catch (err) {
17409
- console.error("[admin/browser/launch] Failed to start browser:", err);
17410
- return c.json(
17411
- { ok: false, error: err instanceof Error ? err.message : "Unknown error" },
17412
- 500
17413
- );
17414
- }
17415
- });
17416
- var browser_default = app42;
17417
-
17418
- // server/routes/admin/calendar.ts
17419
- import { existsSync as existsSync26 } from "fs";
17420
- import { join as join28 } from "path";
17421
-
17422
- // server/lib/calendar-ics.ts
17423
- var CRLF = "\r\n";
17424
- var PRODID = "-//Maxy//Calendar//EN";
17425
- var MAX_LINE_OCTETS = 75;
17426
- function escapeText(value) {
17427
- return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
17428
- }
17429
- function foldLine(line) {
17430
- const buf = Buffer.from(line, "utf-8");
17431
- if (buf.length <= MAX_LINE_OCTETS) return line;
17432
- const parts = [];
17433
- let offset = 0;
17434
- let first = true;
17435
- while (offset < buf.length) {
17436
- const maxChunk = first ? MAX_LINE_OCTETS : MAX_LINE_OCTETS - 1;
17437
- let end = Math.min(offset + maxChunk, buf.length);
17438
- while (end < buf.length && end > offset && (buf[end] & 192) === 128) end--;
17439
- const chunk = buf.subarray(offset, end).toString("utf-8");
17440
- parts.push(first ? chunk : " " + chunk);
17441
- offset = end;
17442
- first = false;
17443
- }
17444
- return parts.join(CRLF);
17445
- }
17446
- function toICSDateTime(iso) {
17447
- const d = new Date(iso);
17448
- if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
17449
- const pad = (n) => String(n).padStart(2, "0");
17450
- return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
17451
- }
17452
- function toICSDate(iso) {
17453
- const d = new Date(iso);
17454
- if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
17455
- const pad = (n) => String(n).padStart(2, "0");
17456
- return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
17457
- }
17458
- function attendeeLine(a) {
17459
- if (!a.email) return null;
17460
- const cn = a.name ? `;CN=${escapeText(a.name)}` : "";
17461
- return `ATTENDEE${cn}:mailto:${a.email}`;
17462
- }
17463
- function serializeMeetingICS(m, opts) {
17464
- const now = opts.now ?? /* @__PURE__ */ new Date();
17465
- const lines = [];
17466
- lines.push("BEGIN:VCALENDAR");
17467
- lines.push("VERSION:2.0");
17468
- lines.push(`PRODID:${PRODID}`);
17469
- lines.push("CALSCALE:GREGORIAN");
17470
- lines.push("METHOD:PUBLISH");
17471
- lines.push("BEGIN:VEVENT");
17472
- lines.push(`UID:${m.uid ?? m.id}`);
17473
- lines.push(`DTSTAMP:${toICSDateTime(now.toISOString())}`);
17474
- if (m.isAllDay) {
17475
- lines.push(`DTSTART;VALUE=DATE:${toICSDate(m.startsAt)}`);
17476
- if (m.endsAt) lines.push(`DTEND;VALUE=DATE:${toICSDate(m.endsAt)}`);
17477
- } else {
17478
- lines.push(`DTSTART:${toICSDateTime(m.startsAt)}`);
17479
- if (m.endsAt) lines.push(`DTEND:${toICSDateTime(m.endsAt)}`);
17480
- }
17481
- lines.push(`SUMMARY:${escapeText(m.title ?? "")}`);
17482
- if (m.location) lines.push(`LOCATION:${escapeText(m.location)}`);
17483
- if (opts.includeAttendees) {
17484
- for (const a of m.attendees) {
17485
- const line = attendeeLine(a);
17486
- if (line) lines.push(line);
17487
- }
17488
- }
17489
- lines.push("END:VEVENT");
17490
- lines.push("END:VCALENDAR");
17491
- return lines.map(foldLine).join(CRLF) + CRLF;
17492
- }
17493
- function countVEvents(ics) {
17494
- const unfolded = ics.replace(/\r?\n[ \t]/g, "");
17495
- const lines = unfolded.split(/\r?\n/);
17496
- let count = 0;
17497
- let inEvent = false;
17498
- let nested = 0;
17499
- for (const raw of lines) {
17500
- const line = raw.trim();
17501
- if (line === "BEGIN:VEVENT") {
17502
- inEvent = true;
17503
- nested = 0;
17504
- continue;
17505
- }
17506
- if (!inEvent) continue;
17507
- if (line === "END:VEVENT") {
17508
- inEvent = false;
17509
- count++;
17510
- continue;
17511
- }
17512
- if (line.startsWith("BEGIN:")) nested++;
17513
- else if (line.startsWith("END:") && nested > 0) nested--;
17514
- }
17515
- return count;
17516
- }
17517
-
17518
- // server/lib/calendar-availability.ts
17519
- import { readFileSync as readFileSync28 } from "fs";
17520
- import { join as join27 } from "path";
17521
- var AVAILABILITY_FILENAME = "calendar-availability.json";
17522
- var REQUIRED_KEYS = [
17523
- "timezone",
17524
- "durationMins",
17525
- "bufferMins",
17526
- "weekly"
17527
- ];
17528
- function readAvailabilityConfig(accountDir) {
17529
- const path2 = join27(accountDir, AVAILABILITY_FILENAME);
17530
- let raw;
17531
- try {
17532
- raw = readFileSync28(path2, "utf-8");
17533
- } catch {
17534
- throw new Error(`availability not configured: ${path2} not found`);
17535
- }
17536
- let parsed;
17537
- try {
17538
- parsed = JSON.parse(raw);
17539
- } catch (err) {
17540
- throw new Error(`invalid JSON in ${path2}: ${err.message}`);
17541
- }
17542
- const cfg = parsed;
17543
- for (const key of REQUIRED_KEYS) {
17544
- if (cfg[key] === void 0 || cfg[key] === null) {
17545
- throw new Error(`availability config ${path2} missing required key: ${key}`);
17546
- }
17547
- }
17548
- if (cfg.daysAhead !== void 0 && cfg.daysAhead !== null) {
17549
- if (typeof cfg.daysAhead !== "number" || !Number.isInteger(cfg.daysAhead) || cfg.daysAhead < 1) {
17550
- throw new Error(`availability config ${path2} invalid daysAhead: must be a positive integer`);
17551
- }
17552
- }
17553
- if (cfg.allowMultiSlot !== void 0 && cfg.allowMultiSlot !== null) {
17554
- if (typeof cfg.allowMultiSlot !== "boolean") {
17555
- throw new Error(`availability config ${path2} invalid allowMultiSlot: must be a boolean`);
17556
- }
17557
- }
17558
- return cfg;
17030
+
17031
+ // server/lib/calendar-availability.ts
17032
+ import { readFileSync as readFileSync28 } from "fs";
17033
+ import { join as join27 } from "path";
17034
+ var AVAILABILITY_FILENAME = "calendar-availability.json";
17035
+ var REQUIRED_KEYS = [
17036
+ "timezone",
17037
+ "durationMins",
17038
+ "bufferMins",
17039
+ "weekly"
17040
+ ];
17041
+ function readAvailabilityConfig(accountDir) {
17042
+ const path2 = join27(accountDir, AVAILABILITY_FILENAME);
17043
+ let raw;
17044
+ try {
17045
+ raw = readFileSync28(path2, "utf-8");
17046
+ } catch {
17047
+ throw new Error(`availability not configured: ${path2} not found`);
17048
+ }
17049
+ let parsed;
17050
+ try {
17051
+ parsed = JSON.parse(raw);
17052
+ } catch (err) {
17053
+ throw new Error(`invalid JSON in ${path2}: ${err.message}`);
17054
+ }
17055
+ const cfg = parsed;
17056
+ for (const key of REQUIRED_KEYS) {
17057
+ if (cfg[key] === void 0 || cfg[key] === null) {
17058
+ throw new Error(`availability config ${path2} missing required key: ${key}`);
17059
+ }
17060
+ }
17061
+ if (cfg.daysAhead !== void 0 && cfg.daysAhead !== null) {
17062
+ if (typeof cfg.daysAhead !== "number" || !Number.isInteger(cfg.daysAhead) || cfg.daysAhead < 1) {
17063
+ throw new Error(`availability config ${path2} invalid daysAhead: must be a positive integer`);
17064
+ }
17065
+ }
17066
+ if (cfg.allowMultiSlot !== void 0 && cfg.allowMultiSlot !== null) {
17067
+ if (typeof cfg.allowMultiSlot !== "boolean") {
17068
+ throw new Error(`availability config ${path2} invalid allowMultiSlot: must be a boolean`);
17069
+ }
17070
+ }
17071
+ return cfg;
17559
17072
  }
17560
17073
 
17561
17074
  // server/routes/admin/calendar.ts
@@ -17945,7 +17458,7 @@ app44.route("/calendar", calendar_default);
17945
17458
  var admin_default = app44;
17946
17459
 
17947
17460
  // server/routes/access/verify-token.ts
17948
- var TAG36 = "[access-verify]";
17461
+ var TAG32 = "[access-verify]";
17949
17462
  var MINT_TAG = "[access-session-mint]";
17950
17463
  var COOKIE_NAME = "__access_session";
17951
17464
  var app45 = new Hono();
@@ -17964,39 +17477,39 @@ app45.post("/", async (c) => {
17964
17477
  }
17965
17478
  const rateMsg = checkAccessRateLimit(ip, agentSlug);
17966
17479
  if (rateMsg) {
17967
- console.error(`${TAG36} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
17480
+ console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
17968
17481
  return c.json({ error: rateMsg }, 429);
17969
17482
  }
17970
17483
  const grant = await findGrantByMagicToken(token);
17971
17484
  if (!grant) {
17972
17485
  recordAccessFailedAttempt(ip, agentSlug);
17973
- console.error(`${TAG36} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
17486
+ console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
17974
17487
  return c.json({ error: "invalid-or-expired-link" }, 401);
17975
17488
  }
17976
17489
  if (grant.agentSlug !== agentSlug) {
17977
17490
  recordAccessFailedAttempt(ip, agentSlug);
17978
17491
  console.error(
17979
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
17492
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
17980
17493
  );
17981
17494
  return c.json({ error: "invalid-or-expired-link" }, 401);
17982
17495
  }
17983
17496
  if (grant.status === "expired" || grant.status === "revoked") {
17984
17497
  recordAccessFailedAttempt(ip, agentSlug);
17985
17498
  console.error(
17986
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
17499
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
17987
17500
  );
17988
17501
  return c.json({ error: "access-no-longer-valid" }, 401);
17989
17502
  }
17990
17503
  if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
17991
17504
  recordAccessFailedAttempt(ip, agentSlug);
17992
17505
  console.error(
17993
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
17506
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
17994
17507
  );
17995
17508
  return c.json({ error: "access-no-longer-valid" }, 401);
17996
17509
  }
17997
17510
  if (!grant.sliceToken) {
17998
17511
  console.error(
17999
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
17512
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
18000
17513
  );
18001
17514
  return c.json({ error: "grant-misconfigured" }, 500);
18002
17515
  }
@@ -18012,12 +17525,12 @@ app45.post("/", async (c) => {
18012
17525
  await consumeMagicTokenAndActivate(grant.grantId);
18013
17526
  } catch (err) {
18014
17527
  console.error(
18015
- `${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
17528
+ `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
18016
17529
  );
18017
17530
  return c.json({ error: "verification-failed" }, 500);
18018
17531
  }
18019
17532
  clearAccessRateLimit(ip, agentSlug);
18020
- console.log(`${TAG36} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
17533
+ console.log(`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
18021
17534
  console.log(
18022
17535
  `${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
18023
17536
  );
@@ -18035,9 +17548,9 @@ var verify_token_default = app45;
18035
17548
 
18036
17549
  // app/lib/access-email.ts
18037
17550
  import { spawn as spawn2 } from "child_process";
18038
- import { resolve as resolve27 } from "path";
18039
- var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve27(process.cwd(), "..");
18040
- var SEND_SCRIPT = resolve27(
17551
+ import { resolve as resolve24 } from "path";
17552
+ var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
17553
+ var SEND_SCRIPT = resolve24(
18041
17554
  PLATFORM_ROOT7,
18042
17555
  "plugins",
18043
17556
  "email",
@@ -18093,7 +17606,7 @@ async function sendMagicLinkEmail(payload) {
18093
17606
  }
18094
17607
 
18095
17608
  // server/routes/access/request-magic-link.ts
18096
- var TAG37 = "[access-request-link]";
17609
+ var TAG33 = "[access-request-link]";
18097
17610
  var app46 = new Hono();
18098
17611
  var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
18099
17612
  app46.post("/", async (c) => {
@@ -18110,18 +17623,18 @@ app46.post("/", async (c) => {
18110
17623
  }
18111
17624
  const rateMsg = checkRequestLinkRateLimit(contactValue);
18112
17625
  if (rateMsg) {
18113
- console.error(`${TAG37} contactValue=${maskContact(contactValue)} result=rate-limited`);
17626
+ console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
18114
17627
  return c.json({ error: rateMsg }, 429);
18115
17628
  }
18116
17629
  recordRequestLinkAttempt(contactValue);
18117
17630
  const accountId = process.env.ACCOUNT_ID ?? "";
18118
17631
  if (!accountId) {
18119
- console.error(`${TAG37} contactValue=${maskContact(contactValue)} result=no-account-id`);
17632
+ console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
18120
17633
  return c.json({ message: VISITOR_MESSAGE }, 200);
18121
17634
  }
18122
17635
  const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
18123
17636
  if (!grant) {
18124
- console.log(`${TAG37} contactValue=${maskContact(contactValue)} result=notfound`);
17637
+ console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
18125
17638
  return c.json({ message: VISITOR_MESSAGE }, 200);
18126
17639
  }
18127
17640
  let token;
@@ -18129,7 +17642,7 @@ app46.post("/", async (c) => {
18129
17642
  token = await generateNewMagicToken(grant.grantId);
18130
17643
  } catch (err) {
18131
17644
  console.error(
18132
- `${TAG37} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
17645
+ `${TAG33} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
18133
17646
  );
18134
17647
  return c.json({ message: VISITOR_MESSAGE }, 200);
18135
17648
  }
@@ -18161,12 +17674,12 @@ app46.post("/", async (c) => {
18161
17674
  });
18162
17675
  if (!sendResult.ok) {
18163
17676
  console.error(
18164
- `${TAG37} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
17677
+ `${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18165
17678
  );
18166
17679
  return c.json({ message: VISITOR_MESSAGE }, 200);
18167
17680
  }
18168
17681
  console.log(
18169
- `${TAG37} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
17682
+ `${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18170
17683
  );
18171
17684
  return c.json({ message: VISITOR_MESSAGE }, 200);
18172
17685
  });
@@ -18179,8 +17692,8 @@ app47.route("/request-magic-link", request_magic_link_default);
18179
17692
  var access_default = app47;
18180
17693
 
18181
17694
  // server/routes/sites.ts
18182
- import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync7, statSync as statSync11 } from "fs";
18183
- import { resolve as resolve28 } from "path";
17695
+ import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync5, statSync as statSync11 } from "fs";
17696
+ import { resolve as resolve25 } from "path";
18184
17697
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
18185
17698
  var MIME = {
18186
17699
  ".html": "text/html; charset=utf-8",
@@ -18237,8 +17750,8 @@ app48.get("/:rel{.*}", (c) => {
18237
17750
  }
18238
17751
  segments.push(seg);
18239
17752
  }
18240
- const rootDir = resolve28(account.accountDir, "sites");
18241
- let filePath = segments.length === 0 ? rootDir : resolve28(rootDir, ...segments);
17753
+ const rootDir = resolve25(account.accountDir, "sites");
17754
+ let filePath = segments.length === 0 ? rootDir : resolve25(rootDir, ...segments);
18242
17755
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
18243
17756
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
18244
17757
  return c.text("Forbidden", 403);
@@ -18258,7 +17771,7 @@ app48.get("/:rel{.*}", (c) => {
18258
17771
  return c.redirect(target, 301);
18259
17772
  }
18260
17773
  if (stat8?.isDirectory()) {
18261
- filePath = resolve28(filePath, "index.html");
17774
+ filePath = resolve25(filePath, "index.html");
18262
17775
  }
18263
17776
  if (!filePath.startsWith(rootDir + "/")) {
18264
17777
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
@@ -18271,8 +17784,8 @@ app48.get("/:rel{.*}", (c) => {
18271
17784
  let realPath;
18272
17785
  let realRoot;
18273
17786
  try {
18274
- realPath = realpathSync7(filePath);
18275
- realRoot = realpathSync7(rootDir);
17787
+ realPath = realpathSync5(filePath);
17788
+ realRoot = realpathSync5(rootDir);
18276
17789
  } catch {
18277
17790
  console.error(`[sites] not-found path=${reqPath} status=404`);
18278
17791
  return c.text("Not found", 404);
@@ -18895,14 +18408,14 @@ async function writeEvent(opts) {
18895
18408
  var visitor_event_default = app51;
18896
18409
 
18897
18410
  // server/routes/session.ts
18898
- import { resolve as resolve29 } from "path";
18411
+ import { resolve as resolve26 } from "path";
18899
18412
  import { existsSync as existsSync29, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "fs";
18900
18413
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18901
18414
  function writeBrandingCache(accountId, agentSlug, branding) {
18902
18415
  try {
18903
- const cacheDir = resolve29(MAXY_DIR, "branding-cache", accountId);
18416
+ const cacheDir = resolve26(MAXY_DIR, "branding-cache", accountId);
18904
18417
  mkdirSync7(cacheDir, { recursive: true });
18905
- writeFileSync12(resolve29(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
18418
+ writeFileSync12(resolve26(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
18906
18419
  } catch (err) {
18907
18420
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
18908
18421
  }
@@ -18979,8 +18492,8 @@ app52.post("/", async (c) => {
18979
18492
  }
18980
18493
  let agentConfig = null;
18981
18494
  if (account) {
18982
- const agentDir = resolve29(account.accountDir, "agents", agentSlug);
18983
- if (!existsSync29(agentDir) || !existsSync29(resolve29(agentDir, "config.json"))) {
18495
+ const agentDir = resolve26(account.accountDir, "agents", agentSlug);
18496
+ if (!existsSync29(agentDir) || !existsSync29(resolve26(agentDir, "config.json"))) {
18984
18497
  return c.json({ error: "Agent not found" }, 404);
18985
18498
  }
18986
18499
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -19342,7 +18855,7 @@ function startGraphHealthTimer() {
19342
18855
 
19343
18856
  // app/lib/file-watcher.ts
19344
18857
  import * as fsp2 from "fs/promises";
19345
- import { resolve as resolve30, sep as sep8 } from "path";
18858
+ import { resolve as resolve27, sep as sep8 } from "path";
19346
18859
  var ACCOUNT_UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19347
18860
  var DEFAULT_COALESCE_MS = 500;
19348
18861
  var ROOTS = ["accounts"];
@@ -19361,7 +18874,7 @@ async function startFileWatcher(opts = {}) {
19361
18874
  const dropFn = opts.drop ?? dropFileIndex;
19362
18875
  for (const r of ROOTS) {
19363
18876
  try {
19364
- await fsp2.mkdir(resolve30(dataRoot, r), { recursive: true });
18877
+ await fsp2.mkdir(resolve27(dataRoot, r), { recursive: true });
19365
18878
  } catch (err) {
19366
18879
  console.error(
19367
18880
  `[file-watcher] start-failed root="${r}" err="${err.message}" \u2014 index will be maintained by the 5-min reconcile backstop only`
@@ -19382,7 +18895,7 @@ async function startFileWatcher(opts = {}) {
19382
18895
  timers.set(relativePath, t);
19383
18896
  }
19384
18897
  async function runHook(relativePath, accountId) {
19385
- const absolute = resolve30(dataRoot, relativePath);
18898
+ const absolute = resolve27(dataRoot, relativePath);
19386
18899
  let exists = false;
19387
18900
  try {
19388
18901
  const st = await fsp2.stat(absolute);
@@ -19406,7 +18919,7 @@ async function startFileWatcher(opts = {}) {
19406
18919
  }
19407
18920
  }
19408
18921
  async function watchRoot(rootName) {
19409
- const absRoot = resolve30(dataRoot, rootName);
18922
+ const absRoot = resolve27(dataRoot, rootName);
19410
18923
  try {
19411
18924
  const iter = fsp2.watch(absRoot, { recursive: true, signal: controller.signal });
19412
18925
  for await (const event of iter) {
@@ -19445,7 +18958,7 @@ async function startFileWatcher(opts = {}) {
19445
18958
 
19446
18959
  // app/lib/migrate-uploads.ts
19447
18960
  import { mkdir as mkdir5, readdir as readdir5, rename as rename2, rm as rm3 } from "fs/promises";
19448
- import { dirname as dirname8, relative as relative6, resolve as resolve31 } from "path";
18961
+ import { dirname as dirname8, relative as relative6, resolve as resolve28 } from "path";
19449
18962
  var ACCOUNT_UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19450
18963
  async function walkFiles(dir) {
19451
18964
  const out = [];
@@ -19456,7 +18969,7 @@ async function walkFiles(dir) {
19456
18969
  return out;
19457
18970
  }
19458
18971
  for (const e of entries) {
19459
- const abs = resolve31(dir, e.name);
18972
+ const abs = resolve28(dir, e.name);
19460
18973
  if (e.isDirectory()) {
19461
18974
  out.push(...await walkFiles(abs));
19462
18975
  } else if (e.isFile()) {
@@ -19476,7 +18989,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
19476
18989
  let moved = 0;
19477
18990
  for (const oldAbs of files) {
19478
18991
  const suffix = relative6(srcDir, oldAbs);
19479
- const newAbs = resolve31(destDir, suffix);
18992
+ const newAbs = resolve28(destDir, suffix);
19480
18993
  await mkdir5(dirname8(newAbs), { recursive: true });
19481
18994
  await rename2(oldAbs, newAbs);
19482
18995
  moved++;
@@ -19503,7 +19016,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
19503
19016
  }
19504
19017
  async function migrateUploads(opts = {}) {
19505
19018
  const dataRoot = opts.dataRoot ?? DATA_ROOT;
19506
- const oldRoot = resolve31(dataRoot, "uploads");
19019
+ const oldRoot = resolve28(dataRoot, "uploads");
19507
19020
  let topEntries;
19508
19021
  try {
19509
19022
  topEntries = await readdir5(oldRoot, { withFileTypes: true });
@@ -19524,8 +19037,8 @@ async function migrateUploads(opts = {}) {
19524
19037
  const name = entry.name;
19525
19038
  if (ACCOUNT_UUID_RE4.test(name)) {
19526
19039
  moved += await relocateTree(
19527
- resolve31(oldRoot, name),
19528
- resolve31(dataRoot, "accounts", name, "uploads"),
19040
+ resolve28(oldRoot, name),
19041
+ resolve28(dataRoot, "accounts", name, "uploads"),
19529
19042
  dataRoot,
19530
19043
  name,
19531
19044
  session
@@ -19537,8 +19050,8 @@ async function migrateUploads(opts = {}) {
19537
19050
  continue;
19538
19051
  }
19539
19052
  moved += await relocateTree(
19540
- resolve31(oldRoot, "public"),
19541
- resolve31(dataRoot, "accounts", installAccountId, "uploads", "public"),
19053
+ resolve28(oldRoot, "public"),
19054
+ resolve28(dataRoot, "accounts", installAccountId, "uploads", "public"),
19542
19055
  dataRoot,
19543
19056
  null,
19544
19057
  // public uploads carry no graph nodes
@@ -19567,7 +19080,7 @@ async function migrateUploads(opts = {}) {
19567
19080
  }
19568
19081
 
19569
19082
  // app/lib/migrate-admin-webchat-sidecars.ts
19570
- import { readdir as readdir6, readFile as readFile7, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
19083
+ import { readdir as readdir6, readFile as readFile6, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
19571
19084
  import { existsSync as existsSync30 } from "fs";
19572
19085
  import { join as join30 } from "path";
19573
19086
  var ADMIN_ROLE2 = "admin";
@@ -19580,7 +19093,7 @@ var defaultResolveName = async (accountId, userId) => {
19580
19093
  async function readRaw2(path2) {
19581
19094
  let raw;
19582
19095
  try {
19583
- raw = await readFile7(path2, "utf8");
19096
+ raw = await readFile6(path2, "utf8");
19584
19097
  } catch {
19585
19098
  return null;
19586
19099
  }
@@ -20549,16 +20062,16 @@ var WebchatGateway = class _WebchatGateway {
20549
20062
  * The public /api/chat route awaits this to return the reply on the same SSE
20550
20063
  * response, preserving the pre-756 POST→single-blob browser contract. */
20551
20064
  awaitReply(key, timeoutMs) {
20552
- return new Promise((resolve35) => {
20065
+ return new Promise((resolve33) => {
20553
20066
  const timer2 = setTimeout(() => {
20554
20067
  this.replyAwaiters.delete(key);
20555
- resolve35({ timeout: true });
20068
+ resolve33({ timeout: true });
20556
20069
  }, timeoutMs);
20557
20070
  if (timer2.unref) timer2.unref();
20558
20071
  this.replyAwaiters.set(key, (r) => {
20559
20072
  clearTimeout(timer2);
20560
20073
  this.replyAwaiters.delete(key);
20561
- resolve35(r);
20074
+ resolve33(r);
20562
20075
  });
20563
20076
  });
20564
20077
  }
@@ -20569,11 +20082,11 @@ var WebchatGateway = class _WebchatGateway {
20569
20082
  * terminal dialog stays answerable). */
20570
20083
  awaitPermissionVerdict(p, timeoutMs) {
20571
20084
  const k = _WebchatGateway.promptKey(p.key, p.requestId);
20572
- return new Promise((resolve35) => {
20085
+ return new Promise((resolve33) => {
20573
20086
  this.pendingPrompts.get(k)?.resolve({ timeout: true });
20574
20087
  const timer2 = setTimeout(() => {
20575
20088
  this.pendingPrompts.delete(k);
20576
- resolve35({ timeout: true });
20089
+ resolve33({ timeout: true });
20577
20090
  }, timeoutMs);
20578
20091
  if (timer2.unref) timer2.unref();
20579
20092
  this.pendingPrompts.set(k, {
@@ -20582,7 +20095,7 @@ var WebchatGateway = class _WebchatGateway {
20582
20095
  resolve: (r) => {
20583
20096
  clearTimeout(timer2);
20584
20097
  this.pendingPrompts.delete(k);
20585
- resolve35(r);
20098
+ resolve33(r);
20586
20099
  }
20587
20100
  });
20588
20101
  console.error(`[webchat:perm] op=open key=${keyDisplay(p.key)} id=${p.requestId} tool=${p.toolName}`);
@@ -20654,6 +20167,45 @@ var WebchatGateway = class _WebchatGateway {
20654
20167
  if (handle.unref) handle.unref();
20655
20168
  return handle;
20656
20169
  }
20170
+ /** A gated public PTY exited explicitly on the manager side (operator stop,
20171
+ * claude crash, manager DELETE), not via the idle reaper. The manager POSTs
20172
+ * the sessionId to the loopback /api/admin/public-session-exit route, which
20173
+ * reaches this through the process-wide gateway handle. The slice ringfence
20174
+ * the reviewer needs lives only in this gateway's publicSessions map — the
20175
+ * manager has none of it — so resolve the record here and dispatch the same
20176
+ * reviewer the idle reaper uses, tagged dispatchFor='exit'. Dedup is internal
20177
+ * to firePublicSessionEndReview, so a session already reviewed by the reaper
20178
+ * (or a racing respawn) is a no-op.
20179
+ *
20180
+ * Only gated records (non-empty slice) are reviewed and evicted here, the
20181
+ * same gate the reaper applies. An open-mode public record is left for the
20182
+ * idle reaper. No deleteSession — the PTY is already gone and the JSONL is
20183
+ * the audit record the reviewer just read. */
20184
+ handlePublicSessionExit(sessionId) {
20185
+ let found = null;
20186
+ for (const [key2, rec2] of this.publicSessions) {
20187
+ if (rec2.sessionId === sessionId) {
20188
+ found = { key: key2, rec: rec2 };
20189
+ break;
20190
+ }
20191
+ }
20192
+ if (!found) {
20193
+ console.error(`[webchat:inbound] op=public-exit sessionId=${sessionId.slice(0, 8)} result=no-record`);
20194
+ return;
20195
+ }
20196
+ const { key, rec } = found;
20197
+ if (!rec.sliceToken || rec.sliceToken.length === 0) return;
20198
+ this.publicSessions.delete(key);
20199
+ console.error(`[webchat:inbound] op=public-exit key=${keyDisplay(key)} sessionId=${sessionId.slice(0, 8)} result=review`);
20200
+ void this.deps.firePublicSessionEndReview?.({
20201
+ sessionId: rec.sessionId,
20202
+ sliceToken: rec.sliceToken,
20203
+ personId: rec.personId ?? null,
20204
+ accountId: rec.accountId,
20205
+ agentSlug: rec.agentSlug,
20206
+ dispatchFor: "exit"
20207
+ });
20208
+ }
20657
20209
  /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
20658
20210
  routes() {
20659
20211
  return createWebchatChannelRoutes({
@@ -20770,97 +20322,385 @@ var WebchatGateway = class _WebchatGateway {
20770
20322
  );
20771
20323
  }
20772
20324
  }
20773
- for (const key of this.recoverable.keys()) {
20774
- if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
20775
- }
20776
- if (this.recoveryAttempted.size > 0) {
20777
- const liveIds = this.hub.inFlightIds();
20778
- for (const attemptKey of this.recoveryAttempted) {
20779
- if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
20780
- }
20325
+ for (const key of this.recoverable.keys()) {
20326
+ if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
20327
+ }
20328
+ if (this.recoveryAttempted.size > 0) {
20329
+ const liveIds = this.hub.inFlightIds();
20330
+ for (const attemptKey of this.recoveryAttempted) {
20331
+ if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
20332
+ }
20333
+ }
20334
+ } finally {
20335
+ this.recovering = false;
20336
+ }
20337
+ }
20338
+ /** Handle one inbound webchat message (browser POST → here). */
20339
+ async handleInbound(input) {
20340
+ const bytes = Buffer.byteLength(input.text, "utf8");
20341
+ const hadSubscriber = this.hub.hasSubscriber(input.key);
20342
+ const willDeliverNow = this.hub.isReady(input.key);
20343
+ this.hub.deliver(
20344
+ { key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
20345
+ Date.now()
20346
+ );
20347
+ if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
20348
+ console.error(
20349
+ `[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
20350
+ );
20351
+ if (input.role === "admin") {
20352
+ this.recoverable.set(input.key, {
20353
+ accountId: input.accountId,
20354
+ key: input.key,
20355
+ role: input.role,
20356
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20357
+ ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
20358
+ });
20359
+ }
20360
+ const existing = this.publicSessions.get(input.key);
20361
+ if (existing) existing.lastActivityAt = Date.now();
20362
+ if (!hadSubscriber && !this.spawning.has(input.key)) {
20363
+ this.spawning.add(input.key);
20364
+ console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
20365
+ try {
20366
+ const spawned = await this.deps.ensureChannelSession({
20367
+ accountId: input.accountId,
20368
+ key: input.key,
20369
+ role: input.role,
20370
+ gatewayUrl: this.deps.gatewayUrl,
20371
+ serverPath: this.deps.serverPath,
20372
+ // Task 800 — only present on a session-targeted launch, so the
20373
+ // default-path call shape stays byte-identical.
20374
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20375
+ // The authenticated admin's userId on an admin bootstrap inbound; the
20376
+ // default-path call shape stays byte-identical when absent.
20377
+ ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
20378
+ // Task 756 — public spawn params on a role:'public' inbound.
20379
+ ...input.public !== void 0 ? { public: input.public } : {}
20380
+ });
20381
+ if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
20382
+ this.publicSessions.set(input.key, {
20383
+ sessionId: spawned.sessionId,
20384
+ accountId: input.accountId,
20385
+ agentSlug: input.public.agentSlug,
20386
+ sliceToken: input.public.sliceToken,
20387
+ personId: input.public.personId,
20388
+ lastActivityAt: Date.now()
20389
+ });
20390
+ }
20391
+ } finally {
20392
+ this.spawning.delete(input.key);
20393
+ }
20394
+ }
20395
+ }
20396
+ };
20397
+
20398
+ // app/lib/webchat/gateway/public-spawn-request.ts
20399
+ function buildPublicWebchatSpawnRequest(input) {
20400
+ const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
20401
+ return {
20402
+ senderId: input.key,
20403
+ role: "public",
20404
+ channel: "webchat",
20405
+ accountId: input.accountId,
20406
+ agentSlug,
20407
+ model: SONNET_MODEL,
20408
+ // sliceToken/personId are present only for a gated visitor; open-mode
20409
+ // public agents carry neither (mirrors the bridge's open-mode key).
20410
+ ...sliceToken ? { sliceToken } : {},
20411
+ ...personId ? { personId } : {},
20412
+ ...visitorId ? { visitorId } : {},
20413
+ ...input.previousContext ? { previousContext: input.previousContext } : {},
20414
+ name,
20415
+ webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
20416
+ };
20417
+ }
20418
+
20419
+ // app/lib/channel-pty-bridge/follower.ts
20420
+ function followerPendingMaxMs() {
20421
+ return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
20422
+ }
20423
+ function followerRetryMs() {
20424
+ return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
20425
+ }
20426
+ function startFollower(opts) {
20427
+ const abort = new AbortController();
20428
+ const { entry, tag } = opts;
20429
+ void (async () => {
20430
+ try {
20431
+ const sid = entry.sessionId.slice(0, 8);
20432
+ const deadline = Date.now() + followerPendingMaxMs();
20433
+ const retryMs = followerRetryMs();
20434
+ let res;
20435
+ let attempt = 0;
20436
+ for (; ; ) {
20437
+ res = await fetch(
20438
+ managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
20439
+ { signal: abort.signal }
20440
+ );
20441
+ console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
20442
+ if (res.status === 202) {
20443
+ attempt += 1;
20444
+ await res.body?.cancel().catch(() => {
20445
+ });
20446
+ if (abort.signal.aborted) return;
20447
+ if (Date.now() >= deadline) {
20448
+ console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
20449
+ opts.onError?.("follow-pending-timeout");
20450
+ return;
20451
+ }
20452
+ console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
20453
+ await new Promise((r) => setTimeout(r, retryMs));
20454
+ if (abort.signal.aborted) return;
20455
+ continue;
20456
+ }
20457
+ break;
20458
+ }
20459
+ if (!res.ok || !res.body) {
20460
+ opts.onError?.(`follow-status-${res.status}`);
20461
+ return;
20462
+ }
20463
+ console.error(`${tag} follower-open sessionId=${sid}`);
20464
+ const reader = res.body.getReader();
20465
+ const decoder = new TextDecoder("utf8");
20466
+ let buffered = "";
20467
+ const fileDelivery = opts.fileDelivery ?? null;
20468
+ let firedFileTools = [];
20469
+ let suppressing = opts.suppressResumeReplay === true;
20470
+ let discardedTurns = 0;
20471
+ let discardedFileTools = 0;
20472
+ while (!abort.signal.aborted) {
20473
+ const { value, done } = await reader.read();
20474
+ if (done) break;
20475
+ buffered += decoder.decode(value, { stream: true });
20476
+ let nl = buffered.indexOf("\n");
20477
+ while (nl !== -1) {
20478
+ const line = buffered.slice(0, nl);
20479
+ buffered = buffered.slice(nl + 1);
20480
+ nl = buffered.indexOf("\n");
20481
+ if (!line) continue;
20482
+ let event;
20483
+ try {
20484
+ event = JSON.parse(line);
20485
+ } catch (err) {
20486
+ console.error(
20487
+ `${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
20488
+ );
20489
+ continue;
20490
+ }
20491
+ if (event.type === "__channel_resume_boundary__") {
20492
+ if (suppressing) {
20493
+ suppressing = false;
20494
+ console.error(
20495
+ `${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
20496
+ );
20497
+ }
20498
+ continue;
20499
+ }
20500
+ if (event.type === "user") {
20501
+ entry.pendingTurnText = "";
20502
+ firedFileTools = [];
20503
+ continue;
20504
+ }
20505
+ if (event.type !== "assistant") continue;
20506
+ const msg = event.message;
20507
+ if (!msg) continue;
20508
+ let sawContent = false;
20509
+ if (Array.isArray(msg.content)) {
20510
+ for (const block of msg.content) {
20511
+ if (block?.type === "text" && typeof block.text === "string") {
20512
+ entry.pendingTurnText += block.text;
20513
+ sawContent = true;
20514
+ } else if (block?.type === "tool_use") {
20515
+ sawContent = true;
20516
+ if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
20517
+ if (suppressing) {
20518
+ discardedFileTools += 1;
20519
+ continue;
20520
+ }
20521
+ firedFileTools.push(block.name);
20522
+ try {
20523
+ await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
20524
+ } catch (err) {
20525
+ console.error(
20526
+ `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
20527
+ );
20528
+ }
20529
+ }
20530
+ }
20531
+ }
20532
+ }
20533
+ if (msg.stop_reason === "end_turn") {
20534
+ if (suppressing) {
20535
+ entry.pendingTurnText = "";
20536
+ firedFileTools = [];
20537
+ discardedTurns += 1;
20538
+ continue;
20539
+ }
20540
+ const flush = entry.pendingTurnText;
20541
+ entry.pendingTurnText = "";
20542
+ if (flush.trim()) {
20543
+ await fanOut(entry.subscribers, flush, opts.onError, tag);
20544
+ }
20545
+ if (fileDelivery && firedFileTools.length > 0) {
20546
+ const fired = firedFileTools;
20547
+ firedFileTools = [];
20548
+ try {
20549
+ fileDelivery.onTurnEnd(fired);
20550
+ } catch (err) {
20551
+ console.error(
20552
+ `${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
20553
+ );
20554
+ }
20555
+ }
20556
+ opts.onTurnComplete?.();
20557
+ } else if (sawContent && !suppressing) {
20558
+ opts.onActivity?.();
20559
+ }
20560
+ }
20561
+ }
20562
+ } catch (err) {
20563
+ if (!abort.signal.aborted) {
20564
+ opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
20781
20565
  }
20782
20566
  } finally {
20783
- this.recovering = false;
20784
- }
20785
- }
20786
- /** Handle one inbound webchat message (browser POST → here). */
20787
- async handleInbound(input) {
20788
- const bytes = Buffer.byteLength(input.text, "utf8");
20789
- const hadSubscriber = this.hub.hasSubscriber(input.key);
20790
- const willDeliverNow = this.hub.isReady(input.key);
20791
- this.hub.deliver(
20792
- { key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
20793
- Date.now()
20794
- );
20795
- if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
20796
- console.error(
20797
- `[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
20798
- );
20799
- if (input.role === "admin") {
20800
- this.recoverable.set(input.key, {
20801
- accountId: input.accountId,
20802
- key: input.key,
20803
- role: input.role,
20804
- ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20805
- ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
20806
- });
20567
+ opts.onClose();
20807
20568
  }
20808
- const existing = this.publicSessions.get(input.key);
20809
- if (existing) existing.lastActivityAt = Date.now();
20810
- if (!hadSubscriber && !this.spawning.has(input.key)) {
20811
- this.spawning.add(input.key);
20812
- console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
20813
- try {
20814
- const spawned = await this.deps.ensureChannelSession({
20815
- accountId: input.accountId,
20816
- key: input.key,
20817
- role: input.role,
20818
- gatewayUrl: this.deps.gatewayUrl,
20819
- serverPath: this.deps.serverPath,
20820
- // Task 800 — only present on a session-targeted launch, so the
20821
- // default-path call shape stays byte-identical.
20822
- ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
20823
- // The authenticated admin's userId on an admin bootstrap inbound; the
20824
- // default-path call shape stays byte-identical when absent.
20825
- ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
20826
- // Task 756 — public spawn params on a role:'public' inbound.
20827
- ...input.public !== void 0 ? { public: input.public } : {}
20828
- });
20829
- if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
20830
- this.publicSessions.set(input.key, {
20831
- sessionId: spawned.sessionId,
20832
- accountId: input.accountId,
20833
- agentSlug: input.public.agentSlug,
20834
- sliceToken: input.public.sliceToken,
20835
- personId: input.public.personId,
20836
- lastActivityAt: Date.now()
20837
- });
20838
- }
20839
- } finally {
20840
- this.spawning.delete(input.key);
20569
+ })();
20570
+ return abort;
20571
+ }
20572
+ async function fanOut(subscribers, text, onError, tag) {
20573
+ const callbacks = Array.from(subscribers);
20574
+ await Promise.all(
20575
+ callbacks.map(
20576
+ (cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
20577
+ const m = err instanceof Error ? err.message : String(err);
20578
+ console.error(`${tag} subscriber-error message=${m}`);
20579
+ onError?.(`subscriber-error: ${m}`);
20580
+ })
20581
+ )
20582
+ );
20583
+ }
20584
+
20585
+ // app/lib/channel-pty-bridge/file-delivery.ts
20586
+ var SEND_USER_FILE = "SendUserFile";
20587
+ function makeFileDelivery(opts) {
20588
+ const { entry, tag, channel, sendFile } = opts;
20589
+ let failedFiles = [];
20590
+ let attempts = 0;
20591
+ return {
20592
+ isFileDeliveryTool(toolName) {
20593
+ return toolName === SEND_USER_FILE;
20594
+ },
20595
+ async onFileToolUse(use) {
20596
+ if (use.toolName !== SEND_USER_FILE) return;
20597
+ const input = use.input ?? {};
20598
+ const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
20599
+ const caption = typeof input.caption === "string" ? input.caption : void 0;
20600
+ for (let i = 0; i < files.length; i++) {
20601
+ attempts++;
20602
+ const result = await sendFile(files[i], i === 0 ? caption : void 0);
20603
+ const name = files[i].split("/").pop() ?? files[i];
20604
+ console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
20605
+ if (!result.ok) failedFiles.push(files[i]);
20606
+ }
20607
+ },
20608
+ onTurnEnd(firedTools) {
20609
+ const failed = failedFiles;
20610
+ const tried = attempts;
20611
+ failedFiles = [];
20612
+ attempts = 0;
20613
+ const sid = entry.sessionId.slice(0, 8);
20614
+ for (const file of failed) {
20615
+ console.error(
20616
+ `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
20617
+ );
20618
+ }
20619
+ if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
20620
+ console.error(
20621
+ `${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
20622
+ );
20841
20623
  }
20842
20624
  }
20843
- }
20844
- };
20625
+ };
20626
+ }
20845
20627
 
20846
- // app/lib/webchat/gateway/public-spawn-request.ts
20847
- function buildPublicWebchatSpawnRequest(input) {
20848
- const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
20628
+ // app/lib/whatsapp/inbound/file-delivery-bridge.ts
20629
+ var TAG34 = "[whatsapp-adaptor]";
20630
+ var SEND_USER_FILE2 = "SendUserFile";
20631
+ var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
20632
+ function platformRoot() {
20633
+ return process.env.MAXY_PLATFORM_ROOT || "";
20634
+ }
20635
+ function makeWhatsAppSendFile(entry) {
20636
+ return async (filePath, caption) => {
20637
+ let maxyAccountId;
20638
+ try {
20639
+ maxyAccountId = resolvePlatformAccountId();
20640
+ } catch (err) {
20641
+ console.error(
20642
+ `${TAG34} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
20643
+ );
20644
+ return { ok: false, error: "account-unresolved" };
20645
+ }
20646
+ const result = await sendWhatsAppDocument({
20647
+ to: entry.senderId,
20648
+ filePath,
20649
+ caption,
20650
+ accountId: entry.accountId,
20651
+ maxyAccountId,
20652
+ platformRoot: platformRoot()
20653
+ });
20654
+ if (result.ok) return { ok: true };
20655
+ console.error(
20656
+ `${TAG34} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
20657
+ );
20658
+ return { ok: false, error: result.error };
20659
+ };
20660
+ }
20661
+ function makeWhatsAppFileDelivery(entry) {
20662
+ const shared = makeFileDelivery({
20663
+ entry,
20664
+ tag: TAG34,
20665
+ channel: "whatsapp",
20666
+ sendFile: makeWhatsAppSendFile(entry)
20667
+ });
20668
+ let turnStartedAt = null;
20669
+ let routeCalls = [];
20849
20670
  return {
20850
- senderId: input.key,
20851
- role: "public",
20852
- channel: "webchat",
20853
- accountId: input.accountId,
20854
- agentSlug,
20855
- model: SONNET_MODEL,
20856
- // sliceToken/personId are present only for a gated visitor; open-mode
20857
- // public agents carry neither (mirrors the bridge's open-mode key).
20858
- ...sliceToken ? { sliceToken } : {},
20859
- ...personId ? { personId } : {},
20860
- ...visitorId ? { visitorId } : {},
20861
- ...input.previousContext ? { previousContext: input.previousContext } : {},
20862
- name,
20863
- webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
20671
+ isFileDeliveryTool(toolName) {
20672
+ return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
20673
+ },
20674
+ async onFileToolUse(use) {
20675
+ if (turnStartedAt === null) turnStartedAt = Date.now();
20676
+ if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
20677
+ const input = use.input ?? {};
20678
+ routeCalls.push({
20679
+ to: typeof input.to === "string" ? input.to : void 0,
20680
+ filePath: typeof input.filePath === "string" ? input.filePath : void 0
20681
+ });
20682
+ return;
20683
+ }
20684
+ await shared.onFileToolUse(use);
20685
+ },
20686
+ onTurnEnd(firedTools) {
20687
+ const startedAt = turnStartedAt ?? 0;
20688
+ const routes = routeCalls;
20689
+ turnStartedAt = null;
20690
+ routeCalls = [];
20691
+ const sid = entry.sessionId.slice(0, 8);
20692
+ shared.onTurnEnd(firedTools);
20693
+ for (const call of routes) {
20694
+ const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
20695
+ const delivered = routeAt !== void 0 && routeAt >= startedAt;
20696
+ if (!delivered) {
20697
+ const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
20698
+ console.error(
20699
+ `${TAG34} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
20700
+ );
20701
+ }
20702
+ }
20703
+ }
20864
20704
  };
20865
20705
  }
20866
20706
 
@@ -21211,6 +21051,122 @@ function buildTelegramSpawnRequest(input) {
21211
21051
  };
21212
21052
  }
21213
21053
 
21054
+ // app/lib/telegram/outbound/send-document.ts
21055
+ import { realpathSync as realpathSync6 } from "fs";
21056
+ import { readFile as readFile7, stat as stat7 } from "fs/promises";
21057
+ import { resolve as resolve29, basename as basename9 } from "path";
21058
+ var TAG35 = "[telegram:outbound]";
21059
+ var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
21060
+ async function sendTelegramDocument(input) {
21061
+ const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
21062
+ if (!botToken || !filePath) {
21063
+ return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
21064
+ }
21065
+ if (!maxyAccountId || !platformRoot3) {
21066
+ return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
21067
+ }
21068
+ const accountDir = resolve29(platformRoot3, "..", "data/accounts", maxyAccountId);
21069
+ let resolvedPath;
21070
+ try {
21071
+ resolvedPath = realpathSync6(filePath);
21072
+ const accountResolved = realpathSync6(accountDir);
21073
+ if (!resolvedPath.startsWith(accountResolved + "/")) {
21074
+ console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
21075
+ return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
21076
+ }
21077
+ } catch (err) {
21078
+ const code = err.code;
21079
+ if (code === "ENOENT") {
21080
+ console.error(`${TAG35} document ENOENT path=${filePath}`);
21081
+ return { ok: false, status: 404, error: `File not found: ${filePath}` };
21082
+ }
21083
+ console.error(`${TAG35} document path error: ${String(err)}`);
21084
+ return { ok: false, status: 500, error: String(err) };
21085
+ }
21086
+ const fileStat = await stat7(resolvedPath);
21087
+ if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
21088
+ return {
21089
+ ok: false,
21090
+ status: 400,
21091
+ error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
21092
+ };
21093
+ }
21094
+ const filename = basename9(resolvedPath);
21095
+ const buffer = Buffer.from(await readFile7(resolvedPath));
21096
+ const form = new FormData();
21097
+ form.append("chat_id", String(chatId));
21098
+ if (caption) form.append("caption", caption);
21099
+ form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
21100
+ let ok = false;
21101
+ let messageId;
21102
+ let error = "send failed";
21103
+ try {
21104
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
21105
+ method: "POST",
21106
+ body: form
21107
+ });
21108
+ const data = await res.json();
21109
+ ok = data.ok;
21110
+ messageId = data.result?.message_id;
21111
+ if (!data.ok) error = data.description ?? "Unknown Telegram error";
21112
+ } catch (e) {
21113
+ error = e instanceof Error ? e.message : String(e);
21114
+ }
21115
+ console.error(
21116
+ `${TAG35} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
21117
+ );
21118
+ return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
21119
+ }
21120
+
21121
+ // app/lib/telegram/outbound/file-delivery.ts
21122
+ var TAG36 = "[telegram:outbound]";
21123
+ function platformRoot2() {
21124
+ return process.env.MAXY_PLATFORM_ROOT || "";
21125
+ }
21126
+ function makeTelegramSendFile(entry) {
21127
+ let botToken;
21128
+ return async (filePath, caption) => {
21129
+ if (botToken === void 0) {
21130
+ const tg = resolveAccount()?.config.telegram;
21131
+ botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
21132
+ }
21133
+ if (!botToken) {
21134
+ console.error(`${TAG36} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
21135
+ return { ok: false, error: "no-bot-token" };
21136
+ }
21137
+ if (entry.replyTarget == null) {
21138
+ console.error(`${TAG36} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
21139
+ return { ok: false, error: "no-reply-target" };
21140
+ }
21141
+ let maxyAccountId;
21142
+ try {
21143
+ maxyAccountId = resolvePlatformAccountId();
21144
+ } catch (err) {
21145
+ console.error(
21146
+ `${TAG36} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
21147
+ );
21148
+ return { ok: false, error: "account-unresolved" };
21149
+ }
21150
+ const result = await sendTelegramDocument({
21151
+ botToken,
21152
+ chatId: Number(entry.replyTarget),
21153
+ filePath,
21154
+ caption,
21155
+ maxyAccountId,
21156
+ platformRoot: platformRoot2()
21157
+ });
21158
+ return result.ok ? { ok: true } : { ok: false, error: result.error };
21159
+ };
21160
+ }
21161
+ function makeTelegramFileDelivery(entry) {
21162
+ return makeFileDelivery({
21163
+ entry,
21164
+ tag: TAG36,
21165
+ channel: "telegram",
21166
+ sendFile: makeTelegramSendFile(entry)
21167
+ });
21168
+ }
21169
+
21214
21170
  // app/lib/telegram/gateway/native-file-follower.ts
21215
21171
  function startTelegramNativeFileFollower(input) {
21216
21172
  const entry = {
@@ -21245,12 +21201,166 @@ function startTelegramNativeFileFollower(input) {
21245
21201
  }
21246
21202
 
21247
21203
  // server/telegram-descriptor.ts
21248
- import { resolve as resolve32, join as join31 } from "path";
21204
+ import { resolve as resolve30, join as join31 } from "path";
21249
21205
  function telegramGatewayUrl() {
21250
21206
  return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
21251
21207
  }
21252
21208
  function telegramServerPath() {
21253
- return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
21209
+ return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve30(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
21210
+ }
21211
+
21212
+ // app/lib/channel-pty-bridge/public-session-end-review.ts
21213
+ import { randomUUID as randomUUID13 } from "crypto";
21214
+ var TAG37 = "[public-session-review]";
21215
+ var CONSUMED_CAP = 500;
21216
+ var consumed = /* @__PURE__ */ new Set();
21217
+ function consumeOnce(sessionId) {
21218
+ if (consumed.has(sessionId)) return false;
21219
+ consumed.add(sessionId);
21220
+ if (consumed.size > CONSUMED_CAP) {
21221
+ const oldest = consumed.values().next().value;
21222
+ if (oldest !== void 0) consumed.delete(oldest);
21223
+ }
21224
+ return true;
21225
+ }
21226
+ function managerBase3() {
21227
+ const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
21228
+ return `http://127.0.0.1:${port2}`;
21229
+ }
21230
+ function uiBase() {
21231
+ const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
21232
+ return `http://127.0.0.1:${port2}`;
21233
+ }
21234
+ async function fetchJsonl(sessionId) {
21235
+ try {
21236
+ const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
21237
+ if (!res.ok) return null;
21238
+ return await res.text();
21239
+ } catch (err) {
21240
+ console.error(
21241
+ `${TAG37} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
21242
+ );
21243
+ return null;
21244
+ }
21245
+ }
21246
+ function countOperatorTurns(jsonl) {
21247
+ let count = 0;
21248
+ for (const raw of jsonl.split("\n")) {
21249
+ const line = raw.trim();
21250
+ if (!line) continue;
21251
+ try {
21252
+ const obj = JSON.parse(line);
21253
+ if (obj.type === "user") count += 1;
21254
+ } catch {
21255
+ }
21256
+ }
21257
+ return count;
21258
+ }
21259
+ async function fetchPriorWrites(sliceToken, accountId) {
21260
+ const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
21261
+ try {
21262
+ const res = await fetch(url);
21263
+ if (!res.ok) {
21264
+ console.error(
21265
+ `${TAG37} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
21266
+ );
21267
+ return [];
21268
+ }
21269
+ const body = await res.json();
21270
+ return Array.isArray(body.writes) ? body.writes : [];
21271
+ } catch (err) {
21272
+ console.error(
21273
+ `${TAG37} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
21274
+ );
21275
+ return [];
21276
+ }
21277
+ }
21278
+ function composeInitialMessage(input) {
21279
+ const priorJson = JSON.stringify(input.priorWrites, null, 2);
21280
+ return [
21281
+ `The gated public-agent session for agent "${input.agentSlug}" has just been reaped. Review the transcript and decide what is worth saving to the visitor's per-visitor memory slice. Dispatch \`database-operator\` for each individual write via the Task tool. Do not call memory-write or memory-update directly.`,
21282
+ "",
21283
+ "<slice-token>",
21284
+ input.sliceToken,
21285
+ "</slice-token>",
21286
+ "",
21287
+ "<person-id>",
21288
+ input.personId ?? "(unknown)",
21289
+ "</person-id>",
21290
+ "",
21291
+ "<prior-writes>",
21292
+ priorJson,
21293
+ "</prior-writes>",
21294
+ "",
21295
+ "<transcript>",
21296
+ input.jsonl,
21297
+ "</transcript>"
21298
+ ].join("\n");
21299
+ }
21300
+ async function dispatchReviewer(input, initialMessage) {
21301
+ const sessionId = randomUUID13();
21302
+ console.log(`${TAG37} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
21303
+ const spawned = await managerRcSpawn({
21304
+ sessionId,
21305
+ initialMessage,
21306
+ closeAfterTurn: true,
21307
+ sliceToken: input.sliceToken,
21308
+ personId: input.personId ?? void 0,
21309
+ logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
21310
+ });
21311
+ if ("error" in spawned) {
21312
+ return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
21313
+ }
21314
+ return { ok: true, managerSessionId: spawned.sessionId };
21315
+ }
21316
+ async function firePublicSessionEndReview(input) {
21317
+ const sliceShort = input.sliceToken.slice(0, 8);
21318
+ const personField = input.personId ?? "none";
21319
+ const dispatchedAt = Date.now();
21320
+ if (consumed.has(input.sessionId)) {
21321
+ console.log(
21322
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
21323
+ );
21324
+ return;
21325
+ }
21326
+ const jsonl = await fetchJsonl(input.sessionId);
21327
+ if (!jsonl) {
21328
+ console.log(
21329
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
21330
+ );
21331
+ return;
21332
+ }
21333
+ const operatorTurns = countOperatorTurns(jsonl);
21334
+ if (operatorTurns === 0) {
21335
+ console.log(
21336
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
21337
+ );
21338
+ return;
21339
+ }
21340
+ const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
21341
+ const initialMessage = composeInitialMessage({
21342
+ agentSlug: input.agentSlug,
21343
+ sliceToken: input.sliceToken,
21344
+ personId: input.personId,
21345
+ jsonl,
21346
+ priorWrites
21347
+ });
21348
+ if (!consumeOnce(input.sessionId)) {
21349
+ console.log(
21350
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
21351
+ );
21352
+ return;
21353
+ }
21354
+ const dispatched = await dispatchReviewer(input, initialMessage);
21355
+ if (!dispatched.ok) {
21356
+ console.error(
21357
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
21358
+ );
21359
+ return;
21360
+ }
21361
+ console.log(
21362
+ `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
21363
+ );
21254
21364
  }
21255
21365
 
21256
21366
  // app/lib/whatsapp/inbound/channel-admin-binding-drift.ts
@@ -21319,7 +21429,7 @@ function broadcastAdminShutdown(reason) {
21319
21429
  // ../lib/entitlement/src/index.ts
21320
21430
  import { createPublicKey, createHash as createHash5, verify as cryptoVerify } from "crypto";
21321
21431
  import { existsSync as existsSync31, readFileSync as readFileSync32, statSync as statSync12 } from "fs";
21322
- import { resolve as resolve33 } from "path";
21432
+ import { resolve as resolve31 } from "path";
21323
21433
 
21324
21434
  // ../lib/entitlement/src/canonicalize.ts
21325
21435
  function canonicalize(value) {
@@ -21354,7 +21464,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
21354
21464
  var GRACE_DAYS = 7;
21355
21465
  var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
21356
21466
  function pubkeyPath(brand) {
21357
- return resolve33(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
21467
+ return resolve31(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
21358
21468
  }
21359
21469
  var memo = null;
21360
21470
  function memoKey(mtimeMs, account) {
@@ -21366,7 +21476,7 @@ function resolveEntitlement(brand, account) {
21366
21476
  if (brand.commercialMode !== true) {
21367
21477
  return logResolved(implicitTrust(account), null);
21368
21478
  }
21369
- const entitlementPath = resolve33(brand.configDir, "entitlement.json");
21479
+ const entitlementPath = resolve31(brand.configDir, "entitlement.json");
21370
21480
  if (!existsSync31(entitlementPath)) {
21371
21481
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
21372
21482
  }
@@ -21628,7 +21738,7 @@ var app54 = new Hono();
21628
21738
  var nativeFileFollowers = /* @__PURE__ */ new Map();
21629
21739
  var waGateway = new WaGateway({
21630
21740
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
21631
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
21741
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
21632
21742
  // Task 751 — file delivery on the native channel: resolve the platform
21633
21743
  // account + path validation here and funnel through the shared send core.
21634
21744
  sendDocument: async ({ senderId, accountId, filePath, caption }) => {
@@ -21644,7 +21754,7 @@ var waGateway = new WaGateway({
21644
21754
  caption,
21645
21755
  accountId,
21646
21756
  maxyAccountId,
21647
- platformRoot: resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
21757
+ platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
21648
21758
  });
21649
21759
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
21650
21760
  },
@@ -21747,6 +21857,7 @@ var webchatGateway = new WebchatGateway({
21747
21857
  deleteSession: (sessionId) => managerDelete(sessionId),
21748
21858
  firePublicSessionEndReview: (input) => firePublicSessionEndReview(input)
21749
21859
  });
21860
+ setWebchatGateway(webchatGateway);
21750
21861
  app54.route("/", webchatGateway.routes());
21751
21862
  webchatGateway.startSweeper();
21752
21863
  webchatGateway.startPublicReaper();
@@ -21958,7 +22069,7 @@ app54.post("/__remote-auth/change-password", async (c) => {
21958
22069
  }
21959
22070
  try {
21960
22071
  if (targetUserId) {
21961
- await setAccessPassword(targetUserId, newPassword, USERS_FILE);
22072
+ await setAccessPassword(targetUserId, newPassword, USERS_FILE, { actor: targetUserId, logFile: USERS_AUDIT_LOG });
21962
22073
  } else {
21963
22074
  await setRemotePassword(newPassword);
21964
22075
  }
@@ -22045,7 +22156,7 @@ app54.post("/api/remote-auth/set-password", async (c) => {
22045
22156
  }
22046
22157
  try {
22047
22158
  if (sessionUserId) {
22048
- await setAccessPassword(sessionUserId, body.password, USERS_FILE);
22159
+ await setAccessPassword(sessionUserId, body.password, USERS_FILE, { actor: sessionUserId, logFile: USERS_AUDIT_LOG });
22049
22160
  } else {
22050
22161
  await setRemotePassword(body.password);
22051
22162
  }
@@ -22150,8 +22261,8 @@ app54.get("/agent-assets/:slug/:filename", (c) => {
22150
22261
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
22151
22262
  return c.text("Not found", 404);
22152
22263
  }
22153
- const filePath = resolve34(account.accountDir, "agents", slug, "assets", filename);
22154
- const expectedDir = resolve34(account.accountDir, "agents", slug, "assets");
22264
+ const filePath = resolve32(account.accountDir, "agents", slug, "assets", filename);
22265
+ const expectedDir = resolve32(account.accountDir, "agents", slug, "assets");
22155
22266
  if (!filePath.startsWith(expectedDir + "/")) {
22156
22267
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
22157
22268
  return c.text("Forbidden", 403);
@@ -22180,8 +22291,8 @@ app54.get("/generated/:filename", (c) => {
22180
22291
  console.error(`[generated] serve file=${filename} status=404`);
22181
22292
  return c.text("Not found", 404);
22182
22293
  }
22183
- const filePath = resolve34(account.accountDir, "generated", filename);
22184
- const expectedDir = resolve34(account.accountDir, "generated");
22294
+ const filePath = resolve32(account.accountDir, "generated", filename);
22295
+ const expectedDir = resolve32(account.accountDir, "generated");
22185
22296
  if (!filePath.startsWith(expectedDir + "/")) {
22186
22297
  console.error(`[generated] serve file=${filename} status=403`);
22187
22298
  return c.text("Forbidden", 403);
@@ -22235,11 +22346,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
22235
22346
  var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
22236
22347
  var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
22237
22348
  var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
22238
- (p) => existsSync32(resolve34(process.cwd(), "public", p.replace(/^\//, "")))
22349
+ (p) => existsSync32(resolve32(process.cwd(), "public", p.replace(/^\//, "")))
22239
22350
  );
22240
22351
  var SW_SOURCE = (() => {
22241
22352
  try {
22242
- return readFileSync33(resolve34(process.cwd(), "public", "sw.js"), "utf-8");
22353
+ return readFileSync33(resolve32(process.cwd(), "public", "sw.js"), "utf-8");
22243
22354
  } catch {
22244
22355
  return null;
22245
22356
  }
@@ -22290,7 +22401,7 @@ var clientErrorReporterScript = `<script>
22290
22401
  function cachedHtml(file) {
22291
22402
  let html = htmlCache.get(file);
22292
22403
  if (!html) {
22293
- html = readFileSync33(resolve34(process.cwd(), "public", file), "utf-8");
22404
+ html = readFileSync33(resolve32(process.cwd(), "public", file), "utf-8");
22294
22405
  const productNameEsc = escapeHtml(BRAND.productName);
22295
22406
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
22296
22407
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -22408,7 +22519,7 @@ app54.use("/vnc-popout.html", logViewerFetch);
22408
22519
  app54.get("/vnc-popout.html", (c) => {
22409
22520
  let html = htmlCache.get("vnc-popout.html");
22410
22521
  if (!html) {
22411
- html = readFileSync33(resolve34(process.cwd(), "public", "vnc-popout.html"), "utf-8");
22522
+ html = readFileSync33(resolve32(process.cwd(), "public", "vnc-popout.html"), "utf-8");
22412
22523
  const name = escapeHtml(BRAND.productName);
22413
22524
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
22414
22525
  html = html.replace("</head>", ` ${brandScript}
@@ -22532,7 +22643,7 @@ var httpServer = serve({ fetch: app54.fetch, port, hostname });
22532
22643
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
22533
22644
  {
22534
22645
  const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
22535
- const reconcileScript = resolve34(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
22646
+ const reconcileScript = resolve32(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
22536
22647
  const RECONCILE_INTERVAL_MS = 12e4;
22537
22648
  const runReconcile = () => {
22538
22649
  if (!existsSync32(reconcileScript)) return;
@@ -22554,7 +22665,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
22554
22665
  }
22555
22666
  {
22556
22667
  const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
22557
- const auditScript = resolve34(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
22668
+ const auditScript = resolve32(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
22558
22669
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
22559
22670
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
22560
22671
  const runConnectorAudit = () => {
@@ -22657,7 +22768,7 @@ try {
22657
22768
  const migAccount = resolveAccount();
22658
22769
  const ownerUserId = migAccount?.config.admins?.find((a) => a.role === "owner")?.userId;
22659
22770
  if (ownerUserId) {
22660
- const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId);
22771
+ const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId, { actor: "boot", logFile: USERS_AUDIT_LOG });
22661
22772
  console.error(`[remote-access-migrate] result=${result} ownerUserId=${ownerUserId.slice(0, 8)}`);
22662
22773
  } else {
22663
22774
  console.error("[remote-access-migrate] result=noop-no-owner ownerUserId=none");
@@ -22698,6 +22809,35 @@ var adminUserReconcileTimer = setInterval(() => {
22698
22809
  void runAdminUserReconcileTick();
22699
22810
  }, ADMINUSER_RECONCILE_INTERVAL_MS);
22700
22811
  if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
22812
+ var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
22813
+ function countUsersRows() {
22814
+ if (!existsSync32(USERS_FILE)) return 0;
22815
+ const raw = readFileSync33(USERS_FILE, "utf-8").trim();
22816
+ if (!raw) return 0;
22817
+ const users = JSON.parse(raw);
22818
+ return users.filter((u) => typeof u.userId === "string").length;
22819
+ }
22820
+ function runUsersReconcileTick() {
22821
+ try {
22822
+ const d = computeAdminStoreDivergence({ usersFile: USERS_FILE, accountsDir: ACCOUNTS_DIR });
22823
+ if (d.errors.length > 0) {
22824
+ console.error(`[users-reconcile] op=error detail=${d.errors.map((e) => `${e.source}:${e.detail}`).join(";")}`);
22825
+ return;
22826
+ }
22827
+ if (d.divergences === 0) {
22828
+ console.error(`[users-reconcile] op=ok admins=${countUsersRows()}`);
22829
+ return;
22830
+ }
22831
+ const accountOnly = d.accountWithoutUsers.map((a) => a.userId.slice(0, 8)).join(",");
22832
+ const usersOnly = d.usersWithoutAccount.map((u) => u.userId.slice(0, 8)).join(",");
22833
+ console.error(`[users-reconcile] op=divergence accountOnly=${accountOnly} usersOnly=${usersOnly}`);
22834
+ } catch (err) {
22835
+ console.error(`[users-reconcile] op=error detail=${err instanceof Error ? err.message : String(err)}`);
22836
+ }
22837
+ }
22838
+ runUsersReconcileTick();
22839
+ var usersReconcileTimer = setInterval(runUsersReconcileTick, USERS_RECONCILE_INTERVAL_MS);
22840
+ if (typeof usersReconcileTimer.unref === "function") usersReconcileTimer.unref();
22701
22841
  startGraphHealthTimer();
22702
22842
  void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
22703
22843
  void startFileWatcher().catch((err) => {
@@ -22786,7 +22926,7 @@ if (bootAccountConfig?.whatsapp) {
22786
22926
  }
22787
22927
  init({
22788
22928
  configDir: configDirForWhatsApp,
22789
- platformRoot: resolve34(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
22929
+ platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
22790
22930
  accountConfig: bootAccountConfig,
22791
22931
  onMessage: async (msg) => {
22792
22932
  if (msg.isOwnerMirror) {
@@ -22827,7 +22967,6 @@ init({
22827
22967
  }).catch((err) => {
22828
22968
  console.error(`[whatsapp] init failed: ${String(err)}`);
22829
22969
  });
22830
- startReaper();
22831
22970
  var shuttingDown = false;
22832
22971
  process.on("SIGTERM", async () => {
22833
22972
  if (shuttingDown) {