@rubytech/create-maxy-code 0.1.386 → 0.1.387

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 (110) hide show
  1. package/dist/__tests__/cron-heartbeat-registration.test.js +5 -0
  2. package/dist/cron-registration.js +1 -0
  3. package/dist/index.js +4 -0
  4. package/package.json +1 -1
  5. package/payload/platform/neo4j/schema.cypher +36 -1
  6. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +61 -1
  8. package/payload/platform/plugins/docs/references/graph.md +11 -0
  9. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -0
  10. package/payload/platform/plugins/docs/references/sweep-guide.md +43 -0
  11. package/payload/platform/plugins/memory/references/schema-estate-agent.md +34 -0
  12. package/payload/platform/plugins/scheduling/PLUGIN.md +17 -1
  13. package/payload/platform/plugins/scheduling/mcp/dist/index.js +12 -0
  14. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.d.ts +2 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js +101 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js.map +1 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts +54 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts.map +1 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js +117 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js.map +1 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.d.ts +7 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.d.ts.map +1 -1
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.js +6 -3
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.js.map +1 -1
  27. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.d.ts +2 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.d.ts.map +1 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +88 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.d.ts +2 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.d.ts.map +1 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.js +77 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.js.map +1 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +59 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +131 -0
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -0
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +62 -15
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  41. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.d.ts +2 -0
  42. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.d.ts.map +1 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +110 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.d.ts +2 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.d.ts.map +1 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.js +35 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.js.map +1 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.d.ts +2 -0
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.d.ts.map +1 -0
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.js +81 -0
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.js.map +1 -0
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +7 -0
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -4
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -0
  58. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  59. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +6 -0
  60. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  61. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts +2 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  63. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +32 -1
  64. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  65. package/payload/platform/plugins/sweep/.claude-plugin/plugin.json +8 -0
  66. package/payload/platform/plugins/sweep/PLUGIN.md +15 -0
  67. package/payload/platform/plugins/sweep/skills/sweep-workflow/SKILL.md +113 -0
  68. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  69. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +19 -4
  70. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  71. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -4
  72. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +10 -0
  73. package/payload/platform/scripts/seed-neo4j.sh +38 -0
  74. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +5 -0
  75. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  76. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  77. package/payload/platform/services/whatsapp-channel/dist/server.js +1 -1
  78. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  79. package/payload/server/public/assets/{AdminLoginScreens-oxqHKL3C.js → AdminLoginScreens-BejIjbmU.js} +1 -1
  80. package/payload/server/public/assets/{AdminShell-DBD6ruS1.js → AdminShell-D2-uBSB5.js} +1 -1
  81. package/payload/server/public/assets/{Checkbox-yr8-ftrE.js → Checkbox-1F1tzqca.js} +1 -1
  82. package/payload/server/public/assets/Transcript-DkGa4CQH.js +2 -0
  83. package/payload/server/public/assets/{admin-CQfZnuFy.js → admin-DqQARkjy.js} +1 -1
  84. package/payload/server/public/assets/{browser-DbdPeDK1.js → browser-nDtEK6RC.js} +1 -1
  85. package/payload/server/public/assets/{calendar-CFdOhe7Q.js → calendar-CO4Bwmho.js} +1 -1
  86. package/payload/server/public/assets/chat-DeIge_bC.js +1 -0
  87. package/payload/server/public/assets/chevron-left-DhVdq0aN.js +1 -0
  88. package/payload/server/public/assets/data-B1GIdzHk.js +1 -0
  89. package/payload/server/public/assets/{graph-Bluocsf_.js → graph-DFyicKd7.js} +1 -1
  90. package/payload/server/public/assets/{graph-labels-BcLJmY-v.js → graph-labels-D3BQdcpD.js} +1 -1
  91. package/payload/server/public/assets/{operator-DjK3MBSS.js → operator-BxrjWdT9.js} +1 -1
  92. package/payload/server/public/assets/{page-DQNYTb2d.js → page-ByDLq_o1.js} +1 -1
  93. package/payload/server/public/assets/{page-zJQTRXB2.js → page-D-Ep4bXd.js} +1 -1
  94. package/payload/server/public/assets/{public-CHCniosp.js → public-DbdqfdYq.js} +1 -1
  95. package/payload/server/public/assets/{rotate-ccw-JDfwjfIL.js → rotate-ccw-BkX8HODe.js} +1 -1
  96. package/payload/server/public/assets/{useSubAccountSwitcher-CfI3J2IX.css → useSubAccountSwitcher-DS0iqSkP.css} +1 -1
  97. package/payload/server/public/browser.html +4 -4
  98. package/payload/server/public/calendar.html +5 -5
  99. package/payload/server/public/chat.html +8 -8
  100. package/payload/server/public/data.html +7 -7
  101. package/payload/server/public/graph.html +8 -8
  102. package/payload/server/public/index.html +10 -10
  103. package/payload/server/public/operator.html +10 -10
  104. package/payload/server/public/public.html +8 -8
  105. package/payload/server/server.js +551 -271
  106. package/payload/server/public/assets/Transcript-FX_w6no6.js +0 -2
  107. package/payload/server/public/assets/chat-BunxcNDk.js +0 -1
  108. package/payload/server/public/assets/chevron-left-Cngmu0Py.js +0 -1
  109. package/payload/server/public/assets/data-IqZlMSwG.js +0 -1
  110. /package/payload/server/public/assets/{useSubAccountSwitcher-BpQ3oKiJ.js → useSubAccountSwitcher-DMbRhLPv.js} +0 -0
@@ -1727,6 +1727,7 @@ var WhatsAppConfigSchema = z.object({
1727
1727
  accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
1728
1728
  dmPolicy: DmPolicySchema.optional().default("disabled").describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
1729
1729
  adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
1730
+ accountManagers: z.record(z.string(), z.string()).optional().describe("Account-manager bindings: E.164 phone \u2192 sub-account UUID. A designated phone spawns an admin session scoped to that sub-account rather than the house. Managed via add-account-manager / remove-account-manager. A phone here must not also be in adminPhones."),
1730
1731
  allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
1731
1732
  sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
1732
1733
  mediaMaxMb: z.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
@@ -2601,13 +2602,24 @@ function extractMessage(msg) {
2601
2602
  return result;
2602
2603
  }
2603
2604
 
2605
+ // app/lib/whatsapp/managed-account.ts
2606
+ function managedAccountFor(accountManagers, phone) {
2607
+ for (const [managerPhone, subAccountId] of Object.entries(accountManagers)) {
2608
+ if (subAccountId && phonesMatch(managerPhone, phone)) return subAccountId;
2609
+ }
2610
+ return null;
2611
+ }
2612
+
2604
2613
  // app/lib/whatsapp/inbound/access-control.ts
2605
2614
  function resolveAccountConfig(config, accountConfig) {
2606
2615
  return {
2607
2616
  dmPolicy: accountConfig?.dmPolicy ?? config.dmPolicy ?? "disabled",
2608
2617
  adminPhones: accountConfig?.adminPhones ?? config.adminPhones ?? [],
2609
2618
  allowFrom: accountConfig?.allowFrom ?? config.allowFrom ?? [],
2610
- selfChatMode: accountConfig?.selfChatMode ?? false
2619
+ selfChatMode: accountConfig?.selfChatMode ?? false,
2620
+ // Account-manager bindings are a house-level routing table only — never a
2621
+ // per-account override, so this reads the top-level map unconditionally.
2622
+ accountManagers: config.accountManagers ?? {}
2611
2623
  };
2612
2624
  }
2613
2625
  function isAdminPhone(phone, adminPhones) {
@@ -2624,6 +2636,9 @@ function checkDmAccess(params) {
2624
2636
  if (isAdminPhone(senderPhone, cfg.adminPhones)) {
2625
2637
  return { allowed: true, reason: "admin-binding", agentType: "admin" };
2626
2638
  }
2639
+ if (managedAccountFor(cfg.accountManagers, senderPhone)) {
2640
+ return { allowed: true, reason: "account-manager", agentType: "admin" };
2641
+ }
2627
2642
  switch (cfg.dmPolicy) {
2628
2643
  case "open":
2629
2644
  return { allowed: true, reason: "dm-policy-open", agentType: "public" };
@@ -5971,6 +5986,131 @@ ${result.result.text}` : result.result.text;
5971
5986
  return app56;
5972
5987
  }
5973
5988
 
5989
+ // app/lib/channel-pty-bridge/admin-session-id.ts
5990
+ import { createHash } from "crypto";
5991
+ function adminSessionIdFor(accountId, senderId, personId) {
5992
+ const subject = personId && personId.length > 0 ? `person:${personId}` : senderId;
5993
+ const b = createHash("sha256").update(`${accountId}:admin:${subject}`).digest().subarray(0, 16);
5994
+ const v = Buffer.from(b);
5995
+ v[6] = v[6] & 15 | 64;
5996
+ v[8] = v[8] & 63 | 128;
5997
+ const h = v.toString("hex");
5998
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
5999
+ }
6000
+
6001
+ // server/routes/channel/schedule-inject.ts
6002
+ var TAG16 = "[schedule-inject]";
6003
+ function isLoopbackAddr(addr) {
6004
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
6005
+ }
6006
+ function createScheduleInjectRoutes(deps) {
6007
+ const app56 = new Hono();
6008
+ app56.post("/", async (c) => {
6009
+ const env = c.env;
6010
+ const remoteAddr = env?.incoming?.socket?.remoteAddress ?? "";
6011
+ if (!isLoopbackAddr(remoteAddr)) {
6012
+ console.error(`${TAG16} reject reason=non-loopback remoteAddr=${remoteAddr}`);
6013
+ return c.json({ ok: false, error: "schedule-inject-loopback-only" }, 403);
6014
+ }
6015
+ const xffHeader = env?.incoming?.headers?.["x-forwarded-for"];
6016
+ const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
6017
+ if (xffRaw.length > 0) {
6018
+ const offender = xffRaw.split(",").map((t) => t.trim()).filter(Boolean).find((t) => !isLoopbackAddr(t));
6019
+ if (offender !== void 0) {
6020
+ console.error(`${TAG16} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
6021
+ return c.json({ ok: false, error: "schedule-inject-loopback-only" }, 403);
6022
+ }
6023
+ }
6024
+ let body;
6025
+ try {
6026
+ body = await c.req.json();
6027
+ } catch {
6028
+ return c.json({ ok: false, error: "invalid-json" }, 400);
6029
+ }
6030
+ const channel = body.channel;
6031
+ const destination = typeof body.destination === "string" ? body.destination : "";
6032
+ const prompt = typeof body.prompt === "string" ? body.prompt : "";
6033
+ const eventId = typeof body.eventId === "string" ? body.eventId : "";
6034
+ if (channel !== "whatsapp" && channel !== "telegram" || !destination || !prompt) {
6035
+ console.error(`${TAG16} reject reason=bad-request channel=${String(channel)} hasDestination=${Boolean(destination)} hasPrompt=${Boolean(prompt)}`);
6036
+ return c.json({ ok: false, error: "schedule-inject-bad-request" }, 400);
6037
+ }
6038
+ const account = deps.resolveAccount();
6039
+ if (!account) {
6040
+ console.error(`${TAG16} reject reason=no-account eventId=${eventId}`);
6041
+ return c.json({ ok: false, error: "no-account" }, 500);
6042
+ }
6043
+ const houseAccountId = account.accountId;
6044
+ console.error(`${TAG16} op=received eventId=${eventId} channel=${channel} destination=${destination} account=${houseAccountId}`);
6045
+ if (channel === "whatsapp") {
6046
+ const effectiveAccount2 = deps.effectiveAccountFor(houseAccountId, account.accountDir, destination);
6047
+ console.error(`${TAG16} op=effective-account eventId=${eventId} effectiveAccount=${effectiveAccount2}`);
6048
+ const sessionId2 = adminSessionIdFor(effectiveAccount2, destination);
6049
+ const reply2 = (text) => deps.sendWhatsAppText(houseAccountId, destination, text);
6050
+ try {
6051
+ await deps.waHandleInbound({
6052
+ accountId: houseAccountId,
6053
+ senderId: destination,
6054
+ role: "admin",
6055
+ personId: null,
6056
+ text: prompt,
6057
+ media: [],
6058
+ source: "schedule",
6059
+ reply: reply2
6060
+ });
6061
+ } catch (err) {
6062
+ console.error(`${TAG16} op=inject-spawn eventId=${eventId} sessionId=${sessionId2} result=inject-error status=${err instanceof Error ? err.message : String(err)}`);
6063
+ return c.json({ ok: false, error: "inject-error" }, 502);
6064
+ }
6065
+ console.error(`${TAG16} op=inject-spawn eventId=${eventId} sessionId=${sessionId2} result=ok status=-`);
6066
+ return c.json({ ok: true }, 200);
6067
+ }
6068
+ const effectiveAccount = houseAccountId;
6069
+ console.error(`${TAG16} op=effective-account eventId=${eventId} effectiveAccount=${effectiveAccount}`);
6070
+ const botToken = account.config.telegram?.adminBotToken;
6071
+ if (!botToken) {
6072
+ console.error(`${TAG16} reject reason=no-admin-bot-token eventId=${eventId}`);
6073
+ return c.json({ ok: false, error: "no-admin-bot-token" }, 400);
6074
+ }
6075
+ const chatId = Number(destination);
6076
+ const sessionId = adminSessionIdFor(effectiveAccount, destination);
6077
+ const reply = (text) => deps.sendTelegramText(botToken, chatId, text);
6078
+ try {
6079
+ await deps.tgHandleInbound({
6080
+ accountId: houseAccountId,
6081
+ senderId: destination,
6082
+ chatId: destination,
6083
+ role: "admin",
6084
+ personId: null,
6085
+ text: prompt,
6086
+ source: "schedule",
6087
+ reply
6088
+ });
6089
+ } catch (err) {
6090
+ console.error(`${TAG16} op=inject-spawn eventId=${eventId} sessionId=${sessionId} result=inject-error status=${err instanceof Error ? err.message : String(err)}`);
6091
+ return c.json({ ok: false, error: "inject-error" }, 502);
6092
+ }
6093
+ console.error(`${TAG16} op=inject-spawn eventId=${eventId} sessionId=${sessionId} result=ok status=-`);
6094
+ return c.json({ ok: true }, 200);
6095
+ });
6096
+ return app56;
6097
+ }
6098
+
6099
+ // app/lib/telegram/outbound/send-text.ts
6100
+ async function sendTelegramText(botToken, chatId, text) {
6101
+ try {
6102
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
6103
+ method: "POST",
6104
+ headers: { "Content-Type": "application/json" },
6105
+ body: JSON.stringify({ chat_id: chatId, text })
6106
+ });
6107
+ const data = await res.json();
6108
+ return data.ok ? { ok: true } : { ok: false, error: data.description ?? "send failed" };
6109
+ } catch (e) {
6110
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
6111
+ }
6112
+ }
6113
+
5974
6114
  // server/routes/whatsapp.ts
5975
6115
  import { join as join10, resolve as resolve10 } from "path";
5976
6116
  import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync6 } from "fs";
@@ -5981,7 +6121,7 @@ import { randomUUID as randomUUID6 } from "crypto";
5981
6121
  // app/lib/whatsapp/config-persist.ts
5982
6122
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
5983
6123
  import { resolve as resolve8, join as join8 } from "path";
5984
- var TAG16 = "[whatsapp:config]";
6124
+ var TAG17 = "[whatsapp:config]";
5985
6125
  function configPath(accountDir) {
5986
6126
  return resolve8(accountDir, "account.json");
5987
6127
  }
@@ -5998,9 +6138,9 @@ function reloadManagerConfig(accountDir) {
5998
6138
  try {
5999
6139
  const config = readConfig(accountDir);
6000
6140
  reloadConfig(config);
6001
- console.error(`${TAG16} reloaded manager config`);
6141
+ console.error(`${TAG17} reloaded manager config`);
6002
6142
  } catch (err) {
6003
- console.error(`${TAG16} manager config reload failed: ${String(err)}`);
6143
+ console.error(`${TAG17} manager config reload failed: ${String(err)}`);
6004
6144
  }
6005
6145
  }
6006
6146
  var E164_PATTERN = /^\+\d{7,15}$/;
@@ -6026,25 +6166,25 @@ function persistAfterPairing(accountDir, accountId, selfPhone) {
6026
6166
  const adminPhones = wa.adminPhones;
6027
6167
  if (!adminPhones.includes(normalized)) {
6028
6168
  adminPhones.push(normalized);
6029
- console.error(`${TAG16} added selfPhone=${normalized} to adminPhones`);
6169
+ console.error(`${TAG17} added selfPhone=${normalized} to adminPhones`);
6030
6170
  }
6031
6171
  } else {
6032
- console.error(`${TAG16} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
6172
+ console.error(`${TAG17} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
6033
6173
  }
6034
6174
  const parsed = WhatsAppConfigSchema.safeParse(wa);
6035
6175
  if (!parsed.success) {
6036
6176
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6037
- console.error(`${TAG16} validation failed after pairing: ${msg}`);
6177
+ console.error(`${TAG17} validation failed after pairing: ${msg}`);
6038
6178
  return { ok: false, error: `Validation failed: ${msg}` };
6039
6179
  }
6040
6180
  config.whatsapp = parsed.data;
6041
6181
  writeConfig(accountDir, config);
6042
- console.error(`${TAG16} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
6182
+ console.error(`${TAG17} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
6043
6183
  reloadManagerConfig(accountDir);
6044
6184
  return { ok: true };
6045
6185
  } catch (err) {
6046
6186
  const msg = err instanceof Error ? err.message : String(err);
6047
- console.error(`${TAG16} persist failed account=${accountId}: ${msg}`);
6187
+ console.error(`${TAG17} persist failed account=${accountId}: ${msg}`);
6048
6188
  return { ok: false, error: msg };
6049
6189
  }
6050
6190
  }
@@ -6066,6 +6206,9 @@ function addAdminPhone(accountDir, phone) {
6066
6206
  if (adminPhones.includes(normalized)) {
6067
6207
  return { ok: true, message: `Phone ${normalized} is already in the admin list.` };
6068
6208
  }
6209
+ if (managedAccountFor(readAccountManagers(accountDir), normalized)) {
6210
+ return { ok: false, error: `${normalized} is bound as an account manager. Remove the account-manager binding before adding it as a house admin \u2014 a phone is a house admin or a sub-account manager, never both.` };
6211
+ }
6069
6212
  adminPhones.push(normalized);
6070
6213
  const parsed = WhatsAppConfigSchema.safeParse(wa);
6071
6214
  if (!parsed.success) {
@@ -6074,12 +6217,12 @@ function addAdminPhone(accountDir, phone) {
6074
6217
  }
6075
6218
  config.whatsapp = parsed.data;
6076
6219
  writeConfig(accountDir, config);
6077
- console.error(`${TAG16} added admin phone=${normalized}`);
6220
+ console.error(`${TAG17} added admin phone=${normalized}`);
6078
6221
  reloadManagerConfig(accountDir);
6079
6222
  return { ok: true, message: `Added ${normalized} as admin phone. Messages from this number will route to the admin agent.` };
6080
6223
  } catch (err) {
6081
6224
  const msg = err instanceof Error ? err.message : String(err);
6082
- console.error(`${TAG16} addAdminPhone failed: ${msg}`);
6225
+ console.error(`${TAG17} addAdminPhone failed: ${msg}`);
6083
6226
  return { ok: false, error: msg };
6084
6227
  }
6085
6228
  }
@@ -6107,12 +6250,12 @@ function removeAdminPhone(accountDir, phone) {
6107
6250
  }
6108
6251
  config.whatsapp = parsed.data;
6109
6252
  writeConfig(accountDir, config);
6110
- console.error(`${TAG16} removed admin phone=${normalized}`);
6253
+ console.error(`${TAG17} removed admin phone=${normalized}`);
6111
6254
  reloadManagerConfig(accountDir);
6112
6255
  return { ok: true, message: `Removed ${normalized} from admin phones. Messages from this number will now route to the public agent.` };
6113
6256
  } catch (err) {
6114
6257
  const msg = err instanceof Error ? err.message : String(err);
6115
- console.error(`${TAG16} removeAdminPhone failed: ${msg}`);
6258
+ console.error(`${TAG17} removeAdminPhone failed: ${msg}`);
6116
6259
  return { ok: false, error: msg };
6117
6260
  }
6118
6261
  }
@@ -6127,6 +6270,101 @@ function readAdminPhones(accountDir) {
6127
6270
  return [];
6128
6271
  }
6129
6272
  }
6273
+ function setAccountManager(accountDir, phone, subAccountId) {
6274
+ const normalized = phone.trim();
6275
+ if (!E164_PATTERN.test(normalized)) {
6276
+ return { ok: false, error: `Invalid phone format "${normalized}". Expected E.164 (e.g. +441234567890).` };
6277
+ }
6278
+ const sub = subAccountId.trim();
6279
+ if (!sub) {
6280
+ return { ok: false, error: "Missing sub-account id." };
6281
+ }
6282
+ let validIds;
6283
+ try {
6284
+ validIds = listValidAccounts().map((a) => a.accountId);
6285
+ } catch (err) {
6286
+ return { ok: false, error: `Could not read the account registry to validate the sub-account: ${err instanceof Error ? err.message : String(err)}. Try again.` };
6287
+ }
6288
+ if (!validIds.includes(sub)) {
6289
+ return { ok: false, error: `Sub-account "${sub}" is not in this install's account registry. Check the sub-account id and try again.` };
6290
+ }
6291
+ try {
6292
+ const config = readConfig(accountDir);
6293
+ if (!config.whatsapp || typeof config.whatsapp !== "object") {
6294
+ config.whatsapp = {};
6295
+ }
6296
+ const wa = config.whatsapp;
6297
+ const adminPhones = Array.isArray(wa.adminPhones) ? wa.adminPhones.filter((p) => typeof p === "string") : [];
6298
+ if (isAdminPhone(normalized, adminPhones)) {
6299
+ return {
6300
+ ok: false,
6301
+ error: `${normalized} is a house admin phone. Remove it from admin phones before binding it as an account manager \u2014 a phone is a house admin or a sub-account manager, never both.`
6302
+ };
6303
+ }
6304
+ if (!wa.accountManagers || typeof wa.accountManagers !== "object") {
6305
+ wa.accountManagers = {};
6306
+ }
6307
+ wa.accountManagers[normalized] = sub;
6308
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
6309
+ if (!parsed.success) {
6310
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6311
+ return { ok: false, error: `Validation failed: ${msg}` };
6312
+ }
6313
+ config.whatsapp = parsed.data;
6314
+ writeConfig(accountDir, config);
6315
+ console.error(`${TAG17} bound account manager phone=${normalized} managesAccount=${sub}`);
6316
+ reloadManagerConfig(accountDir);
6317
+ return { ok: true, message: `Bound ${normalized} as account manager for sub-account ${sub}. Their WhatsApp messages will reach that sub-account's admin agent; replies still go out over this account's WhatsApp device.` };
6318
+ } catch (err) {
6319
+ const msg = err instanceof Error ? err.message : String(err);
6320
+ console.error(`${TAG17} setAccountManager failed: ${msg}`);
6321
+ return { ok: false, error: msg };
6322
+ }
6323
+ }
6324
+ function clearAccountManager(accountDir, phone) {
6325
+ const normalized = phone.trim();
6326
+ try {
6327
+ const config = readConfig(accountDir);
6328
+ const wa = config.whatsapp;
6329
+ if (!wa || typeof wa !== "object" || !wa.accountManagers || typeof wa.accountManagers !== "object") {
6330
+ return { ok: true, message: `No account-manager binding for ${normalized}.` };
6331
+ }
6332
+ const map = wa.accountManagers;
6333
+ const key = Object.keys(map).find((k) => phonesMatch(k, normalized));
6334
+ if (!key) {
6335
+ return { ok: true, message: `No account-manager binding for ${normalized}.` };
6336
+ }
6337
+ delete map[key];
6338
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
6339
+ if (!parsed.success) {
6340
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6341
+ return { ok: false, error: `Validation failed: ${msg}` };
6342
+ }
6343
+ config.whatsapp = parsed.data;
6344
+ writeConfig(accountDir, config);
6345
+ console.error(`${TAG17} cleared account manager phone=${normalized}`);
6346
+ reloadManagerConfig(accountDir);
6347
+ return { ok: true, message: `Cleared the account-manager binding for ${normalized}.` };
6348
+ } catch (err) {
6349
+ const msg = err instanceof Error ? err.message : String(err);
6350
+ console.error(`${TAG17} clearAccountManager failed: ${msg}`);
6351
+ return { ok: false, error: msg };
6352
+ }
6353
+ }
6354
+ function readAccountManagers(accountDir) {
6355
+ try {
6356
+ const config = readConfig(accountDir);
6357
+ const wa = config.whatsapp;
6358
+ if (!wa || typeof wa.accountManagers !== "object" || wa.accountManagers === null || Array.isArray(wa.accountManagers)) return {};
6359
+ const out = {};
6360
+ for (const [k, v] of Object.entries(wa.accountManagers)) {
6361
+ if (typeof v === "string") out[k] = v;
6362
+ }
6363
+ return out;
6364
+ } catch {
6365
+ return {};
6366
+ }
6367
+ }
6130
6368
  function setPublicAgent(accountDir, slug) {
6131
6369
  const trimmed = slug.trim();
6132
6370
  if (!trimmed) {
@@ -6150,12 +6388,12 @@ function setPublicAgent(accountDir, slug) {
6150
6388
  }
6151
6389
  config.whatsapp = parsed.data;
6152
6390
  writeConfig(accountDir, config);
6153
- console.error(`${TAG16} publicAgent set to ${trimmed}`);
6391
+ console.error(`${TAG17} publicAgent set to ${trimmed}`);
6154
6392
  reloadManagerConfig(accountDir);
6155
6393
  return { ok: true, message: `Public agent set to "${trimmed}". WhatsApp messages from non-admin phones will be handled by this agent.` };
6156
6394
  } catch (err) {
6157
6395
  const msg = err instanceof Error ? err.message : String(err);
6158
- console.error(`${TAG16} setPublicAgent failed: ${msg}`);
6396
+ console.error(`${TAG17} setPublicAgent failed: ${msg}`);
6159
6397
  return { ok: false, error: msg };
6160
6398
  }
6161
6399
  }
@@ -6196,17 +6434,17 @@ function updateConfig(accountDir, fields) {
6196
6434
  const parsed = WhatsAppConfigSchema.safeParse(wa);
6197
6435
  if (!parsed.success) {
6198
6436
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6199
- console.error(`${TAG16} update validation failed: ${msg}`);
6437
+ console.error(`${TAG17} update validation failed: ${msg}`);
6200
6438
  return { ok: false, error: `Validation failed: ${msg}` };
6201
6439
  }
6202
6440
  config.whatsapp = parsed.data;
6203
6441
  writeConfig(accountDir, config);
6204
- console.error(`${TAG16} updated fields=[${fieldNames.join(",")}]`);
6442
+ console.error(`${TAG17} updated fields=[${fieldNames.join(",")}]`);
6205
6443
  reloadManagerConfig(accountDir);
6206
6444
  return { ok: true, message: `Updated WhatsApp config: ${fieldNames.join(", ")}.` };
6207
6445
  } catch (err) {
6208
6446
  const msg = err instanceof Error ? err.message : String(err);
6209
- console.error(`${TAG16} updateConfig failed: ${msg}`);
6447
+ console.error(`${TAG17} updateConfig failed: ${msg}`);
6210
6448
  return { ok: false, error: msg };
6211
6449
  }
6212
6450
  }
@@ -6243,17 +6481,17 @@ function migrateRemovedConfigKeys(accountDir) {
6243
6481
  }
6244
6482
  writeConfig(accountDir, config);
6245
6483
  console.error(
6246
- `${TAG16} migration: stripped unknown keys=[${result.droppedKeys.join(",")}] from account.json`
6484
+ `${TAG17} migration: stripped unknown keys=[${result.droppedKeys.join(",")}] from account.json`
6247
6485
  );
6248
6486
  return { dropped: result.droppedKeys };
6249
6487
  } catch (err) {
6250
- console.error(`${TAG16} migration failed: ${String(err)}`);
6488
+ console.error(`${TAG17} migration failed: ${String(err)}`);
6251
6489
  return { dropped: [] };
6252
6490
  }
6253
6491
  }
6254
6492
 
6255
6493
  // app/lib/whatsapp/login.ts
6256
- var TAG17 = "[whatsapp:login]";
6494
+ var TAG18 = "[whatsapp:login]";
6257
6495
  function maskPhone(digits) {
6258
6496
  if (digits.length <= 4) return "*".repeat(digits.length);
6259
6497
  return digits.slice(0, 2) + "*".repeat(digits.length - 4) + digits.slice(-2);
@@ -6264,7 +6502,7 @@ function closeSocket(sock) {
6264
6502
  try {
6265
6503
  sock.ws?.close?.();
6266
6504
  } catch (err) {
6267
- console.warn(`${TAG17} socket close error during cleanup: ${String(err)}`);
6505
+ console.warn(`${TAG18} socket close error during cleanup: ${String(err)}`);
6268
6506
  }
6269
6507
  }
6270
6508
  function resetActiveLogin(accountId) {
@@ -6291,18 +6529,18 @@ async function runPostPairing(login) {
6291
6529
  const masked = selfPhone ? `${selfPhone.slice(0, 4)}***` : "null";
6292
6530
  console.error(`[whatsapp-persist] wa-persist-on-connect account=${login.accountId} phone=${masked}`);
6293
6531
  } else {
6294
- console.error(`${TAG17} wa-persist-on-connect FAILED account=${login.accountId}: ${result.error}`);
6532
+ console.error(`${TAG18} wa-persist-on-connect FAILED account=${login.accountId}: ${result.error}`);
6295
6533
  }
6296
6534
  } catch (err) {
6297
- console.error(`${TAG17} wa-persist-on-connect error account=${login.accountId}: ${String(err)}`);
6535
+ console.error(`${TAG18} wa-persist-on-connect error account=${login.accountId}: ${String(err)}`);
6298
6536
  }
6299
6537
  } else {
6300
- console.error(`${TAG17} wa-persist-on-connect skipped \u2014 no accountDir account=${login.accountId}`);
6538
+ console.error(`${TAG18} wa-persist-on-connect skipped \u2014 no accountDir account=${login.accountId}`);
6301
6539
  }
6302
6540
  try {
6303
6541
  await registerLoginSocket(login.accountId, login.sock, login.authDir);
6304
6542
  } catch (err) {
6305
- console.error(`${TAG17} registerLoginSocket failed account=${login.accountId}: ${String(err)}`);
6543
+ console.error(`${TAG18} registerLoginSocket failed account=${login.accountId}: ${String(err)}`);
6306
6544
  }
6307
6545
  }
6308
6546
  async function loginConnectionLoop(accountId, login) {
@@ -6311,7 +6549,7 @@ async function loginConnectionLoop(accountId, login) {
6311
6549
  const logTerminal = (outcome, errMsg) => {
6312
6550
  const fc = credsCensus(login.authDir);
6313
6551
  console.error(
6314
- `${TAG17} op=terminal cid=${cidShort} outcome=${outcome} credsFinal={registered=${fc.registered},account=${fc.hasAccountSignature},me=${fc.e164 ?? "null"}}` + (errMsg ? ` error="${errMsg}"` : "")
6552
+ `${TAG18} op=terminal cid=${cidShort} outcome=${outcome} credsFinal={registered=${fc.registered},account=${fc.hasAccountSignature},me=${fc.e164 ?? "null"}}` + (errMsg ? ` error="${errMsg}"` : "")
6315
6553
  );
6316
6554
  };
6317
6555
  while (true) {
@@ -6321,7 +6559,7 @@ async function loginConnectionLoop(accountId, login) {
6321
6559
  if (current?.id === login.id) {
6322
6560
  await runPostPairing(current);
6323
6561
  current.connected = true;
6324
- console.error(`${TAG17} loginConnectionLoop: connected account=${accountId} attempt=${attempt} configPersisted=${current.configPersisted}`);
6562
+ console.error(`${TAG18} loginConnectionLoop: connected account=${accountId} attempt=${attempt} configPersisted=${current.configPersisted}`);
6325
6563
  logTerminal("connected");
6326
6564
  }
6327
6565
  return;
@@ -6333,7 +6571,7 @@ async function loginConnectionLoop(accountId, login) {
6333
6571
  if (!preRetryCensus.hasAccountSignature) {
6334
6572
  current.error = "WhatsApp accepted the code but did not finish linking. This usually means the number has reached its linked-device limit or has a stale linked device. On the phone, open WhatsApp, Linked Devices, remove existing devices, then ask for a new code.";
6335
6573
  console.error(
6336
- `${TAG17} op=pairing-incomplete cid=${cidShort} closeKind=${closeKind(getStatusCode(err), false)} reason=no-account-signature`
6574
+ `${TAG18} op=pairing-incomplete cid=${cidShort} closeKind=${closeKind(getStatusCode(err), false)} reason=no-account-signature`
6337
6575
  );
6338
6576
  return;
6339
6577
  }
@@ -6341,7 +6579,7 @@ async function loginConnectionLoop(accountId, login) {
6341
6579
  if (!classification.shouldRetry || attempt >= LOGIN_MAX_RECONNECTS) {
6342
6580
  if (attempt >= LOGIN_MAX_RECONNECTS) {
6343
6581
  console.error(
6344
- `${TAG17} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
6582
+ `${TAG18} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
6345
6583
  );
6346
6584
  current.error = `Login failed after ${attempt} reconnect attempts: ${formatError(err)}`;
6347
6585
  } else {
@@ -6354,7 +6592,7 @@ async function loginConnectionLoop(accountId, login) {
6354
6592
  attempt++;
6355
6593
  const delay = LOGIN_RECONNECT_DELAYS[attempt - 1] ?? 8e3;
6356
6594
  console.error(
6357
- `${TAG17} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
6595
+ `${TAG18} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
6358
6596
  );
6359
6597
  closeSocket(current.sock);
6360
6598
  await new Promise((r) => setTimeout(r, delay));
@@ -6362,14 +6600,14 @@ async function loginConnectionLoop(accountId, login) {
6362
6600
  if (afterDelay?.id !== login.id) return;
6363
6601
  const rc = credsCensus(login.authDir);
6364
6602
  console.error(
6365
- `${TAG17} op=reconnect cid=${cidShort} attempt=${attempt}/${LOGIN_MAX_RECONNECTS} withRegistered=${rc.registered} withAccount=${rc.hasAccountSignature}`
6603
+ `${TAG18} op=reconnect cid=${cidShort} attempt=${attempt}/${LOGIN_MAX_RECONNECTS} withRegistered=${rc.registered} withAccount=${rc.hasAccountSignature}`
6366
6604
  );
6367
6605
  try {
6368
6606
  const newSock = await createWaSocket({ authDir: login.authDir, correlationId: cidShort, account: accountId });
6369
6607
  current.sock = newSock;
6370
6608
  } catch (sockErr) {
6371
6609
  console.error(
6372
- `${TAG17} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
6610
+ `${TAG18} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
6373
6611
  );
6374
6612
  current.error = `Reconnection failed: ${String(sockErr)}`;
6375
6613
  logTerminal("error", current.error);
@@ -6386,7 +6624,7 @@ async function startLogin(opts) {
6386
6624
  const hasAuth = await authExists(authDir);
6387
6625
  const selfId = readSelfId(authDir);
6388
6626
  console.error(
6389
- `${TAG17} op=start cid=${cid} account=${accountId} force=${!!force} hasAuth=${hasAuth}` + (existing0 ? ` activeLogin={id=${existing0.id.slice(0, 8)},age=${Math.round((Date.now() - existing0.startedAt) / 1e3)}s,hasCode=${!!existing0.pairingCode}}` : " activeLogin=none")
6627
+ `${TAG18} op=start cid=${cid} account=${accountId} force=${!!force} hasAuth=${hasAuth}` + (existing0 ? ` activeLogin={id=${existing0.id.slice(0, 8)},age=${Math.round((Date.now() - existing0.startedAt) / 1e3)}s,hasCode=${!!existing0.pairingCode}}` : " activeLogin=none")
6390
6628
  );
6391
6629
  if (hasAuth && !force) {
6392
6630
  const who = selfId.e164 ?? selfId.jid ?? "unknown";
@@ -6401,7 +6639,7 @@ async function startLogin(opts) {
6401
6639
  }
6402
6640
  const existing = activeLogins.get(accountId);
6403
6641
  if (existing && isLoginFresh(existing) && existing.pairingCode && !force) {
6404
- console.error(`${TAG17} startLogin account=${accountId} guard: returning existing pairing code (age=${Math.round((Date.now() - existing.startedAt) / 1e3)}s)`);
6642
+ console.error(`${TAG18} startLogin account=${accountId} guard: returning existing pairing code (age=${Math.round((Date.now() - existing.startedAt) / 1e3)}s)`);
6405
6643
  return {
6406
6644
  pairingCode: existing.pairingCode,
6407
6645
  phone: existing.phone,
@@ -6409,7 +6647,7 @@ async function startLogin(opts) {
6409
6647
  };
6410
6648
  }
6411
6649
  if (existing) {
6412
- console.error(`${TAG17} startLogin account=${accountId} ${force ? "force override" : "stale/no-code"}, resetting active login`);
6650
+ console.error(`${TAG18} startLogin account=${accountId} ${force ? "force override" : "stale/no-code"}, resetting active login`);
6413
6651
  }
6414
6652
  resetActiveLogin(accountId);
6415
6653
  await clearAuth(authDir);
@@ -6440,7 +6678,7 @@ async function startLogin(opts) {
6440
6678
  if (requested) return;
6441
6679
  requested = true;
6442
6680
  console.error(
6443
- `${TAG17} op=pairing-request cid=${cid} account=${accountId} qrRef=#1 sinceOpenMs=${socketOpenTs ? Date.now() - socketOpenTs : 0}`
6681
+ `${TAG18} op=pairing-request cid=${cid} account=${accountId} qrRef=#1 sinceOpenMs=${socketOpenTs ? Date.now() - socketOpenTs : 0}`
6444
6682
  );
6445
6683
  void sock.requestPairingCode(digits).then((code) => {
6446
6684
  clearTimeout(codeTimer);
@@ -6449,7 +6687,7 @@ async function startLogin(opts) {
6449
6687
  if (current?.id !== login.id) return;
6450
6688
  if (!current.pairingCode) current.pairingCode = code;
6451
6689
  console.error(
6452
- `${TAG17} op=code-issued cid=${cid} account=${accountId} phone=${maskPhone(digits)} codeLen=${code.length} sinceOpenMs=${socketOpenTs ? codeIssuedTs - socketOpenTs : 0}`
6690
+ `${TAG18} op=code-issued cid=${cid} account=${accountId} phone=${maskPhone(digits)} codeLen=${code.length} sinceOpenMs=${socketOpenTs ? codeIssuedTs - socketOpenTs : 0}`
6453
6691
  );
6454
6692
  resolveCode?.(code);
6455
6693
  }).catch((err) => {
@@ -6480,7 +6718,7 @@ async function startLogin(opts) {
6480
6718
  const ttlTimer = setTimeout(() => {
6481
6719
  const current = activeLogins.get(accountId);
6482
6720
  if (current?.id === login.id && !current.connected) {
6483
- console.error(`${TAG17} pairing-window-elapsed account=${accountId} (ttl sweep)`);
6721
+ console.error(`${TAG18} pairing-window-elapsed account=${accountId} (ttl sweep)`);
6484
6722
  resetActiveLogin(accountId);
6485
6723
  }
6486
6724
  }, ACTIVE_LOGIN_TTL_MS);
@@ -6488,7 +6726,7 @@ async function startLogin(opts) {
6488
6726
  ttlTimer.unref();
6489
6727
  }
6490
6728
  loginConnectionLoop(accountId, login).catch((err) => {
6491
- console.error(`${TAG17} loginConnectionLoop unexpected error: ${String(err)}`);
6729
+ console.error(`${TAG18} loginConnectionLoop unexpected error: ${String(err)}`);
6492
6730
  const current = activeLogins.get(accountId);
6493
6731
  if (current?.id === login.id) {
6494
6732
  current.error = `Unexpected login error: ${String(err)}`;
@@ -6513,13 +6751,13 @@ async function waitForLogin(opts) {
6513
6751
  const { accountId, timeoutMs = 6e4 } = opts;
6514
6752
  const login = activeLogins.get(accountId);
6515
6753
  console.error(
6516
- `${TAG17} waitForLogin account=${accountId} timeout=${timeoutMs}ms` + (login ? ` login={id=${login.id.slice(0, 8)},age=${Math.round((Date.now() - login.startedAt) / 1e3)}s,connected=${login.connected},hasCode=${!!login.pairingCode}}` : " login=none")
6754
+ `${TAG18} waitForLogin account=${accountId} timeout=${timeoutMs}ms` + (login ? ` login={id=${login.id.slice(0, 8)},age=${Math.round((Date.now() - login.startedAt) / 1e3)}s,connected=${login.connected},hasCode=${!!login.pairingCode}}` : " login=none")
6517
6755
  );
6518
6756
  if (!login) {
6519
6757
  return { connected: false, message: "No active WhatsApp login in progress.", configPersisted: false };
6520
6758
  }
6521
6759
  if (!isLoginFresh(login)) {
6522
- console.error(`${TAG17} pairing-window-elapsed account=${accountId}`);
6760
+ console.error(`${TAG18} pairing-window-elapsed account=${accountId}`);
6523
6761
  resetActiveLogin(accountId);
6524
6762
  return { connected: false, message: "The pairing window expired. Ask me to generate a new code.", configPersisted: false };
6525
6763
  }
@@ -6527,8 +6765,8 @@ async function waitForLogin(opts) {
6527
6765
  while (Date.now() < deadline) {
6528
6766
  if (login.connected) {
6529
6767
  const selfId = readSelfId(login.authDir);
6530
- console.error(`${TAG17} pairing-connected account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
6531
- console.error(`${TAG17} login complete for account=${accountId} phone=${selfId.e164 ?? "unknown"} configPersisted=${login.configPersisted}`);
6768
+ console.error(`${TAG18} pairing-connected account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
6769
+ console.error(`${TAG18} login complete for account=${accountId} phone=${selfId.e164 ?? "unknown"} configPersisted=${login.configPersisted}`);
6532
6770
  const configPersisted = login.configPersisted;
6533
6771
  activeLogins.delete(accountId);
6534
6772
  return {
@@ -6546,7 +6784,7 @@ async function waitForLogin(opts) {
6546
6784
  await new Promise((r) => setTimeout(r, 1e3));
6547
6785
  }
6548
6786
  const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1e3);
6549
- console.error(`${TAG17} waitForLogin poll timeout account=${accountId} elapsed=${elapsed}s \u2014 login kept alive (pairing code still valid)`);
6787
+ console.error(`${TAG18} waitForLogin poll timeout account=${accountId} elapsed=${elapsed}s \u2014 login kept alive (pairing code still valid)`);
6550
6788
  return { connected: false, message: "Still waiting for you to enter the pairing code. The code is still valid \u2014 ask me to keep waiting, or request a new code.", configPersisted: false };
6551
6789
  }
6552
6790
  function listActiveLoginAccountIds() {
@@ -6561,7 +6799,7 @@ function listActiveLoginAccountIds() {
6561
6799
  import { realpathSync as realpathSync3 } from "fs";
6562
6800
  import { readFile, stat as stat2 } from "fs/promises";
6563
6801
  import { resolve as resolve9, basename as basename2 } from "path";
6564
- var TAG18 = "[whatsapp:outbound]";
6802
+ var TAG19 = "[whatsapp:outbound]";
6565
6803
  var WHATSAPP_DOCUMENT_MAX_BYTES = 100 * 1024 * 1024;
6566
6804
  var lastDocumentOutboundAt = /* @__PURE__ */ new Map();
6567
6805
  var lastRouteDocumentOutboundAt = /* @__PURE__ */ new Map();
@@ -6595,16 +6833,16 @@ async function sendWhatsAppDocument(input) {
6595
6833
  const accountResolved = realpathSync3(accountDir);
6596
6834
  if (!resolvedPath.startsWith(accountResolved + "/")) {
6597
6835
  const sanitised = filePath.replace(accountDir, "<account>/");
6598
- console.error(`${TAG18} document REJECTED path=${sanitised} reason=outside_account_directory`);
6836
+ console.error(`${TAG19} document REJECTED path=${sanitised} reason=outside_account_directory`);
6599
6837
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
6600
6838
  }
6601
6839
  } catch (err) {
6602
6840
  const code = err.code;
6603
6841
  if (code === "ENOENT") {
6604
- console.error(`${TAG18} document ENOENT path=${filePath}`);
6842
+ console.error(`${TAG19} document ENOENT path=${filePath}`);
6605
6843
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
6606
6844
  }
6607
- console.error(`${TAG18} document path error: ${String(err)}`);
6845
+ console.error(`${TAG19} document path error: ${String(err)}`);
6608
6846
  return { ok: false, status: 500, error: String(err) };
6609
6847
  }
6610
6848
  const fileStat = await stat2(resolvedPath);
@@ -6619,7 +6857,7 @@ async function sendWhatsAppDocument(input) {
6619
6857
  const jid = normalizeJid2(to);
6620
6858
  const sock = getSocket(accountId);
6621
6859
  if (!sock) {
6622
- console.error(`${TAG18} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=false reason=not-connected`);
6860
+ console.error(`${TAG19} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=false reason=not-connected`);
6623
6861
  return { ok: false, status: 503, error: `WhatsApp account "${accountId}" is not connected` };
6624
6862
  }
6625
6863
  const buffer = Buffer.from(await readFile(resolvedPath));
@@ -6631,7 +6869,7 @@ async function sendWhatsAppDocument(input) {
6631
6869
  { accountId }
6632
6870
  );
6633
6871
  console.error(
6634
- `${TAG18} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
6872
+ `${TAG19} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
6635
6873
  );
6636
6874
  if (result.success) {
6637
6875
  recordDocumentOutbound(to);
@@ -6751,7 +6989,7 @@ function serializeWhatsAppSchema() {
6751
6989
  // app/lib/whatsapp/status-reconcile.ts
6752
6990
  import { readdirSync as readdirSync3 } from "fs";
6753
6991
  import { join as join9 } from "path";
6754
- var TAG19 = "[whatsapp:reconcile]";
6992
+ var TAG20 = "[whatsapp:reconcile]";
6755
6993
  var HALF_REGISTERED_MIN_AGE_MS = 5 * 6e4;
6756
6994
  function listCredsAccountIds(credsRoot) {
6757
6995
  try {
@@ -6777,14 +7015,14 @@ function reconcileCredsOnDisk(opts) {
6777
7015
  const ageMs = getAuthAgeMs(authDir);
6778
7016
  const abandoned = !inFlight && ageMs !== null && ageMs >= HALF_REGISTERED_MIN_AGE_MS;
6779
7017
  if (!abandoned) {
6780
- console.error(`${TAG19} op=half-registered account=${accountId} registered=true hasAccount=false inFlight=${inFlight} ageMs=${ageMs ?? "null"}`);
7018
+ console.error(`${TAG20} op=half-registered account=${accountId} registered=true hasAccount=false inFlight=${inFlight} ageMs=${ageMs ?? "null"}`);
6781
7019
  entries.push({ accountId, connected: false, linkedUnconfigured: true });
6782
7020
  continue;
6783
7021
  }
6784
- console.error(`${TAG19} op=discard-half-registered account=${accountId} registered=true hasAccount=false ageMs=${ageMs}`);
7022
+ console.error(`${TAG20} op=discard-half-registered account=${accountId} registered=true hasAccount=false ageMs=${ageMs}`);
6785
7023
  const removed = discardAuthDirSync(authDir);
6786
7024
  if (!removed) {
6787
- console.error(`${TAG19} op=discard-half-registered-failed account=${accountId} \u2014 residue persists`);
7025
+ console.error(`${TAG20} op=discard-half-registered-failed account=${accountId} \u2014 residue persists`);
6788
7026
  entries.push({ accountId, connected: false, linkedUnconfigured: true });
6789
7027
  }
6790
7028
  continue;
@@ -6792,14 +7030,14 @@ function reconcileCredsOnDisk(opts) {
6792
7030
  const selfPhone = readSelfId(authDir).e164 ?? void 0;
6793
7031
  const configured = accountDir ? isAccountConfigured(accountDir, accountId) : false;
6794
7032
  console.error(
6795
- `${TAG19} wa-link-unconfigured account=${accountId} creds=present config=${configured ? "present" : "absent"}`
7033
+ `${TAG20} wa-link-unconfigured account=${accountId} creds=present config=${configured ? "present" : "absent"}`
6796
7034
  );
6797
7035
  if (!configured && accountDir) {
6798
7036
  const result = persistAfterPairing(accountDir, accountId, selfPhone ?? null);
6799
7037
  if (result.ok) {
6800
- console.error(`${TAG19} self-healed account=${accountId}`);
7038
+ console.error(`${TAG20} self-healed account=${accountId}`);
6801
7039
  } else {
6802
- console.error(`${TAG19} self-heal FAILED account=${accountId}: ${result.error}`);
7040
+ console.error(`${TAG20} self-heal FAILED account=${accountId}: ${result.error}`);
6803
7041
  }
6804
7042
  }
6805
7043
  entries.push({ accountId, selfPhone, connected: false, linkedUnconfigured: !configured });
@@ -6808,7 +7046,7 @@ function reconcileCredsOnDisk(opts) {
6808
7046
  }
6809
7047
 
6810
7048
  // server/routes/whatsapp.ts
6811
- var TAG20 = "[whatsapp:api]";
7049
+ var TAG21 = "[whatsapp:api]";
6812
7050
  var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
6813
7051
  var app2 = new Hono();
6814
7052
  app2.get("/status", (c) => {
@@ -6821,10 +7059,10 @@ app2.get("/status", (c) => {
6821
7059
  const reconciled = reconcileCredsOnDisk({ credsRoot, accountDir, liveAccountIds: liveIds, activeLoginAccountIds: activeLoginIds });
6822
7060
  const accounts = [...live, ...reconciled];
6823
7061
  const summary = accounts.map((a) => `${a.accountId}:${a.connected ? "up" : a.linkedUnconfigured ? "linked-unconfigured" : "down"}`).join(", ");
6824
- console.error(`${TAG20} status accounts=${accounts.length} [${summary}]`);
7062
+ console.error(`${TAG21} status accounts=${accounts.length} [${summary}]`);
6825
7063
  return c.json({ accounts });
6826
7064
  } catch (err) {
6827
- console.error(`${TAG20} status error: ${String(err)}`);
7065
+ console.error(`${TAG21} status error: ${String(err)}`);
6828
7066
  return c.json({ error: String(err) }, 500);
6829
7067
  }
6830
7068
  });
@@ -6840,10 +7078,10 @@ app2.post("/login/start", async (c) => {
6840
7078
  const authDir = join10(MAXY_DIR, "credentials", "whatsapp", accountId);
6841
7079
  const accountDir = resolveAccount()?.accountDir ?? null;
6842
7080
  const result = await startLogin({ accountId, authDir, phone, accountDir, force });
6843
- console.error(`${TAG20} login/start result account=${accountId} hasCode=${!!result.pairingCode}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
7081
+ console.error(`${TAG21} login/start result account=${accountId} hasCode=${!!result.pairingCode}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
6844
7082
  return c.json(result);
6845
7083
  } catch (err) {
6846
- console.error(`${TAG20} login/start error: ${String(err)}`);
7084
+ console.error(`${TAG21} login/start error: ${String(err)}`);
6847
7085
  return c.json({ error: String(err) }, 500);
6848
7086
  }
6849
7087
  });
@@ -6853,7 +7091,7 @@ app2.post("/login/wait", async (c) => {
6853
7091
  const accountId = validateAccountId(body.accountId);
6854
7092
  const timeoutMs = body.timeoutMs ?? 6e4;
6855
7093
  const result = await waitForLogin({ accountId, timeoutMs });
6856
- console.error(`${TAG20} login/wait result account=${accountId} connected=${result.connected}${result.selfPhone ? ` phone=${result.selfPhone}` : ""} configPersisted=${result.configPersisted}`);
7094
+ console.error(`${TAG21} login/wait result account=${accountId} connected=${result.connected}${result.selfPhone ? ` phone=${result.selfPhone}` : ""} configPersisted=${result.configPersisted}`);
6857
7095
  return c.json({
6858
7096
  connected: result.connected,
6859
7097
  message: result.message,
@@ -6861,7 +7099,7 @@ app2.post("/login/wait", async (c) => {
6861
7099
  configPersisted: result.configPersisted
6862
7100
  });
6863
7101
  } catch (err) {
6864
- console.error(`${TAG20} login/wait error: ${String(err)}`);
7102
+ console.error(`${TAG21} login/wait error: ${String(err)}`);
6865
7103
  return c.json({ error: String(err) }, 500);
6866
7104
  }
6867
7105
  });
@@ -6872,7 +7110,7 @@ app2.post("/disconnect", async (c) => {
6872
7110
  await stopConnection(accountId);
6873
7111
  return c.json({ disconnected: true, accountId });
6874
7112
  } catch (err) {
6875
- console.error(`${TAG20} disconnect error: ${String(err)}`);
7113
+ console.error(`${TAG21} disconnect error: ${String(err)}`);
6876
7114
  return c.json({ error: String(err) }, 500);
6877
7115
  }
6878
7116
  });
@@ -6883,14 +7121,14 @@ app2.post("/reconnect", async (c) => {
6883
7121
  await startConnection(accountId);
6884
7122
  return c.json({ reconnecting: true, accountId });
6885
7123
  } catch (err) {
6886
- console.error(`${TAG20} reconnect error: ${String(err)}`);
7124
+ console.error(`${TAG21} reconnect error: ${String(err)}`);
6887
7125
  return c.json({ error: String(err) }, 500);
6888
7126
  }
6889
7127
  });
6890
7128
  app2.post("/config", async (c) => {
6891
7129
  try {
6892
7130
  const body = await c.req.json().catch(() => ({}));
6893
- const { action, phone, slug, fields, accountId } = body;
7131
+ const { action, phone, slug, fields, accountId, managesAccount } = body;
6894
7132
  if (!action || typeof action !== "string") {
6895
7133
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
6896
7134
  }
@@ -6904,7 +7142,7 @@ app2.post("/config", async (c) => {
6904
7142
  return c.json({ ok: false, error: 'Missing required field "phone" (E.164 format, e.g. +441234567890).' }, 400);
6905
7143
  }
6906
7144
  const result = addAdminPhone(account.accountDir, phone);
6907
- console.error(`${TAG20} config action=add-admin-phone phone=${phone} ok=${result.ok}`);
7145
+ console.error(`${TAG21} config action=add-admin-phone phone=${phone} ok=${result.ok}`);
6908
7146
  return c.json(result, result.ok ? 200 : 400);
6909
7147
  }
6910
7148
  case "remove-admin-phone": {
@@ -6912,26 +7150,50 @@ app2.post("/config", async (c) => {
6912
7150
  return c.json({ ok: false, error: 'Missing required field "phone".' }, 400);
6913
7151
  }
6914
7152
  const result = removeAdminPhone(account.accountDir, phone);
6915
- console.error(`${TAG20} config action=remove-admin-phone phone=${phone} ok=${result.ok}`);
7153
+ console.error(`${TAG21} config action=remove-admin-phone phone=${phone} ok=${result.ok}`);
6916
7154
  return c.json(result, result.ok ? 200 : 400);
6917
7155
  }
6918
7156
  case "list-admin-phones": {
6919
7157
  const phones = readAdminPhones(account.accountDir);
6920
- console.error(`${TAG20} config action=list-admin-phones count=${phones.length}`);
7158
+ console.error(`${TAG21} config action=list-admin-phones count=${phones.length}`);
6921
7159
  return c.json({ ok: true, phones });
6922
7160
  }
7161
+ case "add-account-manager": {
7162
+ if (!phone || typeof phone !== "string") {
7163
+ return c.json({ ok: false, error: 'Missing required field "phone" (E.164 format, e.g. +441234567890).' }, 400);
7164
+ }
7165
+ if (!managesAccount || typeof managesAccount !== "string") {
7166
+ return c.json({ ok: false, error: 'Missing required field "managesAccount" (the sub-account UUID this phone should manage).' }, 400);
7167
+ }
7168
+ const result = setAccountManager(account.accountDir, phone, managesAccount);
7169
+ console.error(`${TAG21} config action=add-account-manager phone=${phone} managesAccount=${managesAccount} ok=${result.ok}`);
7170
+ return c.json(result, result.ok ? 200 : 400);
7171
+ }
7172
+ case "remove-account-manager": {
7173
+ if (!phone || typeof phone !== "string") {
7174
+ return c.json({ ok: false, error: 'Missing required field "phone".' }, 400);
7175
+ }
7176
+ const result = clearAccountManager(account.accountDir, phone);
7177
+ console.error(`${TAG21} config action=remove-account-manager phone=${phone} ok=${result.ok}`);
7178
+ return c.json(result, result.ok ? 200 : 400);
7179
+ }
7180
+ case "list-account-managers": {
7181
+ const accountManagers = readAccountManagers(account.accountDir);
7182
+ console.error(`${TAG21} config action=list-account-managers count=${Object.keys(accountManagers).length}`);
7183
+ return c.json({ ok: true, accountManagers });
7184
+ }
6923
7185
  case "set-public-agent": {
6924
7186
  if (!slug || typeof slug !== "string") {
6925
7187
  return c.json({ ok: false, error: 'Missing required field "slug" (the agent directory name, e.g. "my-agent").' }, 400);
6926
7188
  }
6927
7189
  const result = setPublicAgent(account.accountDir, slug);
6928
- console.error(`${TAG20} config action=set-public-agent slug=${slug} ok=${result.ok}`);
7190
+ console.error(`${TAG21} config action=set-public-agent slug=${slug} ok=${result.ok}`);
6929
7191
  return c.json(result, result.ok ? 200 : 400);
6930
7192
  }
6931
7193
  case "get-public-agent": {
6932
7194
  const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
6933
7195
  const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount });
6934
- console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
7196
+ console.error(`${TAG21} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
6935
7197
  return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
6936
7198
  }
6937
7199
  case "list-public-agents": {
@@ -6948,26 +7210,26 @@ app2.post("/config", async (c) => {
6948
7210
  const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
6949
7211
  agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
6950
7212
  } catch {
6951
- console.error(`${TAG20} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
7213
+ console.error(`${TAG21} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
6952
7214
  }
6953
7215
  }
6954
7216
  } catch (err) {
6955
- console.error(`${TAG20} config action=list-public-agents error="failed to scan agents directory: ${String(err)}"`);
7217
+ console.error(`${TAG21} config action=list-public-agents error="failed to scan agents directory: ${String(err)}"`);
6956
7218
  }
6957
7219
  }
6958
- console.error(`${TAG20} config action=list-public-agents count=${agents.length}`);
7220
+ console.error(`${TAG21} config action=list-public-agents count=${agents.length}`);
6959
7221
  return c.json({ ok: true, agents });
6960
7222
  }
6961
7223
  case "schema": {
6962
7224
  const text = serializeWhatsAppSchema();
6963
- console.error(`${TAG20} config action=schema`);
7225
+ console.error(`${TAG21} config action=schema`);
6964
7226
  return c.json({ ok: true, text });
6965
7227
  }
6966
7228
  case "list-groups": {
6967
7229
  const groupAccountId = accountId ?? "default";
6968
7230
  const sock = getSocket(groupAccountId);
6969
7231
  if (!sock) {
6970
- console.error(`${TAG20} config action=list-groups error="not connected" accountId=${groupAccountId}`);
7232
+ console.error(`${TAG21} config action=list-groups error="not connected" accountId=${groupAccountId}`);
6971
7233
  return c.json({ ok: false, error: `WhatsApp account "${groupAccountId}" is not connected. Connect first, then retry.` });
6972
7234
  }
6973
7235
  try {
@@ -6977,10 +7239,10 @@ app2.post("/config", async (c) => {
6977
7239
  name: g.subject ?? g.id,
6978
7240
  participantCount: Array.isArray(g.participants) ? g.participants.length : 0
6979
7241
  }));
6980
- console.error(`${TAG20} config action=list-groups count=${groups.length} accountId=${groupAccountId}`);
7242
+ console.error(`${TAG21} config action=list-groups count=${groups.length} accountId=${groupAccountId}`);
6981
7243
  return c.json({ ok: true, groups });
6982
7244
  } catch (err) {
6983
- console.error(`${TAG20} config action=list-groups error="${String(err)}" accountId=${groupAccountId}`);
7245
+ console.error(`${TAG21} config action=list-groups error="${String(err)}" accountId=${groupAccountId}`);
6984
7246
  return c.json({ ok: false, error: `Failed to fetch groups: ${String(err)}` });
6985
7247
  }
6986
7248
  }
@@ -6990,22 +7252,22 @@ app2.post("/config", async (c) => {
6990
7252
  }
6991
7253
  const result = updateConfig(account.accountDir, fields);
6992
7254
  const fieldNames = Object.keys(fields);
6993
- console.error(`${TAG20} config action=update-config fields=[${fieldNames.join(",")}] ok=${result.ok}`);
7255
+ console.error(`${TAG21} config action=update-config fields=[${fieldNames.join(",")}] ok=${result.ok}`);
6994
7256
  return c.json(result, result.ok ? 200 : 400);
6995
7257
  }
6996
7258
  case "get-config": {
6997
7259
  const waConfig = getConfig(account.accountDir);
6998
- console.error(`${TAG20} config action=get-config`);
7260
+ console.error(`${TAG21} config action=get-config`);
6999
7261
  return c.json({ ok: true, config: waConfig });
7000
7262
  }
7001
7263
  default:
7002
7264
  return c.json(
7003
- { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, list-public-agents, update-config, get-config, schema, list-groups.` },
7265
+ { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, add-account-manager, remove-account-manager, list-account-managers, set-public-agent, get-public-agent, list-public-agents, update-config, get-config, schema, list-groups.` },
7004
7266
  400
7005
7267
  );
7006
7268
  }
7007
7269
  } catch (err) {
7008
- console.error(`${TAG20} config error: ${String(err)}`);
7270
+ console.error(`${TAG21} config error: ${String(err)}`);
7009
7271
  return c.json({ ok: false, error: String(err) }, 500);
7010
7272
  }
7011
7273
  });
@@ -7026,7 +7288,7 @@ app2.post("/send-document", async (c) => {
7026
7288
  recordRouteDocumentOutbound(to, filePath);
7027
7289
  return c.json({ success: true, messageId: result.messageId });
7028
7290
  } catch (err) {
7029
- console.error(`${TAG20} send-document error: ${String(err)}`);
7291
+ console.error(`${TAG21} send-document error: ${String(err)}`);
7030
7292
  return c.json({ error: String(err) }, 500);
7031
7293
  }
7032
7294
  });
@@ -7063,7 +7325,7 @@ app2.post("/send-admin", async (c) => {
7063
7325
  }
7064
7326
  return c.json({ success: true, messageId: result.messageId });
7065
7327
  } catch (err) {
7066
- console.error(`${TAG20} send-admin error: ${String(err)}`);
7328
+ console.error(`${TAG21} send-admin error: ${String(err)}`);
7067
7329
  return c.json({ error: String(err) }, 500);
7068
7330
  }
7069
7331
  });
@@ -7073,11 +7335,11 @@ app2.get("/activity", (c) => {
7073
7335
  const result = getChannelActivity(accountId);
7074
7336
  const total = result.accounts.reduce((sum, a) => sum + a.total, 0);
7075
7337
  console.error(
7076
- `${TAG20} activity accounts=${result.accounts.length} total=${total} recentEvents=${result.recentEvents.length}` + (accountId ? ` filter=${accountId}` : "")
7338
+ `${TAG21} activity accounts=${result.accounts.length} total=${total} recentEvents=${result.recentEvents.length}` + (accountId ? ` filter=${accountId}` : "")
7077
7339
  );
7078
7340
  return c.json(result);
7079
7341
  } catch (err) {
7080
- console.error(`${TAG20} activity error: ${String(err)}`);
7342
+ console.error(`${TAG21} activity error: ${String(err)}`);
7081
7343
  return c.json({ error: String(err) }, 500);
7082
7344
  }
7083
7345
  });
@@ -7096,10 +7358,10 @@ app2.get("/conversations", (c) => {
7096
7358
  };
7097
7359
  });
7098
7360
  conversations.sort((a, b) => (b.lastMessageTimestamp ?? 0) - (a.lastMessageTimestamp ?? 0));
7099
- console.error(`${TAG20} conversations account=${accountId} count=${conversations.length}`);
7361
+ console.error(`${TAG21} conversations account=${accountId} count=${conversations.length}`);
7100
7362
  return c.json({ conversations });
7101
7363
  } catch (err) {
7102
- console.error(`${TAG20} conversations error: ${String(err)}`);
7364
+ console.error(`${TAG21} conversations error: ${String(err)}`);
7103
7365
  return c.json({ error: String(err) }, 500);
7104
7366
  }
7105
7367
  });
@@ -7114,10 +7376,10 @@ app2.get("/messages", (c) => {
7114
7376
  const limit = limitParam ? parseInt(limitParam, 10) : void 0;
7115
7377
  const effectiveLimit = limit && !Number.isNaN(limit) && limit > 0 ? limit : void 0;
7116
7378
  const messages = getMessages(accountId, jid, effectiveLimit);
7117
- console.error(`${TAG20} messages account=${accountId} jid=${jid} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
7379
+ console.error(`${TAG21} messages account=${accountId} jid=${jid} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
7118
7380
  return c.json({ messages });
7119
7381
  } catch (err) {
7120
- console.error(`${TAG20} messages error: ${String(err)}`);
7382
+ console.error(`${TAG21} messages error: ${String(err)}`);
7121
7383
  return c.json({ error: String(err) }, 500);
7122
7384
  }
7123
7385
  });
@@ -7198,7 +7460,7 @@ app2.get("/conversation-graph-state", async (c) => {
7198
7460
  ms
7199
7461
  });
7200
7462
  } catch (err) {
7201
- console.error(`${TAG20} conversation-graph-state error: ${String(err)}`);
7463
+ console.error(`${TAG21} conversation-graph-state error: ${String(err)}`);
7202
7464
  return c.json({ error: String(err) }, 500);
7203
7465
  }
7204
7466
  });
@@ -7210,12 +7472,12 @@ app2.get("/group-info", async (c) => {
7210
7472
  return c.json({ error: "Missing required parameter: jid" }, 400);
7211
7473
  }
7212
7474
  if (!isGroupJid(jid)) {
7213
- console.error(`${TAG20} group-info error="not a group JID" jid=${jid} account=${accountId}`);
7475
+ console.error(`${TAG21} group-info error="not a group JID" jid=${jid} account=${accountId}`);
7214
7476
  return c.json({ error: `"${jid}" is not a group JID. Group JIDs end with @g.us.` }, 400);
7215
7477
  }
7216
7478
  const sock = getSocket(accountId);
7217
7479
  if (!sock) {
7218
- console.error(`${TAG20} group-info error="not connected" account=${accountId}`);
7480
+ console.error(`${TAG21} group-info error="not connected" account=${accountId}`);
7219
7481
  return c.json({ error: `WhatsApp account "${accountId}" is not connected. Connect first, then retry.` }, 400);
7220
7482
  }
7221
7483
  const meta = await sock.groupMetadata(jid);
@@ -7228,10 +7490,10 @@ app2.get("/group-info", async (c) => {
7228
7490
  participantCount: meta.participants.length,
7229
7491
  participants: meta.participants.map((p) => ({ jid: p.id, admin: p.admin ?? null }))
7230
7492
  };
7231
- console.error(`${TAG20} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} account=${accountId}`);
7493
+ console.error(`${TAG21} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} account=${accountId}`);
7232
7494
  return c.json(result);
7233
7495
  } catch (err) {
7234
- console.error(`${TAG20} group-info error="${String(err)}" jid=${jid} account=${accountId}`);
7496
+ console.error(`${TAG21} group-info error="${String(err)}" jid=${jid} account=${accountId}`);
7235
7497
  return c.json({ error: `Failed to fetch group info: ${String(err)}` }, 500);
7236
7498
  }
7237
7499
  });
@@ -7649,11 +7911,11 @@ app3.get("/", requireAdminSession, async (c) => {
7649
7911
  var sidebar_sessions_default = app3;
7650
7912
 
7651
7913
  // app/lib/admin-identity/pin-validator.ts
7652
- import { createHash } from "crypto";
7914
+ import { createHash as createHash2 } from "crypto";
7653
7915
  import { existsSync as existsSync7, readFileSync as readFileSync13, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
7654
7916
  import { join as join12 } from "path";
7655
7917
  function hashPin(pin) {
7656
- return createHash("sha256").update(pin).digest("hex");
7918
+ return createHash2("sha256").update(pin).digest("hex");
7657
7919
  }
7658
7920
  function readUsersFile(usersFilePath) {
7659
7921
  if (!existsSync7(usersFilePath)) return null;
@@ -8631,7 +8893,7 @@ function resolveOpenVisitorSession(rows, visitorId, agentSlug) {
8631
8893
 
8632
8894
  // server/routes/public-reader.ts
8633
8895
  var app5 = new Hono();
8634
- var TAG21 = "[public-webchat]";
8896
+ var TAG22 = "[public-webchat]";
8635
8897
  function parseAccessSessionId(cookieHeader) {
8636
8898
  if (!cookieHeader) return null;
8637
8899
  const part = cookieHeader.split(";").map((p) => p.trim()).find((p) => p.startsWith("__access_session="));
@@ -8679,7 +8941,7 @@ function enumeratePublicRows() {
8679
8941
  app5.get("/session", (c) => {
8680
8942
  const visitor = resolveVisitor(c);
8681
8943
  if (!visitor) {
8682
- console.error(`${TAG21} op=reader-refused reason=no-anchor path=/session`);
8944
+ console.error(`${TAG22} op=reader-refused reason=no-anchor path=/session`);
8683
8945
  return c.json({ error: "gate-required" }, 401);
8684
8946
  }
8685
8947
  const rows = enumeratePublicRows();
@@ -8698,11 +8960,11 @@ app5.get("/session", (c) => {
8698
8960
  found = effectiveSlug ? resolveOpenVisitorSession(rows, visitor.visitorId, effectiveSlug) : null;
8699
8961
  }
8700
8962
  if (found) {
8701
- console.log(`${TAG21} op=session-resume anchor=${visitor.kind} key=${(found.senderId ?? "").slice(0, 8)} sessionId=${found.sessionId.slice(0, 8)}`);
8963
+ console.log(`${TAG22} op=session-resume anchor=${visitor.kind} key=${(found.senderId ?? "").slice(0, 8)} sessionId=${found.sessionId.slice(0, 8)}`);
8702
8964
  return c.json({ sessionId: found.sessionId, projectDir: found.projectDir, sessionKey: found.senderId });
8703
8965
  }
8704
8966
  const sessionKey = crypto.randomUUID();
8705
- console.log(`${TAG21} op=session-new anchor=${visitor.kind} key=${sessionKey.slice(0, 8)}`);
8967
+ console.log(`${TAG22} op=session-new anchor=${visitor.kind} key=${sessionKey.slice(0, 8)}`);
8706
8968
  return c.json({ sessionId: null, projectDir: null, sessionKey });
8707
8969
  });
8708
8970
  function publicUploadsDir(accountId, sessionId) {
@@ -8731,7 +8993,7 @@ function readFrom2(path2, from) {
8731
8993
  app5.get("/stream", (c) => {
8732
8994
  const visitor = resolveVisitor(c);
8733
8995
  if (!visitor) {
8734
- console.error(`${TAG21} op=reader-refused reason=no-anchor path=/stream`);
8996
+ console.error(`${TAG22} op=reader-refused reason=no-anchor path=/stream`);
8735
8997
  return c.json({ error: "gate-required" }, 401);
8736
8998
  }
8737
8999
  const sessionId = c.req.query("sessionId") ?? "";
@@ -8749,12 +9011,12 @@ app5.get("/stream", (c) => {
8749
9011
  const isPublicWebchat = meta.role === "public" && meta.channel === "webchat";
8750
9012
  const owns = visitor.kind === "person" ? meta.personId === visitor.personId : meta.personId === null && meta.visitorId === visitor.visitorId;
8751
9013
  if (!(isPublicWebchat && owns)) {
8752
- console.error(`${TAG21} op=reader-refused reason=not-owner anchor=${visitor.kind} sessionId=${sessionId.slice(0, 8)}`);
9014
+ console.error(`${TAG22} op=reader-refused reason=not-owner anchor=${visitor.kind} sessionId=${sessionId.slice(0, 8)}`);
8753
9015
  return c.json({ error: "forbidden" }, 403);
8754
9016
  }
8755
9017
  const senderShort = (meta.senderId ?? "").slice(0, 8);
8756
9018
  const encoder = new TextEncoder();
8757
- console.log(`${TAG21} op=reader-open anchor=${visitor.kind} key=${senderShort} mode=delivered-only sessionId=${sessionId.slice(0, 8)}`);
9019
+ console.log(`${TAG22} op=reader-open anchor=${visitor.kind} key=${senderShort} mode=delivered-only sessionId=${sessionId.slice(0, 8)}`);
8758
9020
  const send = (controller, turn, id) => controller.enqueue(encoder.encode(`id: ${id}
8759
9021
  data: ${JSON.stringify(turn)}
8760
9022
 
@@ -8769,7 +9031,7 @@ data: ${JSON.stringify(turn)}
8769
9031
  const { turns, dropped } = filterDeliveredFrames(lines);
8770
9032
  if (uploadsDir) enrichPublicAttachments(turns, listSessionAttachmentsInDir(uploadsDir), attachmentCursor);
8771
9033
  for (const turn of turns) send(controller, turn, offset);
8772
- if (dropped > 0) console.log(`${TAG21} op=reader-filtered key=${senderShort} dropped=${dropped}`);
9034
+ if (dropped > 0) console.log(`${TAG22} op=reader-filtered key=${senderShort} dropped=${dropped}`);
8773
9035
  };
8774
9036
  try {
8775
9037
  const { buf, end } = readFrom2(jsonlPath, 0);
@@ -8804,7 +9066,7 @@ data: ${JSON.stringify(turn)}
8804
9066
  watcher?.close();
8805
9067
  clearInterval(poll);
8806
9068
  clearInterval(heartbeat);
8807
- console.log(`${TAG21} op=reader-close key=${senderShort}`);
9069
+ console.log(`${TAG22} op=reader-close key=${senderShort}`);
8808
9070
  try {
8809
9071
  controller.close();
8810
9072
  } catch {
@@ -8924,18 +9186,6 @@ function writeCanonicalOverrideId(accountDir, userId, sessionId) {
8924
9186
  renameSync(tmp, path2);
8925
9187
  }
8926
9188
 
8927
- // app/lib/channel-pty-bridge/admin-session-id.ts
8928
- import { createHash as createHash2 } from "crypto";
8929
- function adminSessionIdFor(accountId, senderId, personId) {
8930
- const subject = personId && personId.length > 0 ? `person:${personId}` : senderId;
8931
- const b = createHash2("sha256").update(`${accountId}:admin:${subject}`).digest().subarray(0, 16);
8932
- const v = Buffer.from(b);
8933
- v[6] = v[6] & 15 | 64;
8934
- v[8] = v[8] & 63 | 128;
8935
- const h = v.toString("hex");
8936
- return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
8937
- }
8938
-
8939
9189
  // ../lib/models/src/index.ts
8940
9190
  var OPUS_MODEL = "claude-opus-4-8[1m]";
8941
9191
  var SONNET_MODEL = "claude-sonnet-5";
@@ -9913,7 +10163,7 @@ function routeTelegramUpdate(input) {
9913
10163
  }
9914
10164
 
9915
10165
  // server/routes/telegram.ts
9916
- var TAG22 = "[telegram-inbound]";
10166
+ var TAG23 = "[telegram-inbound]";
9917
10167
  function configDirName() {
9918
10168
  const platformRoot3 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
9919
10169
  const brandPath = join20(platformRoot3, "config", "brand.json");
@@ -9929,36 +10179,23 @@ function secretPath(botType) {
9929
10179
  const filename = botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret";
9930
10180
  return resolve15(homedir(), configDirName(), filename);
9931
10181
  }
9932
- async function sendTelegram(botToken, chatId, text) {
9933
- try {
9934
- const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
9935
- method: "POST",
9936
- headers: { "Content-Type": "application/json" },
9937
- body: JSON.stringify({ chat_id: chatId, text })
9938
- });
9939
- const data = await res.json();
9940
- return data.ok ? { ok: true } : { ok: false, error: data.description ?? "send failed" };
9941
- } catch (e) {
9942
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
9943
- }
9944
- }
9945
10182
  var app7 = new Hono();
9946
10183
  app7.post("/", async (c) => {
9947
10184
  const botParam = c.req.query("bot");
9948
10185
  const botType = botParam === "admin" ? "admin" : botParam === "public" ? "public" : null;
9949
10186
  if (!botType) {
9950
- console.error(`${TAG22} op=reject reason=missing-bot-param`);
10187
+ console.error(`${TAG23} op=reject reason=missing-bot-param`);
9951
10188
  return c.json({ ok: false }, 400);
9952
10189
  }
9953
10190
  const sp = secretPath(botType);
9954
10191
  if (!existsSync12(sp)) {
9955
- console.error(`${TAG22} op=reject reason=no-secret-file botType=${botType}`);
10192
+ console.error(`${TAG23} op=reject reason=no-secret-file botType=${botType}`);
9956
10193
  return c.json({ ok: false }, 401);
9957
10194
  }
9958
10195
  const expected = readFileSync21(sp, "utf-8").trim();
9959
10196
  const got = c.req.header("x-telegram-bot-api-secret-token") ?? "";
9960
10197
  if (got !== expected) {
9961
- console.error(`${TAG22} op=reject reason=bad-secret botType=${botType}`);
10198
+ console.error(`${TAG23} op=reject reason=bad-secret botType=${botType}`);
9962
10199
  return c.json({ ok: false }, 401);
9963
10200
  }
9964
10201
  let update;
@@ -9969,40 +10206,40 @@ app7.post("/", async (c) => {
9969
10206
  }
9970
10207
  const account = resolveAccount();
9971
10208
  if (!account) {
9972
- console.error(`${TAG22} op=reject reason=no-account`);
10209
+ console.error(`${TAG23} op=reject reason=no-account`);
9973
10210
  return c.json({ ok: true }, 200);
9974
10211
  }
9975
10212
  const decision = routeTelegramUpdate({ update, botType, config: account.config.telegram ?? {} });
9976
10213
  if (decision.kind === "ignore") {
9977
- console.error(`${TAG22} op=ignore reason=${decision.reason}`);
10214
+ console.error(`${TAG23} op=ignore reason=${decision.reason}`);
9978
10215
  return c.json({ ok: true }, 200);
9979
10216
  }
9980
10217
  if (decision.kind === "denied") {
9981
- console.error(`${TAG22} op=denied reason=${decision.reason} agentType=${decision.agentType}`);
10218
+ console.error(`${TAG23} op=denied reason=${decision.reason} agentType=${decision.agentType}`);
9982
10219
  return c.json({ ok: true }, 200);
9983
10220
  }
9984
10221
  const role = decision.agentType === "admin" ? "admin" : "public";
9985
10222
  const agentSlug = role === "admin" ? "admin" : resolveDefaultAgentSlug(account.accountDir);
9986
10223
  if (!agentSlug) {
9987
- console.error(`${TAG22} op=reject reason=no-default-agent`);
10224
+ console.error(`${TAG23} op=reject reason=no-default-agent`);
9988
10225
  return c.json({ ok: true }, 200);
9989
10226
  }
9990
- console.error(`${TAG22} op=update accountId=${account.accountId} from=${decision.senderId}`);
10227
+ console.error(`${TAG23} op=update accountId=${account.accountId} from=${decision.senderId}`);
9991
10228
  const botToken = botType === "admin" ? account.config.telegram?.adminBotToken : account.config.telegram?.publicBotToken;
9992
10229
  const { senderId, chatId, text } = decision;
9993
10230
  const gateway = getTelegramGateway();
9994
10231
  if (!gateway) {
9995
- console.error(`${TAG22} op=reject reason=gateway-not-ready from=${senderId}`);
10232
+ console.error(`${TAG23} op=reject reason=gateway-not-ready from=${senderId}`);
9996
10233
  return c.json({ ok: true }, 200);
9997
10234
  }
9998
10235
  const reply = async (replyText) => {
9999
10236
  if (!botToken) {
10000
10237
  const reason = account.config.telegram ? "no-bot-token" : "no-telegram-config";
10001
- console.error(`${TAG22} op=reply-dropped reason=${reason} botType=${botType} from=${senderId}`);
10238
+ console.error(`${TAG23} op=reply-dropped reason=${reason} botType=${botType} from=${senderId}`);
10002
10239
  return;
10003
10240
  }
10004
- const sent = await sendTelegram(botToken, chatId, replyText);
10005
- console.error(`${TAG22} op=reply-sent from=${senderId} ok=${sent.ok}${sent.ok ? "" : ` error=${sent.error}`}`);
10241
+ const sent = await sendTelegramText(botToken, chatId, replyText);
10242
+ console.error(`${TAG23} op=reply-sent from=${senderId} ok=${sent.ok}${sent.ok ? "" : ` error=${sent.error}`}`);
10006
10243
  };
10007
10244
  void gateway.handleInbound({
10008
10245
  accountId: account.accountId,
@@ -10020,7 +10257,7 @@ var telegram_default = app7;
10020
10257
  // server/routes/quickbooks.ts
10021
10258
  import { join as join21 } from "path";
10022
10259
  import { existsSync as existsSync13, readFileSync as readFileSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync4, rmSync, renameSync as renameSync4 } from "fs";
10023
- var TAG23 = "[quickbooks]";
10260
+ var TAG24 = "[quickbooks]";
10024
10261
  var INTUIT_TOKEN_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
10025
10262
  var STATE_RE = /^[A-Za-z0-9_-]+$/;
10026
10263
  function pendingPath(accountDir, state) {
@@ -10033,14 +10270,14 @@ async function completeConsent(args) {
10033
10270
  const { accountDir, code, realmId } = args;
10034
10271
  const state = args.state;
10035
10272
  if (!state || !STATE_RE.test(state) || !existsSync13(pendingPath(accountDir, state))) {
10036
- console.error(`${TAG23} op=callback state=${state ?? ""} stateValid=false realmId=${realmId ?? ""}`);
10273
+ console.error(`${TAG24} op=callback state=${state ?? ""} stateValid=false realmId=${realmId ?? ""}`);
10037
10274
  return { ok: false, status: 400, message: "Invalid or unknown authorization state.", stateValid: false };
10038
10275
  }
10039
10276
  const claimFile = `${pendingPath(accountDir, state)}.claimed`;
10040
10277
  try {
10041
10278
  renameSync4(pendingPath(accountDir, state), claimFile);
10042
10279
  } catch {
10043
- console.error(`${TAG23} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10280
+ console.error(`${TAG24} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10044
10281
  return { ok: false, status: 400, message: "Invalid or unknown authorization state.", stateValid: false };
10045
10282
  }
10046
10283
  try {
@@ -10048,14 +10285,14 @@ async function completeConsent(args) {
10048
10285
  try {
10049
10286
  pending = JSON.parse(readFileSync22(claimFile, "utf-8"));
10050
10287
  } catch {
10051
- console.error(`${TAG23} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10288
+ console.error(`${TAG24} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10052
10289
  return { ok: false, status: 400, message: "Authorization state could not be read.", stateValid: false };
10053
10290
  }
10054
10291
  if (Date.now() > pending.expiresAt) {
10055
- console.error(`${TAG23} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10292
+ console.error(`${TAG24} op=callback state=${state} stateValid=false realmId=${realmId ?? ""}`);
10056
10293
  return { ok: false, status: 400, message: "Authorization request expired. Generate a new consent link.", stateValid: false };
10057
10294
  }
10058
- console.error(`${TAG23} op=callback state=${state} stateValid=true realmId=${realmId ?? ""}`);
10295
+ console.error(`${TAG24} op=callback state=${state} stateValid=true realmId=${realmId ?? ""}`);
10059
10296
  if (!code || !realmId) {
10060
10297
  return { ok: false, status: 400, message: "Missing code or realmId in the callback.", stateValid: true };
10061
10298
  }
@@ -10079,12 +10316,12 @@ async function completeConsent(args) {
10079
10316
  });
10080
10317
  const text = await res.text();
10081
10318
  if (!res.ok) {
10082
- console.error(`${TAG23} op=token-exchange realmId=${realmId} persisted=false`);
10319
+ console.error(`${TAG24} op=token-exchange realmId=${realmId} persisted=false`);
10083
10320
  return { ok: false, status: 502, message: `Token exchange failed (${res.status}). Generate a new consent link to retry.`, stateValid: true };
10084
10321
  }
10085
10322
  const body = JSON.parse(text);
10086
10323
  if (!body.refresh_token) {
10087
- console.error(`${TAG23} op=token-exchange realmId=${realmId} persisted=false`);
10324
+ console.error(`${TAG24} op=token-exchange realmId=${realmId} persisted=false`);
10088
10325
  return { ok: false, status: 502, message: "Token exchange returned no refresh token.", stateValid: true };
10089
10326
  }
10090
10327
  const now = Date.now();
@@ -10097,7 +10334,7 @@ async function completeConsent(args) {
10097
10334
  mkdirSync4(join21(accountDir, "secrets"), { recursive: true });
10098
10335
  writeFileSync8(storePath(accountDir), JSON.stringify(store2, null, 2), { mode: 384 });
10099
10336
  const persisted = JSON.parse(readFileSync22(storePath(accountDir), "utf-8")).connections?.[realmId]?.refreshToken === body.refresh_token;
10100
- console.error(`${TAG23} op=token-exchange realmId=${realmId} persisted=${persisted}`);
10337
+ console.error(`${TAG24} op=token-exchange realmId=${realmId} persisted=${persisted}`);
10101
10338
  return { ok: persisted, status: persisted ? 200 : 500, message: persisted ? `Connected company ${realmId}.` : "Failed to persist the connection.", stateValid: true };
10102
10339
  } finally {
10103
10340
  rmSync(claimFile, { force: true });
@@ -10110,7 +10347,7 @@ var app8 = new Hono();
10110
10347
  app8.get("/callback", async (c) => {
10111
10348
  const account = resolveAccount();
10112
10349
  if (!account) {
10113
- console.error(`${TAG23} op=callback stateValid=false realmId= reason=no-account`);
10350
+ console.error(`${TAG24} op=callback stateValid=false realmId= reason=no-account`);
10114
10351
  return c.html(page("QuickBooks", "This install has no account configured."), 500);
10115
10352
  }
10116
10353
  const result = await completeConsent({
@@ -12369,7 +12606,7 @@ function resolveTunnelUrl() {
12369
12606
  if (!state) return null;
12370
12607
  return `https://${hostname2}.${state.domain}`;
12371
12608
  }
12372
- var TAG24 = "[claude-session-manager:wrapper]";
12609
+ var TAG25 = "[claude-session-manager:wrapper]";
12373
12610
  async function refuseIfClaudeAuthDead(c, route, sessionId) {
12374
12611
  const auth = await ensureAuth();
12375
12612
  if (auth.status !== "dead" && auth.status !== "missing") return null;
@@ -12387,12 +12624,12 @@ async function performSpawnWithInitialMessage(args) {
12387
12624
  const aboutOwner = await resolveOwnerProfileBlock(args.senderId, args.userId);
12388
12625
  const ownerMs = Date.now() - ownerStart;
12389
12626
  const aboutOwnerStatus = aboutOwner == null ? "absent" : "ok" in aboutOwner && aboutOwner.ok === false ? `unresolved:${aboutOwner.reason}` : "ok";
12390
- console.log(`${TAG24} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
12627
+ console.log(`${TAG25} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
12391
12628
  const dormantPlugins = computeDormantPlugins(args.senderId);
12392
12629
  const activePlugins = computeActivePlugins(args.senderId);
12393
12630
  const specialistDomains = computeSpecialistDomains(args.senderId);
12394
12631
  const tunnelUrl = resolveTunnelUrl();
12395
- console.log(`${TAG24} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
12632
+ console.log(`${TAG25} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
12396
12633
  const upstreamPayload = JSON.stringify({
12397
12634
  senderId: args.senderId,
12398
12635
  // Task 205 — pass userId through to the manager so it lands as
@@ -12419,24 +12656,24 @@ async function performSpawnWithInitialMessage(args) {
12419
12656
  // unshapely values.
12420
12657
  conversationNodeId: args.conversationNodeId
12421
12658
  });
12422
- console.log(`${TAG24} forward-spawn-start managerBase=${managerBase("claude-session-manager:wrapper")} bytes=${upstreamPayload.length} hidden=${args.hidden} specialist=${args.specialist ?? "none"}`);
12659
+ console.log(`${TAG25} forward-spawn-start managerBase=${managerBase("claude-session-manager:wrapper")} bytes=${upstreamPayload.length} hidden=${args.hidden} specialist=${args.specialist ?? "none"}`);
12423
12660
  const forwardStart = Date.now();
12424
12661
  const upstream = await fetch(`${managerBase("claude-session-manager:wrapper")}/public-spawn`, {
12425
12662
  method: "POST",
12426
12663
  headers: { "content-type": "application/json" },
12427
12664
  body: upstreamPayload
12428
12665
  }).catch((err) => {
12429
- console.error(`${TAG24} fetch-failed op=spawn message=${err instanceof Error ? err.message : String(err)} ms=${Date.now() - forwardStart}`);
12666
+ console.error(`${TAG25} fetch-failed op=spawn message=${err instanceof Error ? err.message : String(err)} ms=${Date.now() - forwardStart}`);
12430
12667
  return null;
12431
12668
  });
12432
12669
  if (!upstream) return {
12433
12670
  response: new Response(JSON.stringify({ error: "manager-unreachable" }), { status: 503, headers: { "content-type": "application/json" } }),
12434
12671
  claudeSessionId: null
12435
12672
  };
12436
- console.log(`${TAG24} forward-spawn-done status=${upstream.status} ms=${Date.now() - forwardStart}`);
12673
+ console.log(`${TAG25} forward-spawn-done status=${upstream.status} ms=${Date.now() - forwardStart}`);
12437
12674
  if (args.initialMessage) {
12438
12675
  const inputBytes = Buffer.byteLength(args.initialMessage, "utf8");
12439
- console.log(`${TAG24} initial-message-inlined bytes=${inputBytes}`);
12676
+ console.log(`${TAG25} initial-message-inlined bytes=${inputBytes}`);
12440
12677
  }
12441
12678
  const bodyText = await upstream.text().catch(() => "");
12442
12679
  let claudeSessionId = null;
@@ -12471,7 +12708,7 @@ app18.post("/", async (c) => {
12471
12708
  if (refusal) return refusal;
12472
12709
  const senderId = getAccountIdForSession(cacheKey) ?? "";
12473
12710
  if (!senderId) {
12474
- console.error(`${TAG24} reject reason=no-account-id cacheKey-prefix=${cacheKey.slice(0, 8)}`);
12711
+ console.error(`${TAG25} reject reason=no-account-id cacheKey-prefix=${cacheKey.slice(0, 8)}`);
12475
12712
  return c.json({ error: "admin-account-not-resolved" }, 500);
12476
12713
  }
12477
12714
  const userId = getUserIdForSession(cacheKey) ?? void 0;
@@ -12480,7 +12717,7 @@ app18.post("/", async (c) => {
12480
12717
  const permissionMode = typeof body.permissionMode === "string" ? body.permissionMode : void 0;
12481
12718
  const specialist = typeof body.specialist === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist) ? body.specialist : void 0;
12482
12719
  const model = typeof body.model === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(body.model) ? body.model : void 0;
12483
- console.log(`${TAG24} spawn-request-in surface=cookie accountId=${senderId.slice(0, 8)} userId=${userId ? userId.slice(0, 8) : "absent"} channel=${channel} permissionMode=${permissionMode ?? "default"} specialist=${specialist ?? "none"} model=${model ?? "default"} initialMessage=${initialMessage ? "yes" : "no"}`);
12720
+ console.log(`${TAG25} spawn-request-in surface=cookie accountId=${senderId.slice(0, 8)} userId=${userId ? userId.slice(0, 8) : "absent"} channel=${channel} permissionMode=${permissionMode ?? "default"} specialist=${specialist ?? "none"} model=${model ?? "default"} initialMessage=${initialMessage ? "yes" : "no"}`);
12484
12721
  const conversationNodeId = cacheKey ? getSessionIdForSession(cacheKey) : void 0;
12485
12722
  const { response, claudeSessionId } = await performSpawnWithInitialMessage({
12486
12723
  senderId,
@@ -12499,33 +12736,33 @@ app18.post("/", async (c) => {
12499
12736
  claudeSessionId,
12500
12737
  senderId
12501
12738
  );
12502
- console.log(`${TAG24} route-done surface=cookie status=${response.status} route-ms=${Date.now() - routeStart}`);
12739
+ console.log(`${TAG25} route-done surface=cookie status=${response.status} route-ms=${Date.now() - routeStart}`);
12503
12740
  return response;
12504
12741
  });
12505
12742
  var claude_sessions_default = app18;
12506
12743
 
12507
12744
  // server/routes/admin/log-ingest.ts
12508
- var TAG25 = "[log-ingest]";
12745
+ var TAG26 = "[log-ingest]";
12509
12746
  var TAG_PATTERN = /^[A-Za-z0-9_:-]{1,32}$/;
12510
12747
  var LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
12511
12748
  var MAX_LINE_BYTES = 4096;
12512
12749
  var app19 = new Hono();
12513
- function isLoopbackAddr(addr) {
12750
+ function isLoopbackAddr2(addr) {
12514
12751
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
12515
12752
  }
12516
12753
  app19.post("/", async (c) => {
12517
12754
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
12518
- if (!isLoopbackAddr(remoteAddr)) {
12519
- console.error(`${TAG25} reject reason=non-loopback remoteAddr=${remoteAddr}`);
12755
+ if (!isLoopbackAddr2(remoteAddr)) {
12756
+ console.error(`${TAG26} reject reason=non-loopback remoteAddr=${remoteAddr}`);
12520
12757
  return c.json({ error: "log-ingest-loopback-only" }, 403);
12521
12758
  }
12522
12759
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
12523
12760
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
12524
12761
  if (xffRaw.length > 0) {
12525
12762
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
12526
- const offender = tokens.find((t) => !isLoopbackAddr(t));
12763
+ const offender = tokens.find((t) => !isLoopbackAddr2(t));
12527
12764
  if (offender !== void 0) {
12528
- console.error(`${TAG25} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
12765
+ console.error(`${TAG26} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
12529
12766
  return c.json({ error: "log-ingest-loopback-only" }, 403);
12530
12767
  }
12531
12768
  }
@@ -12567,18 +12804,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
12567
12804
  ]);
12568
12805
  var app20 = new Hono();
12569
12806
  app20.post("/", async (c) => {
12570
- const TAG38 = "[admin:events]";
12807
+ const TAG39 = "[admin:events]";
12571
12808
  let body;
12572
12809
  try {
12573
12810
  body = await c.req.json();
12574
12811
  } catch (err) {
12575
12812
  const detail = err instanceof Error ? err.message : String(err);
12576
- console.error(`${TAG38} reject reason=body-not-json detail=${detail}`);
12813
+ console.error(`${TAG39} reject reason=body-not-json detail=${detail}`);
12577
12814
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
12578
12815
  }
12579
12816
  const event = typeof body.event === "string" ? body.event : "";
12580
12817
  if (!ALLOWED_EVENTS.has(event)) {
12581
- console.error(`${TAG38} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
12818
+ console.error(`${TAG39} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
12582
12819
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
12583
12820
  }
12584
12821
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -17393,7 +17630,7 @@ function managerLogFollowUrl(sessionId, opts) {
17393
17630
  }
17394
17631
 
17395
17632
  // server/routes/admin/linkedin-ingest.ts
17396
- var TAG26 = "[linkedin-ingest-route]";
17633
+ var TAG27 = "[linkedin-ingest-route]";
17397
17634
  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}$/;
17398
17635
  var ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
17399
17636
  var LINKEDIN_URL = /^https:\/\/www\.linkedin\.com\//;
@@ -17497,29 +17734,29 @@ app37.post("/", requireAdminSession, async (c) => {
17497
17734
  try {
17498
17735
  body = await c.req.json();
17499
17736
  } catch {
17500
- console.error(TAG26 + " rejected status=400 reason=schema:body-not-json");
17737
+ console.error(TAG27 + " rejected status=400 reason=schema:body-not-json");
17501
17738
  return c.json({ ok: false, error: "schema", reason: "body-not-json" }, 400);
17502
17739
  }
17503
17740
  const v = validate(body);
17504
17741
  if (!v.ok) {
17505
- console.error(TAG26 + " rejected status=" + v.status + " reason=" + v.reason + " missing=" + v.missing.join(","));
17742
+ console.error(TAG27 + " rejected status=" + v.status + " reason=" + v.reason + " missing=" + v.missing.join(","));
17506
17743
  return c.json({ ok: false, error: v.error, reason: v.reason, missing: v.missing }, v.status);
17507
17744
  }
17508
17745
  const envelope = v.envelope;
17509
17746
  const cacheKey = c.var.cacheKey ?? "";
17510
17747
  const senderId = getAccountIdForSession(cacheKey) ?? "";
17511
17748
  if (!senderId) {
17512
- console.error(TAG26 + " rejected status=500 reason=admin-account-not-resolved");
17749
+ console.error(TAG27 + " rejected status=500 reason=admin-account-not-resolved");
17513
17750
  return c.json({ ok: false, error: "admin-account-not-resolved" }, 500);
17514
17751
  }
17515
17752
  const payloadBytes = JSON.stringify(envelope).length;
17516
17753
  console.log(
17517
- TAG26 + " received kind=" + envelope.kind + " account=" + senderId.slice(0, 8) + " pageUrl=" + envelope.pageUrl + " dispatchId=" + envelope.dispatchId + " payloadBytes=" + payloadBytes
17754
+ TAG27 + " received kind=" + envelope.kind + " account=" + senderId.slice(0, 8) + " pageUrl=" + envelope.pageUrl + " dispatchId=" + envelope.dispatchId + " payloadBytes=" + payloadBytes
17518
17755
  );
17519
17756
  const initialMessage = buildInitialMessage(envelope);
17520
17757
  const spawnStart = Date.now();
17521
17758
  const sessionId = randomUUID12();
17522
- console.log(TAG26 + " route target=rc-spawn dispatchId=" + envelope.dispatchId + " sessionId=" + sessionId.slice(0, 8));
17759
+ console.log(TAG27 + " route target=rc-spawn dispatchId=" + envelope.dispatchId + " sessionId=" + sessionId.slice(0, 8));
17523
17760
  const spawned = await managerRcSpawn({
17524
17761
  sessionId,
17525
17762
  initialMessage,
@@ -17528,12 +17765,12 @@ app37.post("/", requireAdminSession, async (c) => {
17528
17765
  });
17529
17766
  if ("error" in spawned) {
17530
17767
  console.error(
17531
- TAG26 + " dispatch-failed dispatchId=" + envelope.dispatchId + " status=" + spawned.status + " ms=" + (Date.now() - spawnStart) + " message=" + spawned.error
17768
+ TAG27 + " dispatch-failed dispatchId=" + envelope.dispatchId + " status=" + spawned.status + " ms=" + (Date.now() - spawnStart) + " message=" + spawned.error
17532
17769
  );
17533
17770
  return c.json({ ok: false, error: "dispatch-failed", upstreamStatus: spawned.status, detail: spawned.error }, 502);
17534
17771
  }
17535
17772
  console.log(
17536
- TAG26 + " dispatched dispatchId=" + envelope.dispatchId + " taskId=" + spawned.sessionId + " ms=" + (Date.now() - spawnStart)
17773
+ TAG27 + " dispatched dispatchId=" + envelope.dispatchId + " taskId=" + spawned.sessionId + " ms=" + (Date.now() - spawnStart)
17537
17774
  );
17538
17775
  return c.json({ ok: true, dispatchId: envelope.dispatchId, taskId: spawned.sessionId }, 202);
17539
17776
  });
@@ -17541,7 +17778,7 @@ var linkedin_ingest_default = app37;
17541
17778
 
17542
17779
  // server/routes/admin/post-turn-context.ts
17543
17780
  import neo4j3 from "neo4j-driver";
17544
- var TAG27 = "[post-turn-context]";
17781
+ var TAG28 = "[post-turn-context]";
17545
17782
  var STRIPPED_PROPERTIES2 = /* @__PURE__ */ new Set([
17546
17783
  "embedding",
17547
17784
  "passwordHash",
@@ -17549,7 +17786,7 @@ var STRIPPED_PROPERTIES2 = /* @__PURE__ */ new Set([
17549
17786
  "otpCode",
17550
17787
  "cacheKey"
17551
17788
  ]);
17552
- function isLoopbackAddr2(addr) {
17789
+ function isLoopbackAddr3(addr) {
17553
17790
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17554
17791
  }
17555
17792
  function convertValue(value) {
@@ -17582,17 +17819,17 @@ function pruneProperties(raw) {
17582
17819
  var app38 = new Hono();
17583
17820
  app38.get("/", async (c) => {
17584
17821
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17585
- if (!isLoopbackAddr2(remoteAddr)) {
17586
- console.error(`${TAG27} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17822
+ if (!isLoopbackAddr3(remoteAddr)) {
17823
+ console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17587
17824
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
17588
17825
  }
17589
17826
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17590
17827
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17591
17828
  if (xffRaw.length > 0) {
17592
17829
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17593
- const offender = tokens.find((t) => !isLoopbackAddr2(t));
17830
+ const offender = tokens.find((t) => !isLoopbackAddr3(t));
17594
17831
  if (offender !== void 0) {
17595
- console.error(`${TAG27} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17832
+ console.error(`${TAG28} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17596
17833
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
17597
17834
  }
17598
17835
  }
@@ -17624,7 +17861,7 @@ app38.get("/", async (c) => {
17624
17861
  writes.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
17625
17862
  const total = Date.now() - started;
17626
17863
  console.log(
17627
- `${TAG27} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
17864
+ `${TAG28} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
17628
17865
  );
17629
17866
  return c.json({
17630
17867
  writes: writes.map(({ elementId, labels, properties }) => ({ elementId, labels, properties }))
@@ -17633,7 +17870,7 @@ app38.get("/", async (c) => {
17633
17870
  const elapsed = Date.now() - started;
17634
17871
  const message = err instanceof Error ? err.message : String(err);
17635
17872
  console.error(
17636
- `${TAG27} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
17873
+ `${TAG28} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
17637
17874
  );
17638
17875
  return c.json({ error: `post-turn-context unavailable: ${message}` }, 503);
17639
17876
  } finally {
@@ -17746,24 +17983,24 @@ function formatPreviousContext(writes) {
17746
17983
  }
17747
17984
 
17748
17985
  // server/routes/admin/public-session-context.ts
17749
- var TAG28 = "[public-session-context]";
17750
- function isLoopbackAddr3(addr) {
17986
+ var TAG29 = "[public-session-context]";
17987
+ function isLoopbackAddr4(addr) {
17751
17988
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17752
17989
  }
17753
17990
  var app39 = new Hono();
17754
17991
  app39.get("/", async (c) => {
17755
17992
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17756
- if (!isLoopbackAddr3(remoteAddr)) {
17757
- console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17993
+ if (!isLoopbackAddr4(remoteAddr)) {
17994
+ console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17758
17995
  return c.json({ error: "public-session-context-loopback-only" }, 403);
17759
17996
  }
17760
17997
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17761
17998
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17762
17999
  if (xffRaw.length > 0) {
17763
18000
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17764
- const offender = tokens.find((t) => !isLoopbackAddr3(t));
18001
+ const offender = tokens.find((t) => !isLoopbackAddr4(t));
17765
18002
  if (offender !== void 0) {
17766
- console.error(`${TAG28} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
18003
+ console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17767
18004
  return c.json({ error: "public-session-context-loopback-only" }, 403);
17768
18005
  }
17769
18006
  }
@@ -17777,14 +18014,14 @@ app39.get("/", async (c) => {
17777
18014
  const writes = await fetchSliceWrites(session, sliceToken, accountId);
17778
18015
  const total = Date.now() - started;
17779
18016
  console.log(
17780
- `${TAG28} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
18017
+ `${TAG29} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
17781
18018
  );
17782
18019
  return c.json({ writes });
17783
18020
  } catch (err) {
17784
18021
  const elapsed = Date.now() - started;
17785
18022
  const message = err instanceof Error ? err.message : String(err);
17786
18023
  console.error(
17787
- `${TAG28} neo4j-unreachable sliceToken=${sliceToken.slice(0, 8)} ms=${elapsed} err="${message}"`
18024
+ `${TAG29} neo4j-unreachable sliceToken=${sliceToken.slice(0, 8)} ms=${elapsed} err="${message}"`
17788
18025
  );
17789
18026
  return c.json({ error: `public-session-context unavailable: ${message}` }, 503);
17790
18027
  } finally {
@@ -17803,24 +18040,24 @@ function getWebchatGateway() {
17803
18040
  }
17804
18041
 
17805
18042
  // server/routes/admin/public-session-exit.ts
17806
- var TAG29 = "[public-session-exit-route]";
17807
- function isLoopbackAddr4(addr) {
18043
+ var TAG30 = "[public-session-exit-route]";
18044
+ function isLoopbackAddr5(addr) {
17808
18045
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17809
18046
  }
17810
18047
  var app40 = new Hono();
17811
18048
  app40.post("/", async (c) => {
17812
18049
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17813
- if (!isLoopbackAddr4(remoteAddr)) {
17814
- console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18050
+ if (!isLoopbackAddr5(remoteAddr)) {
18051
+ console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17815
18052
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
17816
18053
  }
17817
18054
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17818
18055
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17819
18056
  if (xffRaw.length > 0) {
17820
18057
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17821
- const offender = tokens.find((t) => !isLoopbackAddr4(t));
18058
+ const offender = tokens.find((t) => !isLoopbackAddr5(t));
17822
18059
  if (offender !== void 0) {
17823
- console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
18060
+ console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17824
18061
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
17825
18062
  }
17826
18063
  }
@@ -17834,7 +18071,7 @@ app40.post("/", async (c) => {
17834
18071
  if (!sessionId) return c.json({ error: "sessionId required" }, 400);
17835
18072
  const gateway = getWebchatGateway();
17836
18073
  if (!gateway) {
17837
- console.error(`${TAG29} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
18074
+ console.error(`${TAG30} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
17838
18075
  return c.json({ error: "webchat-gateway-unset" }, 503);
17839
18076
  }
17840
18077
  gateway.handlePublicSessionExit(sessionId);
@@ -17843,24 +18080,24 @@ app40.post("/", async (c) => {
17843
18080
  var public_session_exit_default = app40;
17844
18081
 
17845
18082
  // server/routes/admin/access-session-evict.ts
17846
- var TAG30 = "[access-session-evict]";
17847
- function isLoopbackAddr5(addr) {
18083
+ var TAG31 = "[access-session-evict]";
18084
+ function isLoopbackAddr6(addr) {
17848
18085
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17849
18086
  }
17850
18087
  var app41 = new Hono();
17851
18088
  app41.post("/", async (c) => {
17852
18089
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17853
- if (!isLoopbackAddr5(remoteAddr)) {
17854
- console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18090
+ if (!isLoopbackAddr6(remoteAddr)) {
18091
+ console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17855
18092
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
17856
18093
  }
17857
18094
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17858
18095
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17859
18096
  if (xffRaw.length > 0) {
17860
18097
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17861
- const offender = tokens.find((t) => !isLoopbackAddr5(t));
18098
+ const offender = tokens.find((t) => !isLoopbackAddr6(t));
17862
18099
  if (offender !== void 0) {
17863
- console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
18100
+ console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17864
18101
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
17865
18102
  }
17866
18103
  }
@@ -17873,31 +18110,31 @@ app41.post("/", async (c) => {
17873
18110
  const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
17874
18111
  if (!grantId) return c.json({ error: "grantId required" }, 400);
17875
18112
  const dropped = evictAccessSessionsByGrant(grantId);
17876
- console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
18113
+ console.log(`${TAG31} grantId=${grantId} dropped=${dropped}`);
17877
18114
  return c.json({ ok: true, dropped });
17878
18115
  });
17879
18116
  var access_session_evict_default = app41;
17880
18117
 
17881
18118
  // server/routes/admin/enrol-person.ts
17882
- var TAG31 = "[enrol]";
17883
- function isLoopbackAddr6(addr) {
18119
+ var TAG32 = "[enrol]";
18120
+ function isLoopbackAddr7(addr) {
17884
18121
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17885
18122
  }
17886
18123
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17887
18124
  var app42 = new Hono();
17888
18125
  app42.post("/", async (c) => {
17889
18126
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17890
- if (!isLoopbackAddr6(remoteAddr)) {
17891
- console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18127
+ if (!isLoopbackAddr7(remoteAddr)) {
18128
+ console.error(`${TAG32} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17892
18129
  return c.json({ error: "enrol-person-loopback-only" }, 403);
17893
18130
  }
17894
18131
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17895
18132
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17896
18133
  if (xffRaw.length > 0) {
17897
18134
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17898
- const offender = tokens.find((t) => !isLoopbackAddr6(t));
18135
+ const offender = tokens.find((t) => !isLoopbackAddr7(t));
17899
18136
  if (offender !== void 0) {
17900
- console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
18137
+ console.error(`${TAG32} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
17901
18138
  return c.json({ error: "enrol-person-loopback-only" }, 403);
17902
18139
  }
17903
18140
  }
@@ -17922,7 +18159,7 @@ app42.post("/", async (c) => {
17922
18159
  }
17923
18160
  const accountId = process.env.ACCOUNT_ID ?? "";
17924
18161
  if (!accountId) {
17925
- console.error(`${TAG31} op=person result=no-account-id`);
18162
+ console.error(`${TAG32} op=person result=no-account-id`);
17926
18163
  return c.json({ error: "no-account-id" }, 500);
17927
18164
  }
17928
18165
  let personId;
@@ -17930,7 +18167,7 @@ app42.post("/", async (c) => {
17930
18167
  personId = await enrolPerson({ accountId, phone, email, agentSlug });
17931
18168
  } catch (err) {
17932
18169
  console.error(
17933
- `${TAG31} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
18170
+ `${TAG32} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17934
18171
  );
17935
18172
  return c.json({ error: "enrol-failed" }, 500);
17936
18173
  }
@@ -17940,11 +18177,11 @@ app42.post("/", async (c) => {
17940
18177
  if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
17941
18178
  } catch (err) {
17942
18179
  console.error(
17943
- `${TAG31} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
18180
+ `${TAG32} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
17944
18181
  );
17945
18182
  }
17946
18183
  console.log(
17947
- `${TAG31} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
18184
+ `${TAG32} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
17948
18185
  );
17949
18186
  return c.json({ personId, grant }, 200);
17950
18187
  });
@@ -18513,7 +18750,7 @@ app45.route("/calendar", calendar_default);
18513
18750
  var admin_default = app45;
18514
18751
 
18515
18752
  // server/routes/access/verify-token.ts
18516
- var TAG32 = "[access-verify]";
18753
+ var TAG33 = "[access-verify]";
18517
18754
  var MINT_TAG = "[access-session-mint]";
18518
18755
  var COOKIE_NAME = "__access_session";
18519
18756
  var app46 = new Hono();
@@ -18532,39 +18769,39 @@ app46.post("/", async (c) => {
18532
18769
  }
18533
18770
  const rateMsg = checkAccessRateLimit(ip, agentSlug);
18534
18771
  if (rateMsg) {
18535
- console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
18772
+ console.error(`${TAG33} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
18536
18773
  return c.json({ error: rateMsg }, 429);
18537
18774
  }
18538
18775
  const grant = await findGrantByMagicToken(token);
18539
18776
  if (!grant) {
18540
18777
  recordAccessFailedAttempt(ip, agentSlug);
18541
- console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
18778
+ console.error(`${TAG33} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
18542
18779
  return c.json({ error: "invalid-or-expired-link" }, 401);
18543
18780
  }
18544
18781
  if (grant.agentSlug !== agentSlug) {
18545
18782
  recordAccessFailedAttempt(ip, agentSlug);
18546
18783
  console.error(
18547
- `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
18784
+ `${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
18548
18785
  );
18549
18786
  return c.json({ error: "invalid-or-expired-link" }, 401);
18550
18787
  }
18551
18788
  if (grant.status === "expired" || grant.status === "revoked") {
18552
18789
  recordAccessFailedAttempt(ip, agentSlug);
18553
18790
  console.error(
18554
- `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
18791
+ `${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
18555
18792
  );
18556
18793
  return c.json({ error: "access-no-longer-valid" }, 401);
18557
18794
  }
18558
18795
  if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
18559
18796
  recordAccessFailedAttempt(ip, agentSlug);
18560
18797
  console.error(
18561
- `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
18798
+ `${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
18562
18799
  );
18563
18800
  return c.json({ error: "access-no-longer-valid" }, 401);
18564
18801
  }
18565
18802
  if (!grant.sliceToken) {
18566
18803
  console.error(
18567
- `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
18804
+ `${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
18568
18805
  );
18569
18806
  return c.json({ error: "grant-misconfigured" }, 500);
18570
18807
  }
@@ -18580,12 +18817,12 @@ app46.post("/", async (c) => {
18580
18817
  await consumeMagicTokenAndActivate(grant.grantId);
18581
18818
  } catch (err) {
18582
18819
  console.error(
18583
- `${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
18820
+ `${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
18584
18821
  );
18585
18822
  return c.json({ error: "verification-failed" }, 500);
18586
18823
  }
18587
18824
  clearAccessRateLimit(ip, agentSlug);
18588
- console.log(`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
18825
+ console.log(`${TAG33} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
18589
18826
  console.log(
18590
18827
  `${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
18591
18828
  );
@@ -18661,7 +18898,7 @@ async function sendMagicLinkEmail(payload) {
18661
18898
  }
18662
18899
 
18663
18900
  // server/routes/access/request-magic-link.ts
18664
- var TAG33 = "[access-request-link]";
18901
+ var TAG34 = "[access-request-link]";
18665
18902
  var app47 = new Hono();
18666
18903
  var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
18667
18904
  app47.post("/", async (c) => {
@@ -18678,18 +18915,18 @@ app47.post("/", async (c) => {
18678
18915
  }
18679
18916
  const rateMsg = checkRequestLinkRateLimit(contactValue);
18680
18917
  if (rateMsg) {
18681
- console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
18918
+ console.error(`${TAG34} contactValue=${maskContact(contactValue)} result=rate-limited`);
18682
18919
  return c.json({ error: rateMsg }, 429);
18683
18920
  }
18684
18921
  recordRequestLinkAttempt(contactValue);
18685
18922
  const accountId = process.env.ACCOUNT_ID ?? "";
18686
18923
  if (!accountId) {
18687
- console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
18924
+ console.error(`${TAG34} contactValue=${maskContact(contactValue)} result=no-account-id`);
18688
18925
  return c.json({ message: VISITOR_MESSAGE }, 200);
18689
18926
  }
18690
18927
  const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
18691
18928
  if (!grant) {
18692
- console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
18929
+ console.log(`${TAG34} contactValue=${maskContact(contactValue)} result=notfound`);
18693
18930
  return c.json({ message: VISITOR_MESSAGE }, 200);
18694
18931
  }
18695
18932
  let token;
@@ -18697,7 +18934,7 @@ app47.post("/", async (c) => {
18697
18934
  token = await generateNewMagicToken(grant.grantId);
18698
18935
  } catch (err) {
18699
18936
  console.error(
18700
- `${TAG33} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
18937
+ `${TAG34} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
18701
18938
  );
18702
18939
  return c.json({ message: VISITOR_MESSAGE }, 200);
18703
18940
  }
@@ -18729,12 +18966,12 @@ app47.post("/", async (c) => {
18729
18966
  });
18730
18967
  if (!sendResult.ok) {
18731
18968
  console.error(
18732
- `${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18969
+ `${TAG34} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18733
18970
  );
18734
18971
  return c.json({ message: VISITOR_MESSAGE }, 200);
18735
18972
  }
18736
18973
  console.log(
18737
- `${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18974
+ `${TAG34} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18738
18975
  );
18739
18976
  return c.json({ message: VISITOR_MESSAGE }, 200);
18740
18977
  });
@@ -20752,6 +20989,7 @@ var WaGateway = class {
20752
20989
  const willDeliverNow = this.hub.isReady(input.senderId);
20753
20990
  const openable = input.media.filter((m) => m.type !== "audio");
20754
20991
  const mediaField = openable.length > 0 ? `media=${openable.map((m) => m.type).join(",")} mediaPaths=${openable.map((m) => m.path).join(",")}` : input.media.some((m) => m.type === "audio") ? "media=audio mediaPath=transcribed" : "media=none";
20992
+ const source = input.source ?? "user";
20755
20993
  this.replies.set(input.senderId, input.reply);
20756
20994
  this.docContexts.set(input.senderId, { accountId: input.accountId });
20757
20995
  this.hub.deliver(
@@ -20759,12 +20997,13 @@ var WaGateway = class {
20759
20997
  senderId: input.senderId,
20760
20998
  text: input.text,
20761
20999
  waMessageId: `wa-${++this.seq}`,
20762
- media: input.media
21000
+ media: input.media,
21001
+ source
20763
21002
  },
20764
21003
  Date.now()
20765
21004
  );
20766
21005
  console.error(
20767
- `[whatsapp-native] op=inbound senderId=${input.senderId} accountId=${input.accountId} bytes=${bytes} ${mediaField} delivery=${willDeliverNow ? "immediate" : "queued"}`
21006
+ `[whatsapp-native] op=inbound senderId=${input.senderId} accountId=${input.accountId} bytes=${bytes} ${mediaField} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"}`
20768
21007
  );
20769
21008
  if (!hadSubscriber && !this.spawning.has(input.senderId)) {
20770
21009
  this.spawning.add(input.senderId);
@@ -21701,7 +21940,7 @@ function makeFileDelivery(opts) {
21701
21940
  }
21702
21941
 
21703
21942
  // app/lib/whatsapp/inbound/file-delivery-bridge.ts
21704
- var TAG34 = "[whatsapp-adaptor]";
21943
+ var TAG35 = "[whatsapp-adaptor]";
21705
21944
  var SEND_USER_FILE2 = "SendUserFile";
21706
21945
  var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
21707
21946
  function platformRoot() {
@@ -21714,7 +21953,7 @@ function makeWhatsAppSendFile(entry) {
21714
21953
  maxyAccountId = resolvePlatformAccountId();
21715
21954
  } catch (err) {
21716
21955
  console.error(
21717
- `${TAG34} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
21956
+ `${TAG35} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
21718
21957
  );
21719
21958
  return { ok: false, error: "account-unresolved" };
21720
21959
  }
@@ -21728,7 +21967,7 @@ function makeWhatsAppSendFile(entry) {
21728
21967
  });
21729
21968
  if (result.ok) return { ok: true };
21730
21969
  console.error(
21731
- `${TAG34} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
21970
+ `${TAG35} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
21732
21971
  );
21733
21972
  return { ok: false, error: result.error };
21734
21973
  };
@@ -21736,7 +21975,7 @@ function makeWhatsAppSendFile(entry) {
21736
21975
  function makeWhatsAppFileDelivery(entry) {
21737
21976
  const shared = makeFileDelivery({
21738
21977
  entry,
21739
- tag: TAG34,
21978
+ tag: TAG35,
21740
21979
  channel: "whatsapp",
21741
21980
  sendFile: makeWhatsAppSendFile(entry)
21742
21981
  });
@@ -21771,7 +22010,7 @@ function makeWhatsAppFileDelivery(entry) {
21771
22010
  if (!delivered) {
21772
22011
  const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
21773
22012
  console.error(
21774
- `${TAG34} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
22013
+ `${TAG35} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
21775
22014
  );
21776
22015
  }
21777
22016
  }
@@ -22080,13 +22319,14 @@ var TelegramGateway = class {
22080
22319
  const bytes = Buffer.byteLength(input.text, "utf8");
22081
22320
  const hadSubscriber = this.hub.hasSubscriber(input.senderId);
22082
22321
  const willDeliverNow = this.hub.isReady(input.senderId);
22322
+ const source = input.source ?? "user";
22083
22323
  this.replies.set(input.senderId, input.reply);
22084
22324
  this.hub.deliver(
22085
- { key: input.senderId, text: input.text, messageId: `tg-${++this.seq}` },
22325
+ { key: input.senderId, text: input.text, messageId: `tg-${++this.seq}`, source },
22086
22326
  Date.now()
22087
22327
  );
22088
22328
  console.error(
22089
- `[telegram-native] op=inbound key=${input.senderId} accountId=${input.accountId} botType=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
22329
+ `[telegram-native] op=inbound key=${input.senderId} accountId=${input.accountId} botType=${input.role} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"}`
22090
22330
  );
22091
22331
  if (!hadSubscriber && !this.spawning.has(input.senderId)) {
22092
22332
  this.spawning.add(input.senderId);
@@ -22136,7 +22376,7 @@ function buildTelegramSpawnRequest(input) {
22136
22376
  import { realpathSync as realpathSync7 } from "fs";
22137
22377
  import { readFile as readFile7, stat as stat7 } from "fs/promises";
22138
22378
  import { resolve as resolve30, basename as basename11 } from "path";
22139
- var TAG35 = "[telegram:outbound]";
22379
+ var TAG36 = "[telegram:outbound]";
22140
22380
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
22141
22381
  async function sendTelegramDocument(input) {
22142
22382
  const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
@@ -22152,16 +22392,16 @@ async function sendTelegramDocument(input) {
22152
22392
  resolvedPath = realpathSync7(filePath);
22153
22393
  const accountResolved = realpathSync7(accountDir);
22154
22394
  if (!resolvedPath.startsWith(accountResolved + "/")) {
22155
- console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
22395
+ console.error(`${TAG36} document REJECTED reason=outside_account_directory`);
22156
22396
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
22157
22397
  }
22158
22398
  } catch (err) {
22159
22399
  const code = err.code;
22160
22400
  if (code === "ENOENT") {
22161
- console.error(`${TAG35} document ENOENT path=${filePath}`);
22401
+ console.error(`${TAG36} document ENOENT path=${filePath}`);
22162
22402
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
22163
22403
  }
22164
- console.error(`${TAG35} document path error: ${String(err)}`);
22404
+ console.error(`${TAG36} document path error: ${String(err)}`);
22165
22405
  return { ok: false, status: 500, error: String(err) };
22166
22406
  }
22167
22407
  const fileStat = await stat7(resolvedPath);
@@ -22194,13 +22434,13 @@ async function sendTelegramDocument(input) {
22194
22434
  error = e instanceof Error ? e.message : String(e);
22195
22435
  }
22196
22436
  console.error(
22197
- `${TAG35} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
22437
+ `${TAG36} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
22198
22438
  );
22199
22439
  return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
22200
22440
  }
22201
22441
 
22202
22442
  // app/lib/telegram/outbound/file-delivery.ts
22203
- var TAG36 = "[telegram:outbound]";
22443
+ var TAG37 = "[telegram:outbound]";
22204
22444
  function platformRoot2() {
22205
22445
  return process.env.MAXY_PLATFORM_ROOT || "";
22206
22446
  }
@@ -22212,11 +22452,11 @@ function makeTelegramSendFile(entry) {
22212
22452
  botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
22213
22453
  }
22214
22454
  if (!botToken) {
22215
- console.error(`${TAG36} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
22455
+ console.error(`${TAG37} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
22216
22456
  return { ok: false, error: "no-bot-token" };
22217
22457
  }
22218
22458
  if (entry.replyTarget == null) {
22219
- console.error(`${TAG36} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
22459
+ console.error(`${TAG37} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
22220
22460
  return { ok: false, error: "no-reply-target" };
22221
22461
  }
22222
22462
  let maxyAccountId;
@@ -22224,7 +22464,7 @@ function makeTelegramSendFile(entry) {
22224
22464
  maxyAccountId = resolvePlatformAccountId();
22225
22465
  } catch (err) {
22226
22466
  console.error(
22227
- `${TAG36} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
22467
+ `${TAG37} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
22228
22468
  );
22229
22469
  return { ok: false, error: "account-unresolved" };
22230
22470
  }
@@ -22242,7 +22482,7 @@ function makeTelegramSendFile(entry) {
22242
22482
  function makeTelegramFileDelivery(entry) {
22243
22483
  return makeFileDelivery({
22244
22484
  entry,
22245
- tag: TAG36,
22485
+ tag: TAG37,
22246
22486
  channel: "telegram",
22247
22487
  sendFile: makeTelegramSendFile(entry)
22248
22488
  });
@@ -22292,7 +22532,7 @@ function telegramServerPath() {
22292
22532
 
22293
22533
  // app/lib/channel-pty-bridge/public-session-end-review.ts
22294
22534
  import { randomUUID as randomUUID13 } from "crypto";
22295
- var TAG37 = "[public-session-review]";
22535
+ var TAG38 = "[public-session-review]";
22296
22536
  var CONSUMED_CAP = 500;
22297
22537
  var consumed = /* @__PURE__ */ new Set();
22298
22538
  function consumeOnce(sessionId) {
@@ -22319,7 +22559,7 @@ async function fetchJsonl(sessionId) {
22319
22559
  return await res.text();
22320
22560
  } catch (err) {
22321
22561
  console.error(
22322
- `${TAG37} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
22562
+ `${TAG38} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
22323
22563
  );
22324
22564
  return null;
22325
22565
  }
@@ -22343,7 +22583,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
22343
22583
  const res = await fetch(url);
22344
22584
  if (!res.ok) {
22345
22585
  console.error(
22346
- `${TAG37} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
22586
+ `${TAG38} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
22347
22587
  );
22348
22588
  return [];
22349
22589
  }
@@ -22351,7 +22591,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
22351
22591
  return Array.isArray(body.writes) ? body.writes : [];
22352
22592
  } catch (err) {
22353
22593
  console.error(
22354
- `${TAG37} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
22594
+ `${TAG38} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
22355
22595
  );
22356
22596
  return [];
22357
22597
  }
@@ -22380,7 +22620,7 @@ function composeInitialMessage(input) {
22380
22620
  }
22381
22621
  async function dispatchReviewer(input, initialMessage) {
22382
22622
  const sessionId = randomUUID13();
22383
- console.log(`${TAG37} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
22623
+ console.log(`${TAG38} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
22384
22624
  const spawned = await managerRcSpawn({
22385
22625
  sessionId,
22386
22626
  initialMessage,
@@ -22400,21 +22640,21 @@ async function firePublicSessionEndReview(input) {
22400
22640
  const dispatchedAt = Date.now();
22401
22641
  if (consumed.has(input.sessionId)) {
22402
22642
  console.log(
22403
- `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
22643
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
22404
22644
  );
22405
22645
  return;
22406
22646
  }
22407
22647
  const jsonl = await fetchJsonl(input.sessionId);
22408
22648
  if (!jsonl) {
22409
22649
  console.log(
22410
- `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
22650
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
22411
22651
  );
22412
22652
  return;
22413
22653
  }
22414
22654
  const operatorTurns = countOperatorTurns(jsonl);
22415
22655
  if (operatorTurns === 0) {
22416
22656
  console.log(
22417
- `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
22657
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
22418
22658
  );
22419
22659
  return;
22420
22660
  }
@@ -22428,23 +22668,34 @@ async function firePublicSessionEndReview(input) {
22428
22668
  });
22429
22669
  if (!consumeOnce(input.sessionId)) {
22430
22670
  console.log(
22431
- `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
22671
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
22432
22672
  );
22433
22673
  return;
22434
22674
  }
22435
22675
  const dispatched = await dispatchReviewer(input, initialMessage);
22436
22676
  if (!dispatched.ok) {
22437
22677
  console.error(
22438
- `${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
22678
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
22439
22679
  );
22440
22680
  return;
22441
22681
  }
22442
22682
  console.log(
22443
- `${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}`
22683
+ `${TAG38} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
22444
22684
  );
22445
22685
  }
22446
22686
 
22447
22687
  // app/lib/whatsapp/inbound/channel-admin-binding-drift.ts
22688
+ function findAccountManagerDrift(accountManagers, adminPhones, validAccountIds) {
22689
+ const drift = [];
22690
+ for (const [phone, managesAccount] of Object.entries(accountManagers)) {
22691
+ if (!validAccountIds.includes(managesAccount)) {
22692
+ drift.push({ phone, managesAccount, reason: "not-in-registry" });
22693
+ } else if (adminPhones.some((a) => phonesMatch(a, phone))) {
22694
+ drift.push({ phone, managesAccount, reason: "phone-also-admin" });
22695
+ }
22696
+ }
22697
+ return drift;
22698
+ }
22448
22699
  function findChannelAdminBindingDrift(adminPhones, admins, users) {
22449
22700
  const drift = [];
22450
22701
  for (const phone of adminPhones) {
@@ -22467,6 +22718,7 @@ function warnOnChannelAdminBindingDrift() {
22467
22718
  } catch {
22468
22719
  return;
22469
22720
  }
22721
+ const validAccountIds = accounts.map((a) => a.accountId);
22470
22722
  for (const acct of accounts) {
22471
22723
  const adminPhones = readAdminPhones(acct.accountDir);
22472
22724
  const drift = findChannelAdminBindingDrift(adminPhones, acct.config.admins ?? [], users);
@@ -22476,6 +22728,12 @@ function warnOnChannelAdminBindingDrift() {
22476
22728
  `[admin-identity] binding-drift accountId=${acct.accountId} phone=${d.phone} reason=${d.reason}${tail}`
22477
22729
  );
22478
22730
  }
22731
+ const managerDrift = findAccountManagerDrift(readAccountManagers(acct.accountDir), adminPhones, validAccountIds);
22732
+ for (const d of managerDrift) {
22733
+ console.error(
22734
+ `[admin-identity] account-manager-drift accountId=${acct.accountId} phone=${d.phone} managesAccount=${d.managesAccount} reason=${d.reason}`
22735
+ );
22736
+ }
22479
22737
  }
22480
22738
  }
22481
22739
 
@@ -22840,8 +23098,14 @@ var waGateway = new WaGateway({
22840
23098
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
22841
23099
  },
22842
23100
  ensureChannelSession: async ({ accountId, senderId, role, personId, gatewayUrl, serverPath }) => {
22843
- const req = buildWaSpawnRequest({ accountId, senderId, role, personId, gatewayUrl, serverPath });
22844
- const userId = role === "admin" ? resolveAdminUserId({ accountId, senderPhone: senderId }) ?? void 0 : void 0;
23101
+ const houseDir = role === "admin" ? listValidAccounts().find((a) => a.accountId === accountId)?.accountDir ?? null : null;
23102
+ const managed = houseDir ? managedAccountFor(readAccountManagers(houseDir), senderId) : null;
23103
+ const effectiveAccountId = managed ?? accountId;
23104
+ if (role === "admin") {
23105
+ console.error(`[whatsapp-native] op=account-manager-route senderId=${senderId} managesAccount=${managed ?? "none"} effectiveAccount=${effectiveAccountId}`);
23106
+ }
23107
+ const req = buildWaSpawnRequest({ accountId: effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath });
23108
+ const userId = role === "admin" ? resolveAdminUserId({ accountId: effectiveAccountId, senderPhone: senderId }) ?? void 0 : void 0;
22845
23109
  if (role === "admin") {
22846
23110
  console.error(`[whatsapp-native] op=admin-identity senderId=${senderId} userId=${userId ?? "owner-fallback"}`);
22847
23111
  }
@@ -22987,6 +23251,22 @@ var chatRoutes = createChatRoutes({
22987
23251
  handleInbound: (input) => webchatGateway.handleInbound(input),
22988
23252
  awaitReply: (key, timeoutMs) => webchatGateway.awaitReply(key, timeoutMs)
22989
23253
  });
23254
+ var scheduleInjectRoutes = createScheduleInjectRoutes({
23255
+ resolveAccount: () => resolveAccount(),
23256
+ waHandleInbound: (input) => waGateway.handleInbound(input),
23257
+ tgHandleInbound: (input) => telegramGateway.handleInbound(input),
23258
+ sendWhatsAppText: async (accountId, destination, text) => {
23259
+ const sock = getSocket(accountId);
23260
+ if (!sock) throw new Error("WhatsApp disconnected \u2014 cannot deliver scheduled reply");
23261
+ await sendTextMessage(sock, toWhatsappJid(destination), text, { accountId });
23262
+ },
23263
+ sendTelegramText: async (botToken, chatId, text) => {
23264
+ const sent = await sendTelegramText(botToken, chatId, text);
23265
+ if (!sent.ok) throw new Error(sent.error ?? "telegram send failed");
23266
+ },
23267
+ effectiveAccountFor: (accountId, accountDir, destination) => managedAccountFor(readAccountManagers(accountDir), destination) ?? accountId
23268
+ });
23269
+ app55.route("/api/channel/schedule-inject", scheduleInjectRoutes);
22990
23270
  app55.use("*", clientIpMiddleware);
22991
23271
  function allowsSameOriginFraming(path2) {
22992
23272
  return path2 === "/vnc-viewer.html" || path2.startsWith("/api/admin/attachment/") || path2.startsWith("/api/public-reader/attachment/") || path2 === "/api/admin/files/download";