@rubytech/create-maxy-code 0.1.481 → 0.1.482

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.
@@ -1563,7 +1563,7 @@ var serveStatic = (options = { root: "" }) => {
1563
1563
  import { readFileSync as readFileSync42, existsSync as existsSync38, watchFile, readdirSync as readdirSync26, statSync as statSync17 } from "fs";
1564
1564
  import { spawn as spawn3 } from "child_process";
1565
1565
  import { createHash as createHash7 } from "crypto";
1566
- import { resolve as resolve37, join as join44, basename as basename12 } from "path";
1566
+ import { resolve as resolve38, join as join45, basename as basename12 } from "path";
1567
1567
  import { homedir as homedir4 } from "os";
1568
1568
  import { monitorEventLoopDelay } from "perf_hooks";
1569
1569
 
@@ -2472,7 +2472,8 @@ var WhatsAppAccountSchema = z.object({
2472
2472
  }).strict().optional().describe("React to incoming messages with an emoji to acknowledge receipt before the agent responds."),
2473
2473
  debounceMs: z.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
2474
2474
  publicAgent: z.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set."),
2475
- afterHoursMessage: z.string().optional().describe("Auto-reply text sent to public senders when the business is closed (outside opening hours). If not set, messages received outside business hours are silently skipped with no reply.")
2475
+ afterHoursMessage: z.string().optional().describe("Auto-reply text sent to public senders when the business is closed (outside opening hours). If not set, messages received outside business hours are silently skipped with no reply."),
2476
+ dispatchInbound: z.boolean().optional().describe("When false, this account's WhatsApp socket only observes: inbound is stored and readable but never dispatched to an agent. Default true. Set with set-dispatch-inbound.")
2476
2477
  }).strict().superRefine((value, ctx) => {
2477
2478
  if (value.dmPolicy !== "open") return;
2478
2479
  const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
@@ -2836,7 +2837,7 @@ var credsSaveQueue = Promise.resolve();
2836
2837
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2837
2838
  console.error(`${TAG6} draining credential save queue\u2026`);
2838
2839
  const timer = new Promise(
2839
- (resolve38) => setTimeout(() => resolve38("timeout"), timeoutMs)
2840
+ (resolve39) => setTimeout(() => resolve39("timeout"), timeoutMs)
2840
2841
  );
2841
2842
  const result = await Promise.race([
2842
2843
  credsSaveQueue.then(() => "drained"),
@@ -3013,11 +3014,11 @@ async function createWaSocket(opts) {
3013
3014
  return sock;
3014
3015
  }
3015
3016
  async function waitForConnection(sock) {
3016
- return new Promise((resolve38, reject) => {
3017
+ return new Promise((resolve39, reject) => {
3017
3018
  const handler = (update) => {
3018
3019
  if (update.connection === "open") {
3019
3020
  sock.ev.off("connection.update", handler);
3020
- resolve38();
3021
+ resolve39();
3021
3022
  }
3022
3023
  if (update.connection === "close") {
3023
3024
  sock.ev.off("connection.update", handler);
@@ -3125,7 +3126,7 @@ function isTimeoutError(err) {
3125
3126
  return Boolean(err?.__maxyTimeout);
3126
3127
  }
3127
3128
  function withTimeout(label, promise, timeoutMs) {
3128
- return new Promise((resolve38, reject) => {
3129
+ return new Promise((resolve39, reject) => {
3129
3130
  const timer = setTimeout(() => {
3130
3131
  const err = new Error(`${label} timed out after ${timeoutMs}ms`);
3131
3132
  err.__maxyTimeout = true;
@@ -3135,7 +3136,7 @@ function withTimeout(label, promise, timeoutMs) {
3135
3136
  promise.then(
3136
3137
  (value) => {
3137
3138
  clearTimeout(timer);
3138
- resolve38(value);
3139
+ resolve39(value);
3139
3140
  },
3140
3141
  (err) => {
3141
3142
  clearTimeout(timer);
@@ -3622,6 +3623,18 @@ function checkGroupAccess(_params) {
3622
3623
  return { allowed: false, reason: "group-observe-only", agentType: "public" };
3623
3624
  }
3624
3625
 
3626
+ // app/lib/whatsapp/inbound/dispatch-mode.ts
3627
+ function shouldDispatchInbound(config, accountId) {
3628
+ return config.accounts?.[accountId]?.dispatchInbound ?? true;
3629
+ }
3630
+ function decideDispatch(conn, payload, deps) {
3631
+ if (shouldDispatchInbound(deps.config, conn.accountId)) {
3632
+ deps.emit(payload);
3633
+ return;
3634
+ }
3635
+ deps.log(`[whatsapp:observe] op=dispatch-suppressed account=${conn.accountId} sender=${payload.senderPhone} agentType=${payload.agentType}`);
3636
+ }
3637
+
3625
3638
  // app/lib/access-gate.ts
3626
3639
  import neo4j from "neo4j-driver";
3627
3640
  import { readFileSync as readFileSync6 } from "fs";
@@ -4175,8 +4188,8 @@ async function persistWhatsAppMessage(input) {
4175
4188
  const { givenName, familyName } = splitName(input.pushName);
4176
4189
  const prev = sessionWriteLocks.get(input.cacheKey);
4177
4190
  let release;
4178
- const mine = new Promise((resolve38) => {
4179
- release = resolve38;
4191
+ const mine = new Promise((resolve39) => {
4192
+ release = resolve39;
4180
4193
  });
4181
4194
  const chained = (prev ?? Promise.resolve()).then(() => mine);
4182
4195
  sessionWriteLocks.set(input.cacheKey, chained);
@@ -5484,6 +5497,16 @@ var configDir = null;
5484
5497
  var whatsAppConfig = {};
5485
5498
  var onInboundMessage = null;
5486
5499
  var initialized = false;
5500
+ function dispatchInboundPayload(conn, payload) {
5501
+ if (!onInboundMessage) return;
5502
+ decideDispatch(conn, payload, { config: whatsAppConfig, emit: onInboundMessage, log: (l) => console.error(l) });
5503
+ }
5504
+ function isObserveAccount(accountId) {
5505
+ return !shouldDispatchInbound(whatsAppConfig, accountId);
5506
+ }
5507
+ function getWhatsAppConfig() {
5508
+ return whatsAppConfig;
5509
+ }
5487
5510
  var messageStore = /* @__PURE__ */ new Map();
5488
5511
  function messageStoreKey(accountId, remoteJid) {
5489
5512
  return `${accountId}:${remoteJid}`;
@@ -5903,11 +5926,11 @@ async function connectWithReconnect(conn) {
5903
5926
  console.error(
5904
5927
  `${TAG17} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
5905
5928
  );
5906
- await new Promise((resolve38) => {
5907
- const timer = setTimeout(resolve38, delay);
5929
+ await new Promise((resolve39) => {
5930
+ const timer = setTimeout(resolve39, delay);
5908
5931
  conn.abortController.signal.addEventListener("abort", () => {
5909
5932
  clearTimeout(timer);
5910
- resolve38();
5933
+ resolve39();
5911
5934
  }, { once: true });
5912
5935
  });
5913
5936
  }
@@ -5915,16 +5938,16 @@ async function connectWithReconnect(conn) {
5915
5938
  }
5916
5939
  }
5917
5940
  function waitForDisconnectEvent(conn) {
5918
- return new Promise((resolve38) => {
5941
+ return new Promise((resolve39) => {
5919
5942
  if (!conn.sock) {
5920
- resolve38();
5943
+ resolve39();
5921
5944
  return;
5922
5945
  }
5923
5946
  const sock = conn.sock;
5924
5947
  const handler = (update) => {
5925
5948
  if (update.connection === "close") {
5926
5949
  sock.ev.off("connection.update", handler);
5927
- resolve38();
5950
+ resolve39();
5928
5951
  }
5929
5952
  };
5930
5953
  sock.ev.on("connection.update", handler);
@@ -5977,7 +6000,7 @@ function monitorInbound(conn) {
5977
6000
  if (entries.length > 1) {
5978
6001
  console.error(`${TAG17} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
5979
6002
  }
5980
- onInboundMessage(combineInboundBatch(entries));
6003
+ dispatchInboundPayload(conn, combineInboundBatch(entries));
5981
6004
  },
5982
6005
  onError: (err) => {
5983
6006
  console.error(`${TAG17} debounce flush error account=${conn.accountId}: ${String(err)}`);
@@ -6202,7 +6225,7 @@ function monitorInbound(conn) {
6202
6225
  const currentSock = conn.sock;
6203
6226
  if (currentSock) await sendComposing(currentSock, remoteJid);
6204
6227
  };
6205
- onInboundMessage(buildSelfChatDispatch({
6228
+ dispatchInboundPayload(conn, buildSelfChatDispatch({
6206
6229
  accountId: conn.accountId,
6207
6230
  selfPhone: conn.selfPhone,
6208
6231
  remoteJid,
@@ -6311,7 +6334,7 @@ async function handleInboundMessage(conn, msg) {
6311
6334
  const currentSock = conn.sock;
6312
6335
  if (currentSock) await sendComposing(currentSock, remoteJid);
6313
6336
  };
6314
- onInboundMessage({
6337
+ dispatchInboundPayload(conn, {
6315
6338
  accountId: conn.accountId,
6316
6339
  agentType: "admin",
6317
6340
  senderPhone: senderPhone2,
@@ -6358,8 +6381,8 @@ async function handleInboundMessage(conn, msg) {
6358
6381
  const conversationKey = isGroup ? remoteJid : senderPhone;
6359
6382
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
6360
6383
  let resolvePending;
6361
- const sttPending = new Promise((resolve38) => {
6362
- resolvePending = resolve38;
6384
+ const sttPending = new Promise((resolve39) => {
6385
+ resolvePending = resolve39;
6363
6386
  });
6364
6387
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
6365
6388
  try {
@@ -6477,7 +6500,7 @@ async function handleInboundMessage(conn, msg) {
6477
6500
  if (conn.debouncer) {
6478
6501
  conn.debouncer.enqueue(payload);
6479
6502
  } else {
6480
- onInboundMessage(payload);
6503
+ dispatchInboundPayload(conn, payload);
6481
6504
  }
6482
6505
  }
6483
6506
 
@@ -6787,20 +6810,20 @@ function buildX11Env(chromiumWrapperPath, transport = "vnc") {
6787
6810
 
6788
6811
  // server/routes/health.ts
6789
6812
  function checkPort(port2, timeoutMs = 500) {
6790
- return new Promise((resolve38) => {
6813
+ return new Promise((resolve39) => {
6791
6814
  const socket = createConnection2(port2, "127.0.0.1");
6792
6815
  socket.setTimeout(timeoutMs);
6793
6816
  socket.once("connect", () => {
6794
6817
  socket.destroy();
6795
- resolve38(true);
6818
+ resolve39(true);
6796
6819
  });
6797
6820
  socket.once("error", () => {
6798
6821
  socket.destroy();
6799
- resolve38(false);
6822
+ resolve39(false);
6800
6823
  });
6801
6824
  socket.once("timeout", () => {
6802
6825
  socket.destroy();
6803
- resolve38(false);
6826
+ resolve39(false);
6804
6827
  });
6805
6828
  });
6806
6829
  }
@@ -7751,6 +7774,48 @@ function setAccountManager(accountDir, phone, subAccountId, mode = "active") {
7751
7774
  return { ok: false, error: msg };
7752
7775
  }
7753
7776
  }
7777
+ function setDispatchInbound(accountDir, accountId, value) {
7778
+ const id = accountId.trim();
7779
+ if (!id) return { ok: false, error: "Missing account id." };
7780
+ let validIds;
7781
+ try {
7782
+ validIds = listValidAccounts().map((a) => a.accountId);
7783
+ } catch (err) {
7784
+ return { ok: false, error: `Could not read the account registry to validate the account: ${err instanceof Error ? err.message : String(err)}. Try again.` };
7785
+ }
7786
+ if (!validIds.includes(id)) {
7787
+ return { ok: false, error: `Account "${id}" is not in this install's account registry.` };
7788
+ }
7789
+ try {
7790
+ const config = readConfig(accountDir);
7791
+ if (!config.whatsapp || typeof config.whatsapp !== "object") {
7792
+ config.whatsapp = {};
7793
+ }
7794
+ const wa = config.whatsapp;
7795
+ if (!wa.accounts || typeof wa.accounts !== "object") {
7796
+ wa.accounts = {};
7797
+ }
7798
+ const accounts = wa.accounts;
7799
+ if (!accounts[id] || typeof accounts[id] !== "object") {
7800
+ accounts[id] = { name: "Main" };
7801
+ }
7802
+ accounts[id].dispatchInbound = value;
7803
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
7804
+ if (!parsed.success) {
7805
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
7806
+ return { ok: false, error: `Validation failed: ${msg}` };
7807
+ }
7808
+ config.whatsapp = parsed.data;
7809
+ writeConfig(accountDir, config);
7810
+ console.error(`${TAG21} set dispatchInbound account=${id} value=${value}`);
7811
+ reloadManagerConfig(accountDir);
7812
+ return { ok: true };
7813
+ } catch (err) {
7814
+ const msg = err instanceof Error ? err.message : String(err);
7815
+ console.error(`${TAG21} setDispatchInbound failed: ${msg}`);
7816
+ return { ok: false, error: msg };
7817
+ }
7818
+ }
7754
7819
  function clearAccountManager(accountDir, phone) {
7755
7820
  const normalized = phone.trim();
7756
7821
  try {
@@ -8089,8 +8154,8 @@ async function startLogin(opts) {
8089
8154
  await clearAuth(authDir);
8090
8155
  let resolveCode = null;
8091
8156
  let rejectCode = null;
8092
- const codePromise = new Promise((resolve38, reject) => {
8093
- resolveCode = resolve38;
8157
+ const codePromise = new Promise((resolve39, reject) => {
8158
+ resolveCode = resolve39;
8094
8159
  rejectCode = reject;
8095
8160
  });
8096
8161
  const codeTimer = setTimeout(
@@ -8489,6 +8554,14 @@ function reconcileCredsOnDisk(opts) {
8489
8554
  return entries;
8490
8555
  }
8491
8556
 
8557
+ // app/lib/whatsapp/outbound/reply-guard.ts
8558
+ function admitReply(input) {
8559
+ if (isGroupJid(input.remoteJid)) return { ok: false, status: 403, reason: "group-send-blocked" };
8560
+ if (!isUserJid(input.remoteJid)) return { ok: false, status: 400, reason: "invalid-remote-jid" };
8561
+ if (!input.hasConversation) return { ok: false, status: 403, reason: "no-existing-conversation" };
8562
+ return { ok: true };
8563
+ }
8564
+
8492
8565
  // app/lib/whatsapp-reader/select-sessions.ts
8493
8566
  function isReaderChannelSession(role, channel) {
8494
8567
  return role === "admin" && (channel === "whatsapp" || channel === "telegram") || role === "public" && (channel === "whatsapp" || channel === "webchat");
@@ -8647,6 +8720,15 @@ function houseSocketScopeDenial(c, op) {
8647
8720
  console.error(`${TAG25} op=authz-deny route=${op} caller=${callerAccountId ?? "none"} houseAdmin=${houseAdminScope ? "y" : "n"}`);
8648
8721
  return c.json({ ok: false, error: "Not authorized. The WhatsApp connection and its settings are house-scoped; this session may only act on its own account." }, 403);
8649
8722
  }
8723
+ function selfOrHouseScopeDenial(c, targetAccountId, op) {
8724
+ const { decision, callerAccountId, houseAdminScope } = evaluateCallerScope(c, targetAccountId);
8725
+ if (decision.authorized) {
8726
+ console.error(`${TAG25} op=authz-ok route=${op} target=${targetAccountId} caller=${callerAccountId ?? "none"} houseAdmin=${houseAdminScope ? "y" : "n"}`);
8727
+ return null;
8728
+ }
8729
+ console.error(`${TAG25} op=authz-deny route=${op} target=${targetAccountId} caller=${callerAccountId ?? "none"} houseAdmin=${houseAdminScope ? "y" : "n"}`);
8730
+ return c.json({ ok: false, error: "Not authorized. This session may only pair or manage its own account." }, 403);
8731
+ }
8650
8732
  var app2 = new Hono();
8651
8733
  app2.get("/status", (c) => {
8652
8734
  try {
@@ -8658,8 +8740,12 @@ app2.get("/status", (c) => {
8658
8740
  const credsRoot = join12(MAXY_DIR, "credentials", "whatsapp");
8659
8741
  const accountDir = resolveAccount()?.accountDir ?? null;
8660
8742
  const reconciled = reconcileCredsOnDisk({ credsRoot, accountDir, liveAccountIds: liveIds, activeLoginAccountIds: activeLoginIds });
8661
- const accounts = [...live, ...reconciled];
8662
- const summary = accounts.map((a) => `${a.accountId}:${a.connected ? "up" : a.linkedUnconfigured ? "linked-unconfigured" : "down"}`).join(", ");
8743
+ const cfg = getWhatsAppConfig();
8744
+ const accounts = [...live, ...reconciled].map((a) => ({
8745
+ ...a,
8746
+ dispatchInbound: shouldDispatchInbound(cfg, a.accountId)
8747
+ }));
8748
+ const summary = accounts.map((a) => `${a.accountId}:${a.connected ? "up" : a.linkedUnconfigured ? "linked-unconfigured" : "down"}:${a.dispatchInbound ? "dispatch" : "observe"}`).join(", ");
8663
8749
  console.error(`${TAG25} status accounts=${accounts.length} [${summary}]`);
8664
8750
  const reachableAccountIds = [
8665
8751
  .../* @__PURE__ */ new Set([...listStoredAccountIds(), ...listConnectionPlatformAccountIds()])
@@ -8679,10 +8765,10 @@ app2.get("/status", (c) => {
8679
8765
  });
8680
8766
  app2.post("/login/start", async (c) => {
8681
8767
  try {
8682
- const denial = houseSocketScopeDenial(c, "login/start");
8683
- if (denial) return denial;
8684
8768
  const body = await c.req.json().catch(() => ({}));
8685
8769
  const accountId = validateAccountId(body.accountId);
8770
+ const denial = selfOrHouseScopeDenial(c, accountId, "login/start");
8771
+ if (denial) return denial;
8686
8772
  const force = body.force ?? false;
8687
8773
  const phone = typeof body.phone === "string" ? body.phone.trim() : "";
8688
8774
  if (!phone) {
@@ -8700,10 +8786,10 @@ app2.post("/login/start", async (c) => {
8700
8786
  });
8701
8787
  app2.post("/login/wait", async (c) => {
8702
8788
  try {
8703
- const denial = houseSocketScopeDenial(c, "login/wait");
8704
- if (denial) return denial;
8705
8789
  const body = await c.req.json().catch(() => ({}));
8706
8790
  const accountId = validateAccountId(body.accountId);
8791
+ const denial = selfOrHouseScopeDenial(c, accountId, "login/wait");
8792
+ if (denial) return denial;
8707
8793
  const timeoutMs = body.timeoutMs ?? 6e4;
8708
8794
  const result = await waitForLogin({ accountId, timeoutMs });
8709
8795
  console.error(`${TAG25} login/wait result account=${accountId} connected=${result.connected}${result.selfPhone ? ` phone=${result.selfPhone}` : ""} configPersisted=${result.configPersisted}`);
@@ -8720,10 +8806,10 @@ app2.post("/login/wait", async (c) => {
8720
8806
  });
8721
8807
  app2.post("/disconnect", async (c) => {
8722
8808
  try {
8723
- const denial = houseSocketScopeDenial(c, "disconnect");
8724
- if (denial) return denial;
8725
8809
  const body = await c.req.json().catch(() => ({}));
8726
8810
  const accountId = validateAccountId(body.accountId);
8811
+ const denial = selfOrHouseScopeDenial(c, accountId, "disconnect");
8812
+ if (denial) return denial;
8727
8813
  await stopConnection(accountId);
8728
8814
  return c.json({ disconnected: true, accountId });
8729
8815
  } catch (err) {
@@ -8733,10 +8819,10 @@ app2.post("/disconnect", async (c) => {
8733
8819
  });
8734
8820
  app2.post("/reconnect", async (c) => {
8735
8821
  try {
8736
- const denial = houseSocketScopeDenial(c, "reconnect");
8737
- if (denial) return denial;
8738
8822
  const body = await c.req.json().catch(() => ({}));
8739
8823
  const accountId = validateAccountId(body.accountId);
8824
+ const denial = selfOrHouseScopeDenial(c, accountId, "reconnect");
8825
+ if (denial) return denial;
8740
8826
  await startConnection(accountId);
8741
8827
  return c.json({ reconnecting: true, accountId });
8742
8828
  } catch (err) {
@@ -8751,6 +8837,22 @@ app2.post("/config", async (c) => {
8751
8837
  if (!action || typeof action !== "string") {
8752
8838
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
8753
8839
  }
8840
+ if (action === "set-dispatch-inbound") {
8841
+ if (!accountId || typeof accountId !== "string") {
8842
+ return c.json({ ok: false, error: 'Missing required field "accountId" (the account whose socket to set).' }, 400);
8843
+ }
8844
+ const target = validateAccountId(accountId);
8845
+ const scopeDenial = selfOrHouseScopeDenial(c, target, `config action=set-dispatch-inbound target=${target}`);
8846
+ if (scopeDenial) return scopeDenial;
8847
+ if (typeof body.dispatchInbound !== "boolean") {
8848
+ return c.json({ ok: false, error: 'Missing required boolean field "dispatchInbound".' }, 400);
8849
+ }
8850
+ const acct = resolveAccount();
8851
+ if (!acct) return c.json({ ok: false, error: "No account configured." }, 500);
8852
+ const result = setDispatchInbound(acct.accountDir, target, body.dispatchInbound);
8853
+ console.error(`${TAG25} config action=set-dispatch-inbound account=${target} dispatchInbound=${body.dispatchInbound} ok=${result.ok}`);
8854
+ return c.json(result, result.ok ? 200 : 400);
8855
+ }
8754
8856
  const denial = houseSocketScopeDenial(c, `config action=${action}`);
8755
8857
  if (denial) return denial;
8756
8858
  const account = resolveAccount();
@@ -8961,6 +9063,65 @@ app2.post("/send-admin", async (c) => {
8961
9063
  return c.json({ error: String(err) }, 500);
8962
9064
  }
8963
9065
  });
9066
+ app2.post("/reply", async (c) => {
9067
+ const sendId = process.hrtime.bigint().toString(36).slice(-8);
9068
+ try {
9069
+ const body = await c.req.json().catch(() => ({}));
9070
+ if (!body.accountId || typeof body.accountId !== "string") {
9071
+ return c.json({ error: "accountId is required" }, 400);
9072
+ }
9073
+ const accountId = validateAccountId(body.accountId);
9074
+ const denial = selfOrHouseScopeDenial(c, accountId, "reply");
9075
+ if (denial) return denial;
9076
+ const remoteJid = typeof body.remoteJid === "string" ? body.remoteJid : "";
9077
+ const text = typeof body.text === "string" && body.text.trim().length ? body.text : "";
9078
+ if (!remoteJid || !text) {
9079
+ return c.json({ error: "accountId, remoteJid and text are required" }, 400);
9080
+ }
9081
+ const storeKey = resolveReadStoreKey(accountId);
9082
+ const convo = readConversation(storeKey, remoteJid);
9083
+ const admit = admitReply({ remoteJid, hasConversation: convo.length > 0 });
9084
+ if (!admit.ok) {
9085
+ console.error(`[whatsapp:observe] op=reply-refused sendId=${sendId} account=${accountId} remoteJid=${remoteJid} reason=${admit.reason}`);
9086
+ return c.json({ error: admit.reason, reason: admit.reason }, admit.status);
9087
+ }
9088
+ const res = resolveSocket(accountId);
9089
+ if (!res.ok) {
9090
+ console.error(`[whatsapp:observe] op=reply-socket sendId=${sendId} account=${accountId} result=${res.reason} presentKeys=${res.presentKeys.join(",") || "none"}`);
9091
+ return c.json({ error: formatSocketResolutionMessage(res, accountId), reason: res.reason }, 503);
9092
+ }
9093
+ const sent = await sendTextMessage(res.sock, remoteJid, text, { accountId: res.accountId });
9094
+ if (!sent.success) {
9095
+ console.error(`[whatsapp:observe] op=reply-send-fail sendId=${sendId} account=${accountId} error=${sent.error ?? ""}`);
9096
+ return c.json({ error: sent.error ?? "send failed" }, 500);
9097
+ }
9098
+ if (sent.messageId) {
9099
+ const template = convo[convo.length - 1];
9100
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
9101
+ const rec = {
9102
+ messageId: `whatsapp-live:${res.accountId}:${remoteJid}:${sent.messageId}`,
9103
+ sessionId: template.sessionId,
9104
+ dateSent: nowIso,
9105
+ body: text,
9106
+ fromMe: true,
9107
+ senderTelephone: template.fromMe ? template.senderTelephone : "",
9108
+ senderName: null,
9109
+ remoteJid,
9110
+ msgKeyId: sent.messageId,
9111
+ quotedId: null,
9112
+ quotedSender: null,
9113
+ scope: template.scope,
9114
+ createdAt: nowIso
9115
+ };
9116
+ appendMessage(storeKey, rec);
9117
+ }
9118
+ console.error(`[whatsapp:observe] op=reply-sent sendId=${sendId} account=${accountId} socket=${res.accountId} waMessageId=${sent.messageId ?? ""}`);
9119
+ return c.json({ success: true, messageId: sent.messageId });
9120
+ } catch (err) {
9121
+ console.error(`${TAG25} reply error: ${String(err)}`);
9122
+ return c.json({ error: String(err) }, 500);
9123
+ }
9124
+ });
8964
9125
  app2.get("/activity", (c) => {
8965
9126
  try {
8966
9127
  const accountId = c.req.query("accountId") ?? void 0;
@@ -14342,15 +14503,15 @@ function operatorRoleFor(config, userId) {
14342
14503
  async function buildAccountOptionList(accounts, userId) {
14343
14504
  const houseAccount = accounts.find((a) => a.config.role === "house");
14344
14505
  console.log(`[admin-accounts] op=list count=${accounts.length} house=${houseAccount?.accountId ?? "none"}`);
14345
- const resolve38 = async (a) => {
14506
+ const resolve39 = async (a) => {
14346
14507
  const businessName = await fetchAccountName(a.accountId) || void 0;
14347
14508
  return { accountId: a.accountId, businessName, role: operatorRoleFor(a.config, userId), isHouse: a.config.role === "house" };
14348
14509
  };
14349
14510
  const houseOptions = await Promise.all(
14350
- accounts.filter((a) => a.config.role === "house").map(resolve38)
14511
+ accounts.filter((a) => a.config.role === "house").map(resolve39)
14351
14512
  );
14352
14513
  const nonHouseOptions = await Promise.all(
14353
- accounts.filter((a) => a.config.role !== "house").map(resolve38)
14514
+ accounts.filter((a) => a.config.role !== "house").map(resolve39)
14354
14515
  );
14355
14516
  const sortKey = (o) => (o.businessName || o.accountId).toLowerCase();
14356
14517
  nonHouseOptions.sort((x, y) => {
@@ -20367,6 +20528,76 @@ function isWithin(target, root) {
20367
20528
  }
20368
20529
  var sidebar_artefacts_default = app29;
20369
20530
 
20531
+ // server/channel-session-override.ts
20532
+ import { readFileSync as readFileSync34, writeFileSync as writeFileSync12, renameSync as renameSync9 } from "fs";
20533
+ import { join as join35 } from "path";
20534
+ var FILE2 = "channel-session-override.json";
20535
+ var UUID2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
20536
+ function channelOverridePath(accountDir) {
20537
+ return join35(accountDir, FILE2);
20538
+ }
20539
+ function senderKey(channel, senderId) {
20540
+ return `${channel}:${senderId}`;
20541
+ }
20542
+ function readRaw2(accountDir) {
20543
+ try {
20544
+ return JSON.parse(readFileSync34(channelOverridePath(accountDir), "utf8"));
20545
+ } catch {
20546
+ return null;
20547
+ }
20548
+ }
20549
+ function readChannelOverrideId(accountDir, channel, senderId) {
20550
+ const raw = readRaw2(accountDir);
20551
+ if (!raw || !raw.bySender || typeof raw.bySender !== "object") return null;
20552
+ const v = raw.bySender[senderKey(channel, senderId)];
20553
+ return typeof v === "string" && UUID2.test(v) ? v : null;
20554
+ }
20555
+ function writeChannelOverrideId(accountDir, channel, senderId, sessionId) {
20556
+ const raw = readRaw2(accountDir);
20557
+ const bySender = {};
20558
+ if (raw?.bySender && typeof raw.bySender === "object") {
20559
+ for (const [k, v] of Object.entries(raw.bySender)) {
20560
+ if (typeof v === "string") bySender[k] = v;
20561
+ }
20562
+ }
20563
+ bySender[senderKey(channel, senderId)] = sessionId;
20564
+ const path3 = channelOverridePath(accountDir);
20565
+ const tmp = `${path3}.${process.pid}.tmp`;
20566
+ writeFileSync12(tmp, JSON.stringify({ bySender }, null, 2), "utf8");
20567
+ renameSync9(tmp, path3);
20568
+ }
20569
+ function clearChannelOverrideForSession(accountDir, sessionId) {
20570
+ const raw = readRaw2(accountDir);
20571
+ if (!raw?.bySender || typeof raw.bySender !== "object") return [];
20572
+ const kept = {};
20573
+ const cleared = [];
20574
+ for (const [k, v] of Object.entries(raw.bySender)) {
20575
+ if (typeof v !== "string") continue;
20576
+ if (v === sessionId) cleared.push(k);
20577
+ else kept[k] = v;
20578
+ }
20579
+ if (cleared.length === 0) return [];
20580
+ const path3 = channelOverridePath(accountDir);
20581
+ const tmp = `${path3}.${process.pid}.tmp`;
20582
+ writeFileSync12(tmp, JSON.stringify({ bySender: kept }, null, 2), "utf8");
20583
+ renameSync9(tmp, path3);
20584
+ return cleared;
20585
+ }
20586
+
20587
+ // server/routes/admin/clear-forward-pointers.ts
20588
+ function clearForwardPointers(sessionId) {
20589
+ try {
20590
+ for (const a of listValidAccounts()) {
20591
+ const cleared = clearChannelOverrideForSession(a.accountDir, sessionId);
20592
+ if (cleared.length > 0) {
20593
+ console.log(`[session-lifecycle] op=channel-forward-cleared sessionId=${sessionId.slice(0, 8)} account=${a.accountId} senderKeys=${cleared.join(",")}`);
20594
+ }
20595
+ }
20596
+ } catch (err) {
20597
+ console.error(`[session-lifecycle] op=channel-forward-clear-failed sessionId=${sessionId.slice(0, 8)} err=${err instanceof Error ? err.message : String(err)}`);
20598
+ }
20599
+ }
20600
+
20370
20601
  // server/routes/admin/session-delete.ts
20371
20602
  var app30 = new Hono();
20372
20603
  app30.post("/", requireAdminSession, async (c) => {
@@ -20399,6 +20630,7 @@ app30.post("/", requireAdminSession, async (c) => {
20399
20630
  return c.json({ error: "manager-unreachable" }, 502);
20400
20631
  }
20401
20632
  if (del.status === 204) {
20633
+ clearForwardPointers(sessionId);
20402
20634
  console.log(`[session-delete] sessionId=${sessionId} deleted`);
20403
20635
  return c.json({ deleted: true });
20404
20636
  }
@@ -20467,6 +20699,7 @@ app32.post("/", requireAdminSession, async (c) => {
20467
20699
  return c.json({ error: "manager-unreachable" }, 502);
20468
20700
  }
20469
20701
  if (res.status === 200) {
20702
+ if (mode === "archive") clearForwardPointers(sessionId);
20470
20703
  const dir = mode === "archive" ? "top\u2192archive" : "archive\u2192top";
20471
20704
  console.log(`[session-archive] sessionId=${sessionId.slice(0, 8)} moved ${dir}`);
20472
20705
  return c.json({ archived: mode === "archive" });
@@ -20696,49 +20929,40 @@ var session_rc_spawn_default = app35;
20696
20929
  // server/routes/admin/session-reseat.ts
20697
20930
  import { randomUUID as randomUUID12 } from "crypto";
20698
20931
 
20699
- // server/channel-session-override.ts
20700
- import { readFileSync as readFileSync34, writeFileSync as writeFileSync12, renameSync as renameSync9 } from "fs";
20701
- import { join as join35 } from "path";
20702
- var FILE2 = "channel-session-override.json";
20703
- var UUID2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
20704
- function channelOverridePath(accountDir) {
20705
- return join35(accountDir, FILE2);
20932
+ // server/wa-descriptor.ts
20933
+ import { resolve as resolve26, join as join36 } from "path";
20934
+ function waGatewayUrl() {
20935
+ return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
20706
20936
  }
20707
- function senderKey(channel, senderId) {
20708
- return `${channel}:${senderId}`;
20937
+ function waServerPath() {
20938
+ return process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve26(process.env.MAXY_PLATFORM_ROOT ?? join36(__dirname, ".."), "services/whatsapp-channel/dist/server.js");
20709
20939
  }
20710
- function readRaw2(accountDir) {
20711
- try {
20712
- return JSON.parse(readFileSync34(channelOverridePath(accountDir), "utf8"));
20713
- } catch {
20714
- return null;
20715
- }
20940
+ function waChannelDescriptor(senderId) {
20941
+ return { senderId, gatewayUrl: waGatewayUrl(), serverPath: waServerPath() };
20716
20942
  }
20717
- function readChannelOverrideId(accountDir, channel, senderId) {
20718
- const raw = readRaw2(accountDir);
20719
- if (!raw || !raw.bySender || typeof raw.bySender !== "object") return null;
20720
- const v = raw.bySender[senderKey(channel, senderId)];
20721
- return typeof v === "string" && UUID2.test(v) ? v : null;
20943
+
20944
+ // server/telegram-descriptor.ts
20945
+ import { resolve as resolve27, join as join37 } from "path";
20946
+ function telegramGatewayUrl() {
20947
+ return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
20722
20948
  }
20723
- function writeChannelOverrideId(accountDir, channel, senderId, sessionId) {
20724
- const raw = readRaw2(accountDir);
20725
- const bySender = {};
20726
- if (raw?.bySender && typeof raw.bySender === "object") {
20727
- for (const [k, v] of Object.entries(raw.bySender)) {
20728
- if (typeof v === "string") bySender[k] = v;
20729
- }
20730
- }
20731
- bySender[senderKey(channel, senderId)] = sessionId;
20732
- const path3 = channelOverridePath(accountDir);
20733
- const tmp = `${path3}.${process.pid}.tmp`;
20734
- writeFileSync12(tmp, JSON.stringify({ bySender }, null, 2), "utf8");
20735
- renameSync9(tmp, path3);
20949
+ function telegramServerPath() {
20950
+ return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve27(process.env.MAXY_PLATFORM_ROOT ?? join37(__dirname, ".."), "services/telegram-channel/dist/server.js");
20951
+ }
20952
+ function telegramChannelDescriptor(senderId) {
20953
+ return { senderId, gatewayUrl: telegramGatewayUrl(), serverPath: telegramServerPath() };
20736
20954
  }
20737
20955
 
20738
20956
  // server/routes/admin/session-reseat.ts
20739
20957
  function isAdminChannelSource(source) {
20740
20958
  return !!source && source.role === "admin" && (source.channel === "whatsapp" || source.channel === "telegram") && typeof source.senderId === "string" && source.senderId.length > 0;
20741
20959
  }
20960
+ function shouldWriteChannelForward(source) {
20961
+ return isAdminChannelSource(source) && !!source.accountId;
20962
+ }
20963
+ function isPublicChannelSource(source) {
20964
+ return !!source && source.role === "public" && (source.channel === "whatsapp" || source.channel === "telegram");
20965
+ }
20742
20966
  function resolveReseatPlan(body, mintId = randomUUID12, adminUserId, authenticatedName, source) {
20743
20967
  const sessionId = mintId();
20744
20968
  const key = `session:${sessionId}`;
@@ -20752,6 +20976,8 @@ function resolveReseatPlan(body, mintId = randomUUID12, adminUserId, authenticat
20752
20976
  payload.channel = source.channel;
20753
20977
  payload.senderId = source.senderId;
20754
20978
  if (source.accountId) payload.targetAccountId = source.accountId;
20979
+ if (source.channel === "whatsapp") payload.waChannel = waChannelDescriptor(source.senderId);
20980
+ else payload.telegramChannel = telegramChannelDescriptor(source.senderId);
20755
20981
  } else {
20756
20982
  payload.channel = "webchat";
20757
20983
  payload.senderId = key;
@@ -20799,6 +21025,10 @@ app36.post("/", requireAdminSession, async (c) => {
20799
21025
  const authenticatedName = accountId ? await resolveAuthenticatedName(accountId, adminUserId) : void 0;
20800
21026
  const sourceMeta = readSourceRowMeta(fromSessionId);
20801
21027
  const source = sourceMeta ? { channel: sourceMeta.channel, senderId: sourceMeta.senderId, role: sourceMeta.role, accountId: sourceMeta.accountId } : null;
21028
+ if (isPublicChannelSource(source)) {
21029
+ console.error(`[chat-reseat] op=reseat-refused from=${fromSessionId} reason=public-channel channel=${source?.channel ?? "none"}`);
21030
+ return c.json({ error: "public channel re-seat not supported (would detach the seat)" }, 400);
21031
+ }
20802
21032
  const plan = resolveReseatPlan(
20803
21033
  { fromSessionId, model, ...permissionMode ? { permissionMode } : {}, ...effort ? { effort } : {} },
20804
21034
  randomUUID12,
@@ -20851,19 +21081,21 @@ app36.post("/", requireAdminSession, async (c) => {
20851
21081
  if (isChannelFork) {
20852
21082
  const channel = plan.payload.channel;
20853
21083
  const senderId = plan.payload.senderId;
20854
- try {
20855
- const provenance = source?.accountId ? "source-account" : "boot-account-fallback";
20856
- const dir = source?.accountId ? listValidAccounts().find((a) => a.accountId === source.accountId)?.accountDir ?? null : resolveAccount()?.accountDir ?? null;
20857
- if (dir) {
20858
- writeChannelOverrideId(dir, channel, senderId, plan.sessionId);
20859
- console.log(`[chat-reseat] op=channel-forward reseatReqId=${reseatReqId} senderId=${senderId} from=${fromSessionId} to=${plan.sessionId} keyedOn=${provenance}`);
20860
- } else {
20861
- console.error(`[session-reseat] channel-forward-failed reason=no-account-dir accountId=${source?.accountId ?? "none"} senderId=${senderId}`);
21084
+ if (!shouldWriteChannelForward(source)) {
21085
+ console.error(`[chat-reseat] op=channel-forward-skipped reseatReqId=${reseatReqId} senderId=${senderId} from=${fromSessionId} reason=no-source-account`);
21086
+ } else {
21087
+ try {
21088
+ const dir = listValidAccounts().find((a) => a.accountId === source.accountId)?.accountDir ?? null;
21089
+ if (dir) {
21090
+ writeChannelOverrideId(dir, channel, senderId, plan.sessionId);
21091
+ console.log(`[chat-reseat] op=channel-forward reseatReqId=${reseatReqId} senderId=${senderId} from=${fromSessionId} to=${plan.sessionId} account=${source.accountId}`);
21092
+ } else {
21093
+ console.error(`[session-reseat] channel-forward-failed reason=no-account-dir accountId=${source.accountId} senderId=${senderId}`);
21094
+ }
21095
+ } catch (err) {
21096
+ console.error(`[session-reseat] channel-forward-failed err=${err instanceof Error ? err.message : String(err)} senderId=${senderId}`);
20862
21097
  }
20863
- } catch (err) {
20864
- console.error(`[session-reseat] channel-forward-failed err=${err instanceof Error ? err.message : String(err)} senderId=${senderId}`);
20865
21098
  }
20866
- console.log(`[chat-reseat] op=channel-fork-unbound reseatReqId=${reseatReqId} senderId=${senderId} session=${plan.sessionId} note=binds-on-next-inbound`);
20867
21099
  }
20868
21100
  const target = `/chat?session=${plan.sessionId}`;
20869
21101
  console.log(`[chat-reseat] op=acquired reseatReqId=${reseatReqId} pid=${parsed?.spawnedPid ?? "none"} new=${plan.sessionId}`);
@@ -21000,10 +21232,10 @@ var system_stats_default = app37;
21000
21232
 
21001
21233
  // server/routes/admin/health.ts
21002
21234
  import { existsSync as existsSync31, readFileSync as readFileSync35 } from "fs";
21003
- import { resolve as resolve26, join as join36 } from "path";
21004
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve26(process.cwd(), "..");
21235
+ import { resolve as resolve28, join as join38 } from "path";
21236
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve28(process.cwd(), "..");
21005
21237
  var brandHostname = "maxy";
21006
- var brandJsonPath = join36(PLATFORM_ROOT6, "config", "brand.json");
21238
+ var brandJsonPath = join38(PLATFORM_ROOT6, "config", "brand.json");
21007
21239
  if (existsSync31(brandJsonPath)) {
21008
21240
  try {
21009
21241
  const brand = JSON.parse(readFileSync35(brandJsonPath, "utf-8"));
@@ -21011,7 +21243,7 @@ if (existsSync31(brandJsonPath)) {
21011
21243
  } catch {
21012
21244
  }
21013
21245
  }
21014
- var VERSION_FILE = resolve26(PLATFORM_ROOT6, `config/.${brandHostname}-version`);
21246
+ var VERSION_FILE = resolve28(PLATFORM_ROOT6, `config/.${brandHostname}-version`);
21015
21247
  var PROCESS_STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
21016
21248
  var PROBE_TIMEOUT_MS = 1e3;
21017
21249
  function readVersion() {
@@ -21793,7 +22025,7 @@ var browser_default = app45;
21793
22025
 
21794
22026
  // server/routes/admin/calendar.ts
21795
22027
  import { existsSync as existsSync32 } from "fs";
21796
- import { join as join38 } from "path";
22028
+ import { join as join40 } from "path";
21797
22029
 
21798
22030
  // server/lib/calendar-ics.ts
21799
22031
  var CRLF = "\r\n";
@@ -21893,7 +22125,7 @@ function countVEvents(ics) {
21893
22125
 
21894
22126
  // server/lib/calendar-availability.ts
21895
22127
  import { readFileSync as readFileSync36 } from "fs";
21896
- import { join as join37 } from "path";
22128
+ import { join as join39 } from "path";
21897
22129
  var AVAILABILITY_FILENAME = "calendar-availability.json";
21898
22130
  var REQUIRED_KEYS = [
21899
22131
  "timezone",
@@ -21902,7 +22134,7 @@ var REQUIRED_KEYS = [
21902
22134
  "weekly"
21903
22135
  ];
21904
22136
  function readAvailabilityConfig(accountDir) {
21905
- const path3 = join37(accountDir, AVAILABILITY_FILENAME);
22137
+ const path3 = join39(accountDir, AVAILABILITY_FILENAME);
21906
22138
  let raw;
21907
22139
  try {
21908
22140
  raw = readFileSync36(path3, "utf-8");
@@ -22264,7 +22496,7 @@ app46.get("/booking-link", requireAdminSession, (c) => {
22264
22496
  console.error('[calendar] op=booking-link-fail err="no account configured"');
22265
22497
  return c.json({ bookingDomain: null });
22266
22498
  }
22267
- if (!existsSync32(join38(account.accountDir, AVAILABILITY_FILENAME))) {
22499
+ if (!existsSync32(join40(account.accountDir, AVAILABILITY_FILENAME))) {
22268
22500
  console.log("[calendar] op=booking-link bookingDomain=none source=availability-config");
22269
22501
  return c.json({ bookingDomain: null });
22270
22502
  }
@@ -22923,9 +23155,9 @@ var verify_token_default = app54;
22923
23155
 
22924
23156
  // app/lib/access-email.ts
22925
23157
  import { spawn as spawn2 } from "child_process";
22926
- import { resolve as resolve27 } from "path";
22927
- var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve27(process.cwd(), "..");
22928
- var SEND_SCRIPT = resolve27(
23158
+ import { resolve as resolve29 } from "path";
23159
+ var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve29(process.cwd(), "..");
23160
+ var SEND_SCRIPT = resolve29(
22929
23161
  PLATFORM_ROOT7,
22930
23162
  "plugins",
22931
23163
  "email",
@@ -23068,7 +23300,7 @@ var access_default = app56;
23068
23300
 
23069
23301
  // server/routes/sites.ts
23070
23302
  import { existsSync as existsSync33, readFileSync as readFileSync37, realpathSync as realpathSync7, statSync as statSync14 } from "fs";
23071
- import { resolve as resolve28 } from "path";
23303
+ import { resolve as resolve30 } from "path";
23072
23304
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
23073
23305
  var MIME = {
23074
23306
  ".html": "text/html; charset=utf-8",
@@ -23125,8 +23357,8 @@ app57.get("/:rel{.*}", (c) => {
23125
23357
  }
23126
23358
  segments.push(seg);
23127
23359
  }
23128
- const rootDir = resolve28(account.accountDir, "sites");
23129
- let filePath = segments.length === 0 ? rootDir : resolve28(rootDir, ...segments);
23360
+ const rootDir = resolve30(account.accountDir, "sites");
23361
+ let filePath = segments.length === 0 ? rootDir : resolve30(rootDir, ...segments);
23130
23362
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
23131
23363
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
23132
23364
  return c.text("Forbidden", 403);
@@ -23146,7 +23378,7 @@ app57.get("/:rel{.*}", (c) => {
23146
23378
  return c.redirect(target, 301);
23147
23379
  }
23148
23380
  if (stat9?.isDirectory()) {
23149
- filePath = resolve28(filePath, "index.html");
23381
+ filePath = resolve30(filePath, "index.html");
23150
23382
  }
23151
23383
  if (!filePath.startsWith(rootDir + "/")) {
23152
23384
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
@@ -23284,7 +23516,7 @@ var VISITOR_COOKIE_MAX_AGE_SECONDS = Math.floor(DEFAULT_TTL_MS / 1e3);
23284
23516
 
23285
23517
  // app/lib/brand-config.ts
23286
23518
  import { existsSync as existsSync34, readFileSync as readFileSync39 } from "fs";
23287
- import { join as join39 } from "path";
23519
+ import { join as join41 } from "path";
23288
23520
  var cached2 = null;
23289
23521
  var cachedAttempted = false;
23290
23522
  function readBrandConfig() {
@@ -23292,7 +23524,7 @@ function readBrandConfig() {
23292
23524
  cachedAttempted = true;
23293
23525
  const platformRoot5 = process.env.MAXY_PLATFORM_ROOT;
23294
23526
  if (!platformRoot5) return null;
23295
- const brandPath = join39(platformRoot5, "config", "brand.json");
23527
+ const brandPath = join41(platformRoot5, "config", "brand.json");
23296
23528
  if (!existsSync34(brandPath)) return null;
23297
23529
  try {
23298
23530
  cached2 = JSON.parse(readFileSync39(brandPath, "utf-8"));
@@ -23792,14 +24024,14 @@ async function writeEvent(opts) {
23792
24024
  var visitor_event_default = app60;
23793
24025
 
23794
24026
  // server/routes/session.ts
23795
- import { resolve as resolve29 } from "path";
24027
+ import { resolve as resolve31 } from "path";
23796
24028
  import { existsSync as existsSync35, writeFileSync as writeFileSync14, mkdirSync as mkdirSync9 } from "fs";
23797
24029
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23798
24030
  function writeBrandingCache(accountId, agentSlug, branding) {
23799
24031
  try {
23800
- const cacheDir = resolve29(MAXY_DIR, "branding-cache", accountId);
24032
+ const cacheDir = resolve31(MAXY_DIR, "branding-cache", accountId);
23801
24033
  mkdirSync9(cacheDir, { recursive: true });
23802
- writeFileSync14(resolve29(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
24034
+ writeFileSync14(resolve31(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
23803
24035
  } catch (err) {
23804
24036
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
23805
24037
  }
@@ -23876,8 +24108,8 @@ app61.post("/", async (c) => {
23876
24108
  }
23877
24109
  let agentConfig = null;
23878
24110
  if (account) {
23879
- const agentDir = resolve29(account.accountDir, "agents", agentSlug);
23880
- if (!existsSync35(agentDir) || !existsSync35(resolve29(agentDir, "config.json"))) {
24111
+ const agentDir = resolve31(account.accountDir, "agents", agentSlug);
24112
+ if (!existsSync35(agentDir) || !existsSync35(resolve31(agentDir, "config.json"))) {
23881
24113
  return c.json({ error: "Agent not found" }, 404);
23882
24114
  }
23883
24115
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -24127,7 +24359,7 @@ var calendar_public_default = app62;
24127
24359
  // server/routes/portal-fetch.ts
24128
24360
  import { createReadStream as createReadStream3 } from "fs";
24129
24361
  import { stat as stat6, readFile as readFile6, realpath } from "fs/promises";
24130
- import { join as join40, resolve as resolve30, sep as sep8 } from "path";
24362
+ import { join as join42, resolve as resolve32, sep as sep8 } from "path";
24131
24363
  import { Readable as Readable3 } from "stream";
24132
24364
 
24133
24365
  // ../plugins/cloudflare/bin/schema-exposed-dirs.mjs
@@ -24228,7 +24460,7 @@ ${expiresAtMs}`)
24228
24460
  return diff === 0;
24229
24461
  }
24230
24462
  async function readAccountSecret(accountId) {
24231
- const path3 = resolve30(DATA_ROOT, "accounts", accountId, "secrets", "data-portal.env");
24463
+ const path3 = resolve32(DATA_ROOT, "accounts", accountId, "secrets", "data-portal.env");
24232
24464
  let text;
24233
24465
  try {
24234
24466
  text = await readFile6(path3, "utf8");
@@ -24249,13 +24481,13 @@ function isValidAccountId2(value) {
24249
24481
  async function resolveFetchTarget(accountDir, relPath) {
24250
24482
  let schemaText = null;
24251
24483
  try {
24252
- schemaText = await readFile6(join40(accountDir, "SCHEMA.md"), "utf8");
24484
+ schemaText = await readFile6(join42(accountDir, "SCHEMA.md"), "utf8");
24253
24485
  } catch {
24254
24486
  schemaText = null;
24255
24487
  }
24256
24488
  const { exposed } = resolveExposedDirs(schemaText);
24257
24489
  if (exposed.length === 0) return { ok: false, reason: "not-exposed" };
24258
- const lexical = resolve30(accountDir, relPath);
24490
+ const lexical = resolve32(accountDir, relPath);
24259
24491
  let root;
24260
24492
  let absolute;
24261
24493
  try {
@@ -24293,7 +24525,7 @@ async function handle(c, headOnly) {
24293
24525
  console.error(`${TAG45} op=denied account=${q(accountId)} reason=secret`);
24294
24526
  return c.json({ ok: false, error: "denied" }, 401);
24295
24527
  }
24296
- const target = await resolveFetchTarget(resolve30(DATA_ROOT, "accounts", accountId), relPath);
24528
+ const target = await resolveFetchTarget(resolve32(DATA_ROOT, "accounts", accountId), relPath);
24297
24529
  if (!target.ok) {
24298
24530
  const op = target.reason === "enoent" ? "miss" : "denied";
24299
24531
  console.error(`${TAG45} op=${op} account=${accountId} reason=${target.reason}`);
@@ -24405,7 +24637,7 @@ function startTimeEntryCensus(openSession, opts = {}) {
24405
24637
 
24406
24638
  // app/lib/ledger-census.ts
24407
24639
  import { readdirSync as readdirSync25, readFileSync as readFileSync40, statSync as statSync15 } from "fs";
24408
- import { join as join41 } from "path";
24640
+ import { join as join43 } from "path";
24409
24641
 
24410
24642
  // ../lib/ledger-core/dist/reconcile.js
24411
24643
  var LEDGER_INDEX_NAMES = [
@@ -24459,7 +24691,7 @@ function countWriteRejects24h(logDir, now) {
24459
24691
  }
24460
24692
  let total = 0;
24461
24693
  for (const name of entries) {
24462
- const path3 = join41(logDir, name);
24694
+ const path3 = join43(logDir, name);
24463
24695
  try {
24464
24696
  if (now - statSync15(path3).mtimeMs > DAY_MS) continue;
24465
24697
  for (const line of readFileSync40(path3, "utf8").split("\n")) {
@@ -24731,7 +24963,7 @@ function startGraphHealthTimer() {
24731
24963
 
24732
24964
  // app/lib/file-watcher.ts
24733
24965
  import * as fsp2 from "fs/promises";
24734
- import { resolve as resolve31, sep as sep9 } from "path";
24966
+ import { resolve as resolve33, sep as sep9 } from "path";
24735
24967
  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;
24736
24968
  var DEFAULT_COALESCE_MS = 500;
24737
24969
  var ROOTS = ["accounts"];
@@ -24750,7 +24982,7 @@ async function startFileWatcher(opts = {}) {
24750
24982
  const dropFn = opts.drop ?? dropFileIndex;
24751
24983
  for (const r of ROOTS) {
24752
24984
  try {
24753
- await fsp2.mkdir(resolve31(dataRoot, r), { recursive: true });
24985
+ await fsp2.mkdir(resolve33(dataRoot, r), { recursive: true });
24754
24986
  } catch (err) {
24755
24987
  console.error(
24756
24988
  `[file-watcher] start-failed root="${r}" err="${err.message}" \u2014 index will be maintained by the 5-min reconcile backstop only`
@@ -24771,7 +25003,7 @@ async function startFileWatcher(opts = {}) {
24771
25003
  timers.set(relativePath, t);
24772
25004
  }
24773
25005
  async function runHook(relativePath, accountId) {
24774
- const absolute = resolve31(dataRoot, relativePath);
25006
+ const absolute = resolve33(dataRoot, relativePath);
24775
25007
  let exists = false;
24776
25008
  try {
24777
25009
  const st = await fsp2.stat(absolute);
@@ -24795,7 +25027,7 @@ async function startFileWatcher(opts = {}) {
24795
25027
  }
24796
25028
  }
24797
25029
  async function watchRoot(rootName) {
24798
- const absRoot = resolve31(dataRoot, rootName);
25030
+ const absRoot = resolve33(dataRoot, rootName);
24799
25031
  try {
24800
25032
  const iter = fsp2.watch(absRoot, { recursive: true, signal: controller.signal });
24801
25033
  for await (const event of iter) {
@@ -24834,7 +25066,7 @@ async function startFileWatcher(opts = {}) {
24834
25066
 
24835
25067
  // app/lib/migrate-uploads.ts
24836
25068
  import { mkdir as mkdir5, readdir as readdir5, rename as rename2, rm as rm3 } from "fs/promises";
24837
- import { dirname as dirname13, relative as relative6, resolve as resolve32 } from "path";
25069
+ import { dirname as dirname13, relative as relative6, resolve as resolve34 } from "path";
24838
25070
  var ACCOUNT_UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24839
25071
  async function walkFiles(dir) {
24840
25072
  const out = [];
@@ -24845,7 +25077,7 @@ async function walkFiles(dir) {
24845
25077
  return out;
24846
25078
  }
24847
25079
  for (const e of entries) {
24848
- const abs = resolve32(dir, e.name);
25080
+ const abs = resolve34(dir, e.name);
24849
25081
  if (e.isDirectory()) {
24850
25082
  out.push(...await walkFiles(abs));
24851
25083
  } else if (e.isFile()) {
@@ -24865,7 +25097,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
24865
25097
  let moved = 0;
24866
25098
  for (const oldAbs of files) {
24867
25099
  const suffix = relative6(srcDir, oldAbs);
24868
- const newAbs = resolve32(destDir, suffix);
25100
+ const newAbs = resolve34(destDir, suffix);
24869
25101
  await mkdir5(dirname13(newAbs), { recursive: true });
24870
25102
  await rename2(oldAbs, newAbs);
24871
25103
  moved++;
@@ -24892,7 +25124,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
24892
25124
  }
24893
25125
  async function migrateUploads(opts = {}) {
24894
25126
  const dataRoot = opts.dataRoot ?? DATA_ROOT;
24895
- const oldRoot = resolve32(dataRoot, "uploads");
25127
+ const oldRoot = resolve34(dataRoot, "uploads");
24896
25128
  let topEntries;
24897
25129
  try {
24898
25130
  topEntries = await readdir5(oldRoot, { withFileTypes: true });
@@ -24913,8 +25145,8 @@ async function migrateUploads(opts = {}) {
24913
25145
  const name = entry.name;
24914
25146
  if (ACCOUNT_UUID_RE5.test(name)) {
24915
25147
  moved += await relocateTree(
24916
- resolve32(oldRoot, name),
24917
- resolve32(dataRoot, "accounts", name, "uploads"),
25148
+ resolve34(oldRoot, name),
25149
+ resolve34(dataRoot, "accounts", name, "uploads"),
24918
25150
  dataRoot,
24919
25151
  name,
24920
25152
  session
@@ -24926,8 +25158,8 @@ async function migrateUploads(opts = {}) {
24926
25158
  continue;
24927
25159
  }
24928
25160
  moved += await relocateTree(
24929
- resolve32(oldRoot, "public"),
24930
- resolve32(dataRoot, "accounts", installAccountId, "uploads", "public"),
25161
+ resolve34(oldRoot, "public"),
25162
+ resolve34(dataRoot, "accounts", installAccountId, "uploads", "public"),
24931
25163
  dataRoot,
24932
25164
  null,
24933
25165
  // public uploads carry no graph nodes
@@ -24958,7 +25190,7 @@ async function migrateUploads(opts = {}) {
24958
25190
  // app/lib/migrate-admin-webchat-sidecars.ts
24959
25191
  import { readdir as readdir6, readFile as readFile7, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
24960
25192
  import { existsSync as existsSync36 } from "fs";
24961
- import { join as join42 } from "path";
25193
+ import { join as join44 } from "path";
24962
25194
  var ADMIN_ROLE2 = "admin";
24963
25195
  var WEBCHAT_CHANNEL2 = "webchat";
24964
25196
  var SESSION_META_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.meta\.json$/i;
@@ -25003,7 +25235,7 @@ async function collectLiveSidecars(projectsRoot) {
25003
25235
  }
25004
25236
  for (const slug of slugs) {
25005
25237
  if (!slug.isDirectory()) continue;
25006
- const slugDir = join42(projectsRoot, slug.name);
25238
+ const slugDir = join44(projectsRoot, slug.name);
25007
25239
  let entries;
25008
25240
  try {
25009
25241
  entries = await readdir6(slugDir, { withFileTypes: true });
@@ -25012,9 +25244,9 @@ async function collectLiveSidecars(projectsRoot) {
25012
25244
  }
25013
25245
  for (const entry of entries) {
25014
25246
  if (entry.isFile() && SESSION_META_RE.test(entry.name)) {
25015
- out.push(join42(slugDir, entry.name));
25247
+ out.push(join44(slugDir, entry.name));
25016
25248
  } else if (entry.isDirectory() && entry.name === "subagents") {
25017
- const subDir = join42(slugDir, entry.name);
25249
+ const subDir = join44(slugDir, entry.name);
25018
25250
  let subs;
25019
25251
  try {
25020
25252
  subs = await readdir6(subDir, { withFileTypes: true });
@@ -25022,7 +25254,7 @@ async function collectLiveSidecars(projectsRoot) {
25022
25254
  continue;
25023
25255
  }
25024
25256
  for (const s of subs) {
25025
- if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join42(subDir, s.name));
25257
+ if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join44(subDir, s.name));
25026
25258
  }
25027
25259
  }
25028
25260
  }
@@ -25034,7 +25266,7 @@ function shortId(path3) {
25034
25266
  return base.slice(0, 8);
25035
25267
  }
25036
25268
  async function migrateAdminWebchatSidecars(opts = {}) {
25037
- const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join42(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
25269
+ const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join44(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
25038
25270
  const usersFile = opts.usersFile ?? USERS_FILE;
25039
25271
  const accountId = opts.accountId ?? resolveAccount()?.accountId ?? null;
25040
25272
  const resolveName = opts.resolveName ?? defaultResolveName;
@@ -25388,11 +25620,9 @@ function createWaChannelRoutes(deps) {
25388
25620
  void stream2.writeSSE({ data: JSON.stringify(payload) });
25389
25621
  });
25390
25622
  console.error(`[whatsapp-native] op=channel-attached senderId=${senderId}`);
25391
- deps.onAttached?.(senderId);
25392
25623
  stream2.onAbort(() => {
25393
25624
  deps.hub.detach(senderId);
25394
25625
  console.error(`[whatsapp-native] op=channel-detached senderId=${senderId}`);
25395
- deps.onDetached?.(senderId);
25396
25626
  });
25397
25627
  while (!stream2.aborted) {
25398
25628
  await stream2.sleep(15e3);
@@ -25533,31 +25763,16 @@ var WaGateway = class {
25533
25763
  docContexts = /* @__PURE__ */ new Map();
25534
25764
  spawning = /* @__PURE__ */ new Set();
25535
25765
  seq = 0;
25536
- /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
25537
- /** Task 1789senderId -> the ms at which its channel server last detached,
25538
- * cleared on attach. The standing audit reads this; nothing else does. */
25539
- detachedAt = /* @__PURE__ */ new Map();
25540
- /** Seat lost. `atMs` is injected so the audit's accounting is testable. */
25541
- recordDetached(senderId, atMs) {
25542
- this.detachedAt.set(senderId, atMs);
25543
- }
25544
- /** Seat regained — the sender is no longer unseated. */
25545
- recordAttached(senderId) {
25546
- this.detachedAt.delete(senderId);
25547
- }
25548
- /** Senders with no channel server for longer than `olderThanMs`. */
25549
- unseatedSenders(nowMs, olderThanMs) {
25550
- const out = [];
25551
- for (const [senderId, sinceMs] of this.detachedAt) {
25552
- if (nowMs - sinceMs > olderThanMs) out.push({ senderId, sinceMs });
25553
- }
25554
- return out.sort((a, b) => a.senderId.localeCompare(b.senderId));
25555
- }
25766
+ /** Hono sub-app exposing the loopback channel routes; mount on the UI app.
25767
+ * Task 1836 — the process-local unseated-sender census that used to live here
25768
+ * was retired. Its quiet state was ambiguous (a UI restart emptied it, so
25769
+ * silence meant "no detach seen since boot", not "every sender is seated") and
25770
+ * it grew unbounded. The genuine "no seat with pending work" signal is
25771
+ * InboundHub.sweep -> spawn-undelivered, which is driven by live queue state
25772
+ * and self-prunes. */
25556
25773
  routes() {
25557
25774
  return createWaChannelRoutes({
25558
25775
  hub: this.hub,
25559
- onAttached: (senderId) => this.recordAttached(senderId),
25560
- onDetached: (senderId) => this.recordDetached(senderId, Date.now()),
25561
25776
  sendOutbound: (senderId, text) => this.sendOutbound(senderId, text),
25562
25777
  sendDocument: (senderId, filePath, caption) => this.sendDocument(senderId, filePath, caption),
25563
25778
  onReady: (senderId) => {
@@ -25736,12 +25951,6 @@ function logDuplicateSenderGroups(groups) {
25736
25951
  );
25737
25952
  }
25738
25953
  }
25739
- function logUnseatedSenders(entries, nowMs) {
25740
- for (const e of entries) {
25741
- const secs = Math.round((nowMs - e.sinceMs) / 1e3);
25742
- console.error(`[whatsapp-native] op=sender-unseated senderId=${e.senderId} since=${new Date(e.sinceMs).toISOString()} forSeconds=${secs}`);
25743
- }
25744
- }
25745
25954
 
25746
25955
  // app/lib/whatsapp/gateway/spawn-request.ts
25747
25956
  function buildWaSpawnRequest(input) {
@@ -26097,16 +26306,16 @@ var WebchatGateway = class _WebchatGateway {
26097
26306
  * The public /api/chat route awaits this to return the reply on the same SSE
26098
26307
  * response, preserving the pre-756 POST→single-blob browser contract. */
26099
26308
  awaitReply(key, timeoutMs) {
26100
- return new Promise((resolve38) => {
26309
+ return new Promise((resolve39) => {
26101
26310
  const timer = setTimeout(() => {
26102
26311
  this.replyAwaiters.delete(key);
26103
- resolve38({ timeout: true });
26312
+ resolve39({ timeout: true });
26104
26313
  }, timeoutMs);
26105
26314
  if (timer.unref) timer.unref();
26106
26315
  this.replyAwaiters.set(key, (r) => {
26107
26316
  clearTimeout(timer);
26108
26317
  this.replyAwaiters.delete(key);
26109
- resolve38(r);
26318
+ resolve39(r);
26110
26319
  });
26111
26320
  });
26112
26321
  }
@@ -26117,11 +26326,11 @@ var WebchatGateway = class _WebchatGateway {
26117
26326
  * terminal dialog stays answerable). */
26118
26327
  awaitPermissionVerdict(p, timeoutMs) {
26119
26328
  const k = _WebchatGateway.promptKey(p.key, p.requestId);
26120
- return new Promise((resolve38) => {
26329
+ return new Promise((resolve39) => {
26121
26330
  this.pendingPrompts.get(k)?.resolve({ timeout: true });
26122
26331
  const timer = setTimeout(() => {
26123
26332
  this.pendingPrompts.delete(k);
26124
- resolve38({ timeout: true });
26333
+ resolve39({ timeout: true });
26125
26334
  }, timeoutMs);
26126
26335
  if (timer.unref) timer.unref();
26127
26336
  this.pendingPrompts.set(k, {
@@ -26130,7 +26339,7 @@ var WebchatGateway = class _WebchatGateway {
26130
26339
  resolve: (r) => {
26131
26340
  clearTimeout(timer);
26132
26341
  this.pendingPrompts.delete(k);
26133
- resolve38(r);
26342
+ resolve39(r);
26134
26343
  }
26135
26344
  });
26136
26345
  console.error(`[webchat:perm] op=open key=${keyDisplay(p.key)} id=${p.requestId} tool=${p.toolName}`);
@@ -26695,7 +26904,7 @@ async function fanOut(subscribers, text, onError, tag) {
26695
26904
 
26696
26905
  // app/lib/webchat/file-delivery.ts
26697
26906
  import { realpathSync as realpathSync8 } from "fs";
26698
- import { resolve as resolve33 } from "path";
26907
+ import { resolve as resolve35 } from "path";
26699
26908
 
26700
26909
  // app/lib/channel-pty-bridge/file-delivery.ts
26701
26910
  var SEND_USER_FILE = "SendUserFile";
@@ -26796,7 +27005,7 @@ function makeWebchatSendFile(entry) {
26796
27005
  console.error(`${TAG49} file-delivery reject reason=no-account sender=${entry.senderId}`);
26797
27006
  return { ok: false, error: "no-account" };
26798
27007
  }
26799
- const accountDir = resolve33(platformRoot2(), "..", "data/accounts", entry.accountId);
27008
+ const accountDir = resolve35(platformRoot2(), "..", "data/accounts", entry.accountId);
26800
27009
  try {
26801
27010
  const resolved = realpathSync8(filePath);
26802
27011
  const accountResolved = realpathSync8(accountDir);
@@ -27363,7 +27572,7 @@ function buildTelegramSpawnRequest(input) {
27363
27572
  // app/lib/telegram/outbound/send-document.ts
27364
27573
  import { realpathSync as realpathSync9 } from "fs";
27365
27574
  import { readFile as readFile8, stat as stat8 } from "fs/promises";
27366
- import { resolve as resolve34, basename as basename11 } from "path";
27575
+ import { resolve as resolve36, basename as basename11 } from "path";
27367
27576
  var TAG51 = "[telegram:outbound]";
27368
27577
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
27369
27578
  async function sendTelegramDocument(input) {
@@ -27374,7 +27583,7 @@ async function sendTelegramDocument(input) {
27374
27583
  if (!maxyAccountId || !platformRoot5) {
27375
27584
  return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
27376
27585
  }
27377
- const accountDir = resolve34(platformRoot5, "..", "data/accounts", maxyAccountId);
27586
+ const accountDir = resolve36(platformRoot5, "..", "data/accounts", maxyAccountId);
27378
27587
  let resolvedPath;
27379
27588
  try {
27380
27589
  resolvedPath = realpathSync9(filePath);
@@ -27500,15 +27709,6 @@ function startTelegramNativeFileFollower(input) {
27500
27709
  });
27501
27710
  }
27502
27711
 
27503
- // server/telegram-descriptor.ts
27504
- import { resolve as resolve35, join as join43 } from "path";
27505
- function telegramGatewayUrl() {
27506
- return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
27507
- }
27508
- function telegramServerPath() {
27509
- return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve35(process.env.MAXY_PLATFORM_ROOT ?? join43(__dirname, ".."), "services/telegram-channel/dist/server.js");
27510
- }
27511
-
27512
27712
  // app/lib/channel-pty-bridge/public-session-end-review.ts
27513
27713
  import { randomUUID as randomUUID16 } from "crypto";
27514
27714
  var TAG53 = "[public-session-review]";
@@ -27794,7 +27994,7 @@ function broadcastAdminShutdown(reason) {
27794
27994
  // ../lib/entitlement/src/index.ts
27795
27995
  import { createPublicKey, createHash as createHash6, verify as cryptoVerify } from "crypto";
27796
27996
  import { existsSync as existsSync37, readFileSync as readFileSync41, statSync as statSync16 } from "fs";
27797
- import { resolve as resolve36 } from "path";
27997
+ import { resolve as resolve37 } from "path";
27798
27998
 
27799
27999
  // ../lib/entitlement/src/canonicalize.ts
27800
28000
  function canonicalize(value) {
@@ -27829,7 +28029,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
27829
28029
  var GRACE_DAYS = 7;
27830
28030
  var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
27831
28031
  function pubkeyPath(brand) {
27832
- return resolve36(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
28032
+ return resolve37(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
27833
28033
  }
27834
28034
  var memo = null;
27835
28035
  function memoKey(mtimeMs, account) {
@@ -27841,7 +28041,7 @@ function resolveEntitlement(brand, account) {
27841
28041
  if (brand.commercialMode !== true) {
27842
28042
  return logResolved(implicitTrust(account), null);
27843
28043
  }
27844
- const entitlementPath = resolve36(brand.configDir, "entitlement.json");
28044
+ const entitlementPath = resolve37(brand.configDir, "entitlement.json");
27845
28045
  if (!existsSync37(entitlementPath)) {
27846
28046
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
27847
28047
  }
@@ -28033,7 +28233,7 @@ function clientFrom(c) {
28033
28233
  }
28034
28234
  installMediaDownloadGuard();
28035
28235
  var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
28036
- var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join44(PLATFORM_ROOT8, "config", "brand.json") : "";
28236
+ var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join45(PLATFORM_ROOT8, "config", "brand.json") : "";
28037
28237
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
28038
28238
  if (BRAND_JSON_PATH && !existsSync38(BRAND_JSON_PATH)) {
28039
28239
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -28072,7 +28272,7 @@ var brandLoginOpts = {
28072
28272
  appleTouchIconPath: brandAppIcon512Path,
28073
28273
  themeColor: BRAND.defaultColors?.primary
28074
28274
  };
28075
- var ALIAS_DOMAINS_PATH = join44(homedir4(), BRAND.configDir, "alias-domains.json");
28275
+ var ALIAS_DOMAINS_PATH = join45(homedir4(), BRAND.configDir, "alias-domains.json");
28076
28276
  function loadAliasDomains() {
28077
28277
  try {
28078
28278
  if (!existsSync38(ALIAS_DOMAINS_PATH)) return null;
@@ -28149,7 +28349,7 @@ var waGateway = new WaGateway({
28149
28349
  fetchStandingRules: fetchAccountStandingRules,
28150
28350
  resolveSessionOverride: (accountId, senderId) => resolveChannelOverride(accountId, "whatsapp", senderId),
28151
28351
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
28152
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve37(process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
28352
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve38(process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
28153
28353
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
28154
28354
  // path-validation scope) is the sender's effective SESSION account, resolved
28155
28355
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -28164,7 +28364,7 @@ var waGateway = new WaGateway({
28164
28364
  caption,
28165
28365
  accountId,
28166
28366
  maxyAccountId,
28167
- platformRoot: resolve37(process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, ".."))
28367
+ platformRoot: resolve38(process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, ".."))
28168
28368
  });
28169
28369
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
28170
28370
  },
@@ -28278,7 +28478,7 @@ function runDuplicateSenderAudit() {
28278
28478
  try {
28279
28479
  const configDir2 = process.env.CLAUDE_CONFIG_DIR;
28280
28480
  if (!configDir2) return;
28281
- const projectsRoot = join44(configDir2, "projects");
28481
+ const projectsRoot = join45(configDir2, "projects");
28282
28482
  const records = [];
28283
28483
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
28284
28484
  if (isSubagent || archived) continue;
@@ -28288,8 +28488,6 @@ function runDuplicateSenderAudit() {
28288
28488
  records.push({ sessionId, channel: meta.channel, senderId: meta.senderId, accountId: meta.accountId });
28289
28489
  }
28290
28490
  logDuplicateSenderGroups(findDuplicateSenderGroups(records));
28291
- const now = Date.now();
28292
- logUnseatedSenders(waGateway.unseatedSenders(now, 15 * 6e4), now);
28293
28491
  } catch (err) {
28294
28492
  console.error(`[whatsapp-native] op=duplicate-sender-audit-error error=${err instanceof Error ? err.message : String(err)}`);
28295
28493
  }
@@ -28300,6 +28498,7 @@ registerLoop({
28300
28498
  firstRunDelayMs: 0,
28301
28499
  run: runDuplicateSenderAudit
28302
28500
  });
28501
+ console.log("[1789-sequel] op=cluster-complete items=[1835,1836,1837,1839]");
28303
28502
  var webchatGateway = new WebchatGateway({
28304
28503
  fetchStandingRules: fetchAccountStandingRules,
28305
28504
  gatewayUrl: webchatGatewayUrl(),
@@ -28841,8 +29040,8 @@ app64.get("/agent-assets/:slug/:filename", (c) => {
28841
29040
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
28842
29041
  return c.text("Not found", 404);
28843
29042
  }
28844
- const filePath = resolve37(account.accountDir, "agents", slug, "assets", filename);
28845
- const expectedDir = resolve37(account.accountDir, "agents", slug, "assets");
29043
+ const filePath = resolve38(account.accountDir, "agents", slug, "assets", filename);
29044
+ const expectedDir = resolve38(account.accountDir, "agents", slug, "assets");
28846
29045
  if (!filePath.startsWith(expectedDir + "/")) {
28847
29046
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
28848
29047
  return c.text("Forbidden", 403);
@@ -28871,8 +29070,8 @@ app64.get("/generated/:filename", (c) => {
28871
29070
  console.error(`[generated] serve file=${filename} status=404`);
28872
29071
  return c.text("Not found", 404);
28873
29072
  }
28874
- const filePath = resolve37(account.accountDir, "generated", filename);
28875
- const expectedDir = resolve37(account.accountDir, "generated");
29073
+ const filePath = resolve38(account.accountDir, "generated", filename);
29074
+ const expectedDir = resolve38(account.accountDir, "generated");
28876
29075
  if (!filePath.startsWith(expectedDir + "/")) {
28877
29076
  console.error(`[generated] serve file=${filename} status=403`);
28878
29077
  return c.text("Forbidden", 403);
@@ -28926,11 +29125,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
28926
29125
  var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
28927
29126
  var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
28928
29127
  var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
28929
- (p) => existsSync38(resolve37(process.cwd(), "public", p.replace(/^\//, "")))
29128
+ (p) => existsSync38(resolve38(process.cwd(), "public", p.replace(/^\//, "")))
28930
29129
  );
28931
29130
  var SW_SOURCE = (() => {
28932
29131
  try {
28933
- return readFileSync42(resolve37(process.cwd(), "public", "sw.js"), "utf-8");
29132
+ return readFileSync42(resolve38(process.cwd(), "public", "sw.js"), "utf-8");
28934
29133
  } catch {
28935
29134
  return null;
28936
29135
  }
@@ -28938,7 +29137,7 @@ var SW_SOURCE = (() => {
28938
29137
  function readInstalledVersion() {
28939
29138
  try {
28940
29139
  if (!PLATFORM_ROOT8) return "unknown";
28941
- const versionFile = join44(PLATFORM_ROOT8, "config", `.${BRAND.hostname}-version`);
29140
+ const versionFile = join45(PLATFORM_ROOT8, "config", `.${BRAND.hostname}-version`);
28942
29141
  if (!existsSync38(versionFile)) return "unknown";
28943
29142
  const content = readFileSync42(versionFile, "utf-8").trim();
28944
29143
  return content || "unknown";
@@ -28981,7 +29180,7 @@ var clientErrorReporterScript = `<script>
28981
29180
  function cachedHtml(file) {
28982
29181
  let html = htmlCache.get(file);
28983
29182
  if (!html) {
28984
- html = readFileSync42(resolve37(process.cwd(), "public", file), "utf-8");
29183
+ html = readFileSync42(resolve38(process.cwd(), "public", file), "utf-8");
28985
29184
  const productNameEsc = escapeHtml(BRAND.productName);
28986
29185
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
28987
29186
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -28998,11 +29197,11 @@ ${clientErrorReporterScript}
28998
29197
  return html;
28999
29198
  }
29000
29199
  function loadBrandingCache(agentSlug) {
29001
- const configDir2 = join44(homedir4(), BRAND.configDir);
29200
+ const configDir2 = join45(homedir4(), BRAND.configDir);
29002
29201
  try {
29003
29202
  const accountId = getDefaultAccountId();
29004
29203
  if (!accountId) return null;
29005
- const cachePath = join44(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
29204
+ const cachePath = join45(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
29006
29205
  if (!existsSync38(cachePath)) return null;
29007
29206
  return JSON.parse(readFileSync42(cachePath, "utf-8"));
29008
29207
  } catch {
@@ -29099,7 +29298,7 @@ app64.use("/vnc-popout.html", logViewerFetch);
29099
29298
  app64.get("/vnc-popout.html", (c) => {
29100
29299
  let html = htmlCache.get("vnc-popout.html");
29101
29300
  if (!html) {
29102
- html = readFileSync42(resolve37(process.cwd(), "public", "vnc-popout.html"), "utf-8");
29301
+ html = readFileSync42(resolve38(process.cwd(), "public", "vnc-popout.html"), "utf-8");
29103
29302
  const name = escapeHtml(BRAND.productName);
29104
29303
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
29105
29304
  html = html.replace("</head>", ` ${brandScript}
@@ -29236,8 +29435,8 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
29236
29435
  var httpServer = serve({ fetch: app64.fetch, port, hostname });
29237
29436
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29238
29437
  {
29239
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29240
- const reconcileScript = resolve37(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
29438
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29439
+ const reconcileScript = resolve38(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
29241
29440
  const RECONCILE_INTERVAL_MS = 12e4;
29242
29441
  const runReconcile = (ctx) => {
29243
29442
  if (!existsSync38(reconcileScript)) return;
@@ -29262,8 +29461,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29262
29461
  });
29263
29462
  }
29264
29463
  {
29265
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29266
- const outlookScript = resolve37(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
29464
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29465
+ const outlookScript = resolve38(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
29267
29466
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
29268
29467
  const runOutlookComplete = (ctx) => {
29269
29468
  if (!existsSync38(outlookScript)) return;
@@ -29288,8 +29487,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29288
29487
  });
29289
29488
  }
29290
29489
  {
29291
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29292
- const strandedAccountsDir = resolve37(auditRoot, "..", "data/accounts");
29490
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29491
+ const strandedAccountsDir = resolve38(auditRoot, "..", "data/accounts");
29293
29492
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
29294
29493
  const STRANDED_AGE_MS = 16 * 6e4;
29295
29494
  const STRANDED_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -29299,7 +29498,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29299
29498
  const now = Date.now();
29300
29499
  for (const name of readdirSync26(strandedAccountsDir)) {
29301
29500
  if (!STRANDED_UUID_RE.test(name)) continue;
29302
- const pendingPath2 = resolve37(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
29501
+ const pendingPath2 = resolve38(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
29303
29502
  if (!existsSync38(pendingPath2)) continue;
29304
29503
  const ageMs = now - statSync17(pendingPath2).mtimeMs;
29305
29504
  if (ageMs > STRANDED_AGE_MS) {
@@ -29318,8 +29517,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29318
29517
  });
29319
29518
  }
29320
29519
  {
29321
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29322
- const googleAccountsDir = resolve37(googleRoot, "..", "data/accounts");
29520
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29521
+ const googleAccountsDir = resolve38(googleRoot, "..", "data/accounts");
29323
29522
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
29324
29523
  const runGooglePendingAuditSafe = () => {
29325
29524
  try {
@@ -29334,7 +29533,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29334
29533
  firstRunDelayMs: 27e3,
29335
29534
  run: runGooglePendingAuditSafe
29336
29535
  });
29337
- const googleAuditScript = resolve37(googleRoot, "plugins/google/mcp/dist/scripts/account-audit.js");
29536
+ const googleAuditScript = resolve38(googleRoot, "plugins/google/mcp/dist/scripts/account-audit.js");
29338
29537
  const GOOGLE_ACCOUNT_AUDIT_INTERVAL_MS = 36e5;
29339
29538
  const runGoogleAccountAudit = (ctx) => {
29340
29539
  if (!existsSync38(googleAuditScript)) return;
@@ -29362,7 +29561,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29362
29561
  });
29363
29562
  }
29364
29563
  {
29365
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29564
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29366
29565
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
29367
29566
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
29368
29567
  console.error(`[storage-audit] error="${err.message}"`);
@@ -29396,8 +29595,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29396
29595
  });
29397
29596
  }
29398
29597
  {
29399
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29400
- const publishScript = resolve37(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
29598
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29599
+ const publishScript = resolve38(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
29401
29600
  const PUBLISH_INTERVAL_MS = 3e5;
29402
29601
  const currentAccount = () => {
29403
29602
  try {
@@ -29427,7 +29626,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29427
29626
  }
29428
29627
  };
29429
29628
  const auditSnapshotAge = (account) => {
29430
- const availPath = resolve37(account.accountDir, "calendar-availability.json");
29629
+ const availPath = resolve38(account.accountDir, "calendar-availability.json");
29431
29630
  let hasBookingSite = false;
29432
29631
  try {
29433
29632
  if (existsSync38(availPath)) {
@@ -29437,7 +29636,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29437
29636
  } catch {
29438
29637
  }
29439
29638
  if (!hasBookingSite) return;
29440
- const statePath = resolve37(account.accountDir, "state", "booking-availability", "last-publish.json");
29639
+ const statePath = resolve38(account.accountDir, "state", "booking-availability", "last-publish.json");
29441
29640
  let lastSuccessAt = null;
29442
29641
  if (existsSync38(statePath)) {
29443
29642
  try {
@@ -29469,8 +29668,8 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
29469
29668
  startTimeEntryCensus(() => getSession());
29470
29669
  startLedgerCensus(() => getSession());
29471
29670
  {
29472
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..");
29473
- const auditScript = resolve37(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
29671
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..");
29672
+ const auditScript = resolve38(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
29474
29673
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
29475
29674
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
29476
29675
  const runConnectorAudit = (ctx) => {
@@ -29779,9 +29978,13 @@ if (bootAccountConfig?.whatsapp) {
29779
29978
  }
29780
29979
  init({
29781
29980
  configDir: configDirForWhatsApp,
29782
- platformRoot: resolve37(process.env.MAXY_PLATFORM_ROOT ?? join44(__dirname, "..")),
29981
+ platformRoot: resolve38(process.env.MAXY_PLATFORM_ROOT ?? join45(__dirname, "..")),
29783
29982
  accountConfig: bootAccountConfig,
29784
29983
  onMessage: async (msg) => {
29984
+ if (isObserveAccount(msg.accountId)) {
29985
+ console.error(`[whatsapp:observe] op=BREACH account=${msg.accountId} sender=${msg.senderPhone} \u2014 dispatch reached the router from an observe socket`);
29986
+ return;
29987
+ }
29785
29988
  if (msg.isOwnerMirror) {
29786
29989
  console.error(`[whatsapp:route] skipped reason=owner-mirror senderId=${msg.senderPhone}`);
29787
29990
  return;