@rubytech/create-maxy-code 0.1.384 → 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 (112) 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 +73 -2
  8. package/payload/platform/plugins/docs/references/admin-ui.md +12 -1
  9. package/payload/platform/plugins/docs/references/graph.md +11 -0
  10. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -0
  11. package/payload/platform/plugins/docs/references/sweep-guide.md +43 -0
  12. package/payload/platform/plugins/memory/references/schema-estate-agent.md +34 -0
  13. package/payload/platform/plugins/scheduling/PLUGIN.md +17 -1
  14. package/payload/platform/plugins/scheduling/mcp/dist/index.js +12 -0
  15. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.d.ts +2 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.d.ts.map +1 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js +101 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts +54 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.d.ts.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js +117 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/agent-dispatch.js.map +1 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.d.ts +7 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.d.ts.map +1 -1
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.js +6 -3
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-classify.js.map +1 -1
  28. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.d.ts +2 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.d.ts.map +1 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +88 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.d.ts +2 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.d.ts.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.js +77 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/standing-audit.test.js.map +1 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +59 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -0
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +131 -0
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -0
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +62 -15
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.d.ts +2 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.d.ts.map +1 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +110 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.d.ts +2 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.d.ts.map +1 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.js +35 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-agent.test.js.map +1 -0
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.d.ts +2 -0
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.d.ts.map +1 -0
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.js +81 -0
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-agent.test.js.map +1 -0
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +7 -0
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -4
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  58. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -0
  59. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  60. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +6 -0
  61. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts +2 -1
  63. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  64. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +32 -1
  65. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  66. package/payload/platform/plugins/sweep/.claude-plugin/plugin.json +8 -0
  67. package/payload/platform/plugins/sweep/PLUGIN.md +15 -0
  68. package/payload/platform/plugins/sweep/skills/sweep-workflow/SKILL.md +113 -0
  69. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  70. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +19 -4
  71. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  72. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -4
  73. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +10 -0
  74. package/payload/platform/scripts/seed-neo4j.sh +38 -0
  75. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +5 -0
  76. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  77. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  78. package/payload/platform/services/whatsapp-channel/dist/server.js +1 -1
  79. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  80. package/payload/server/public/assets/{AdminLoginScreens-C_h8V0Xs.js → AdminLoginScreens-BejIjbmU.js} +1 -1
  81. package/payload/server/public/assets/{AdminShell-Dqnxpn7U.js → AdminShell-D2-uBSB5.js} +1 -1
  82. package/payload/server/public/assets/{Checkbox-Bl1WRVGb.js → Checkbox-1F1tzqca.js} +1 -1
  83. package/payload/server/public/assets/Transcript-DkGa4CQH.js +2 -0
  84. package/payload/server/public/assets/{admin-ByoqNaSf.js → admin-DqQARkjy.js} +1 -1
  85. package/payload/server/public/assets/{browser-BXY1sNxv.js → browser-nDtEK6RC.js} +1 -1
  86. package/payload/server/public/assets/{calendar-CObRTPpU.js → calendar-CO4Bwmho.js} +1 -1
  87. package/payload/server/public/assets/chat-DeIge_bC.js +1 -0
  88. package/payload/server/public/assets/chevron-left-DhVdq0aN.js +1 -0
  89. package/payload/server/public/assets/data-B1GIdzHk.js +1 -0
  90. package/payload/server/public/assets/{graph-BCL0fVDH.js → graph-DFyicKd7.js} +1 -1
  91. package/payload/server/public/assets/{graph-labels-C0nn9nQi.js → graph-labels-D3BQdcpD.js} +1 -1
  92. package/payload/server/public/assets/{operator-B_1qYO7m.js → operator-BxrjWdT9.js} +1 -1
  93. package/payload/server/public/assets/{page-BQEl7aZO.js → page-ByDLq_o1.js} +1 -1
  94. package/payload/server/public/assets/page-D-Ep4bXd.js +32 -0
  95. package/payload/server/public/assets/{public-BzvwPHM0.js → public-DbdqfdYq.js} +1 -1
  96. package/payload/server/public/assets/{rotate-ccw-CHep9vl5.js → rotate-ccw-BkX8HODe.js} +1 -1
  97. package/payload/server/public/assets/{useSubAccountSwitcher-B1Kf32uG.js → useSubAccountSwitcher-DMbRhLPv.js} +1 -1
  98. package/payload/server/public/assets/{useSubAccountSwitcher-CfI3J2IX.css → useSubAccountSwitcher-DS0iqSkP.css} +1 -1
  99. package/payload/server/public/browser.html +4 -4
  100. package/payload/server/public/calendar.html +5 -5
  101. package/payload/server/public/chat.html +8 -8
  102. package/payload/server/public/data.html +8 -7
  103. package/payload/server/public/graph.html +8 -8
  104. package/payload/server/public/index.html +11 -10
  105. package/payload/server/public/operator.html +10 -10
  106. package/payload/server/public/public.html +8 -8
  107. package/payload/server/server.js +556 -273
  108. package/payload/server/public/assets/Transcript-C93Lifnb.js +0 -1
  109. package/payload/server/public/assets/chat-Dq-VHp7a.js +0 -1
  110. package/payload/server/public/assets/chevron-left-DjbK_wdQ.js +0 -1
  111. package/payload/server/public/assets/data-BXOoK8Ci.js +0 -1
  112. package/payload/server/public/assets/page-Dck23z9U.js +0 -32
@@ -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({
@@ -11211,11 +11448,15 @@ function uploadsDirFor2(accountId, attachmentId) {
11211
11448
  app15.get("/:attachmentId", requireAdminSession, async (c) => {
11212
11449
  const attachmentId = c.req.param("attachmentId");
11213
11450
  const cacheKey = c.var.cacheKey;
11451
+ const shortId2 = attachmentId.slice(0, 8);
11452
+ const shortSession = (c.req.query("session_key") ?? "").slice(0, 8);
11214
11453
  const pinnedAccountId = getAccountIdForSession(cacheKey);
11215
11454
  if (!pinnedAccountId) {
11455
+ console.log(`[admin-attachment] op=miss attachmentId=${shortId2} session=${shortSession} status=401 reason=no-account`);
11216
11456
  return new Response("Unauthorized", { status: 401 });
11217
11457
  }
11218
11458
  if (!ATTACHMENT_ID_RE2.test(attachmentId)) {
11459
+ console.log(`[admin-attachment] op=miss attachmentId=${shortId2} session=${shortSession} status=404 reason=bad-id`);
11219
11460
  return new Response("Not found", { status: 404 });
11220
11461
  }
11221
11462
  let accountId = pinnedAccountId;
@@ -11233,10 +11474,9 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
11233
11474
  }
11234
11475
  }
11235
11476
  }
11236
- const shortId2 = attachmentId.slice(0, 8);
11237
11477
  const shortPinned = pinnedAccountId.slice(0, 8);
11238
11478
  const crossAccountNote = found && accountId !== pinnedAccountId ? ` resolvedAccount=${accountId.slice(0, 8)}` : "";
11239
- console.log(`[admin-attachment] op=lookup attachmentId=${shortId2} pinnedAccount=${shortPinned} found=${found}${crossAccountNote}`);
11479
+ console.log(`[admin-attachment] op=lookup attachmentId=${shortId2} session=${shortSession} pinnedAccount=${shortPinned} found=${found}${crossAccountNote}`);
11240
11480
  if (!found) {
11241
11481
  return new Response("Not found", { status: 404 });
11242
11482
  }
@@ -12366,7 +12606,7 @@ function resolveTunnelUrl() {
12366
12606
  if (!state) return null;
12367
12607
  return `https://${hostname2}.${state.domain}`;
12368
12608
  }
12369
- var TAG24 = "[claude-session-manager:wrapper]";
12609
+ var TAG25 = "[claude-session-manager:wrapper]";
12370
12610
  async function refuseIfClaudeAuthDead(c, route, sessionId) {
12371
12611
  const auth = await ensureAuth();
12372
12612
  if (auth.status !== "dead" && auth.status !== "missing") return null;
@@ -12384,12 +12624,12 @@ async function performSpawnWithInitialMessage(args) {
12384
12624
  const aboutOwner = await resolveOwnerProfileBlock(args.senderId, args.userId);
12385
12625
  const ownerMs = Date.now() - ownerStart;
12386
12626
  const aboutOwnerStatus = aboutOwner == null ? "absent" : "ok" in aboutOwner && aboutOwner.ok === false ? `unresolved:${aboutOwner.reason}` : "ok";
12387
- console.log(`${TAG24} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
12627
+ console.log(`${TAG25} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
12388
12628
  const dormantPlugins = computeDormantPlugins(args.senderId);
12389
12629
  const activePlugins = computeActivePlugins(args.senderId);
12390
12630
  const specialistDomains = computeSpecialistDomains(args.senderId);
12391
12631
  const tunnelUrl = resolveTunnelUrl();
12392
- console.log(`${TAG24} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
12632
+ console.log(`${TAG25} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
12393
12633
  const upstreamPayload = JSON.stringify({
12394
12634
  senderId: args.senderId,
12395
12635
  // Task 205 — pass userId through to the manager so it lands as
@@ -12416,24 +12656,24 @@ async function performSpawnWithInitialMessage(args) {
12416
12656
  // unshapely values.
12417
12657
  conversationNodeId: args.conversationNodeId
12418
12658
  });
12419
- 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"}`);
12420
12660
  const forwardStart = Date.now();
12421
12661
  const upstream = await fetch(`${managerBase("claude-session-manager:wrapper")}/public-spawn`, {
12422
12662
  method: "POST",
12423
12663
  headers: { "content-type": "application/json" },
12424
12664
  body: upstreamPayload
12425
12665
  }).catch((err) => {
12426
- 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}`);
12427
12667
  return null;
12428
12668
  });
12429
12669
  if (!upstream) return {
12430
12670
  response: new Response(JSON.stringify({ error: "manager-unreachable" }), { status: 503, headers: { "content-type": "application/json" } }),
12431
12671
  claudeSessionId: null
12432
12672
  };
12433
- 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}`);
12434
12674
  if (args.initialMessage) {
12435
12675
  const inputBytes = Buffer.byteLength(args.initialMessage, "utf8");
12436
- console.log(`${TAG24} initial-message-inlined bytes=${inputBytes}`);
12676
+ console.log(`${TAG25} initial-message-inlined bytes=${inputBytes}`);
12437
12677
  }
12438
12678
  const bodyText = await upstream.text().catch(() => "");
12439
12679
  let claudeSessionId = null;
@@ -12468,7 +12708,7 @@ app18.post("/", async (c) => {
12468
12708
  if (refusal) return refusal;
12469
12709
  const senderId = getAccountIdForSession(cacheKey) ?? "";
12470
12710
  if (!senderId) {
12471
- 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)}`);
12472
12712
  return c.json({ error: "admin-account-not-resolved" }, 500);
12473
12713
  }
12474
12714
  const userId = getUserIdForSession(cacheKey) ?? void 0;
@@ -12477,7 +12717,7 @@ app18.post("/", async (c) => {
12477
12717
  const permissionMode = typeof body.permissionMode === "string" ? body.permissionMode : void 0;
12478
12718
  const specialist = typeof body.specialist === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist) ? body.specialist : void 0;
12479
12719
  const model = typeof body.model === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(body.model) ? body.model : void 0;
12480
- 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"}`);
12481
12721
  const conversationNodeId = cacheKey ? getSessionIdForSession(cacheKey) : void 0;
12482
12722
  const { response, claudeSessionId } = await performSpawnWithInitialMessage({
12483
12723
  senderId,
@@ -12496,33 +12736,33 @@ app18.post("/", async (c) => {
12496
12736
  claudeSessionId,
12497
12737
  senderId
12498
12738
  );
12499
- 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}`);
12500
12740
  return response;
12501
12741
  });
12502
12742
  var claude_sessions_default = app18;
12503
12743
 
12504
12744
  // server/routes/admin/log-ingest.ts
12505
- var TAG25 = "[log-ingest]";
12745
+ var TAG26 = "[log-ingest]";
12506
12746
  var TAG_PATTERN = /^[A-Za-z0-9_:-]{1,32}$/;
12507
12747
  var LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
12508
12748
  var MAX_LINE_BYTES = 4096;
12509
12749
  var app19 = new Hono();
12510
- function isLoopbackAddr(addr) {
12750
+ function isLoopbackAddr2(addr) {
12511
12751
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
12512
12752
  }
12513
12753
  app19.post("/", async (c) => {
12514
12754
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
12515
- if (!isLoopbackAddr(remoteAddr)) {
12516
- console.error(`${TAG25} reject reason=non-loopback remoteAddr=${remoteAddr}`);
12755
+ if (!isLoopbackAddr2(remoteAddr)) {
12756
+ console.error(`${TAG26} reject reason=non-loopback remoteAddr=${remoteAddr}`);
12517
12757
  return c.json({ error: "log-ingest-loopback-only" }, 403);
12518
12758
  }
12519
12759
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
12520
12760
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
12521
12761
  if (xffRaw.length > 0) {
12522
12762
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
12523
- const offender = tokens.find((t) => !isLoopbackAddr(t));
12763
+ const offender = tokens.find((t) => !isLoopbackAddr2(t));
12524
12764
  if (offender !== void 0) {
12525
- 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)}`);
12526
12766
  return c.json({ error: "log-ingest-loopback-only" }, 403);
12527
12767
  }
12528
12768
  }
@@ -12564,18 +12804,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
12564
12804
  ]);
12565
12805
  var app20 = new Hono();
12566
12806
  app20.post("/", async (c) => {
12567
- const TAG38 = "[admin:events]";
12807
+ const TAG39 = "[admin:events]";
12568
12808
  let body;
12569
12809
  try {
12570
12810
  body = await c.req.json();
12571
12811
  } catch (err) {
12572
12812
  const detail = err instanceof Error ? err.message : String(err);
12573
- console.error(`${TAG38} reject reason=body-not-json detail=${detail}`);
12813
+ console.error(`${TAG39} reject reason=body-not-json detail=${detail}`);
12574
12814
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
12575
12815
  }
12576
12816
  const event = typeof body.event === "string" ? body.event : "";
12577
12817
  if (!ALLOWED_EVENTS.has(event)) {
12578
- 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)}`);
12579
12819
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
12580
12820
  }
12581
12821
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -17390,7 +17630,7 @@ function managerLogFollowUrl(sessionId, opts) {
17390
17630
  }
17391
17631
 
17392
17632
  // server/routes/admin/linkedin-ingest.ts
17393
- var TAG26 = "[linkedin-ingest-route]";
17633
+ var TAG27 = "[linkedin-ingest-route]";
17394
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}$/;
17395
17635
  var ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
17396
17636
  var LINKEDIN_URL = /^https:\/\/www\.linkedin\.com\//;
@@ -17494,29 +17734,29 @@ app37.post("/", requireAdminSession, async (c) => {
17494
17734
  try {
17495
17735
  body = await c.req.json();
17496
17736
  } catch {
17497
- console.error(TAG26 + " rejected status=400 reason=schema:body-not-json");
17737
+ console.error(TAG27 + " rejected status=400 reason=schema:body-not-json");
17498
17738
  return c.json({ ok: false, error: "schema", reason: "body-not-json" }, 400);
17499
17739
  }
17500
17740
  const v = validate(body);
17501
17741
  if (!v.ok) {
17502
- 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(","));
17503
17743
  return c.json({ ok: false, error: v.error, reason: v.reason, missing: v.missing }, v.status);
17504
17744
  }
17505
17745
  const envelope = v.envelope;
17506
17746
  const cacheKey = c.var.cacheKey ?? "";
17507
17747
  const senderId = getAccountIdForSession(cacheKey) ?? "";
17508
17748
  if (!senderId) {
17509
- console.error(TAG26 + " rejected status=500 reason=admin-account-not-resolved");
17749
+ console.error(TAG27 + " rejected status=500 reason=admin-account-not-resolved");
17510
17750
  return c.json({ ok: false, error: "admin-account-not-resolved" }, 500);
17511
17751
  }
17512
17752
  const payloadBytes = JSON.stringify(envelope).length;
17513
17753
  console.log(
17514
- 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
17515
17755
  );
17516
17756
  const initialMessage = buildInitialMessage(envelope);
17517
17757
  const spawnStart = Date.now();
17518
17758
  const sessionId = randomUUID12();
17519
- 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));
17520
17760
  const spawned = await managerRcSpawn({
17521
17761
  sessionId,
17522
17762
  initialMessage,
@@ -17525,12 +17765,12 @@ app37.post("/", requireAdminSession, async (c) => {
17525
17765
  });
17526
17766
  if ("error" in spawned) {
17527
17767
  console.error(
17528
- 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
17529
17769
  );
17530
17770
  return c.json({ ok: false, error: "dispatch-failed", upstreamStatus: spawned.status, detail: spawned.error }, 502);
17531
17771
  }
17532
17772
  console.log(
17533
- 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)
17534
17774
  );
17535
17775
  return c.json({ ok: true, dispatchId: envelope.dispatchId, taskId: spawned.sessionId }, 202);
17536
17776
  });
@@ -17538,7 +17778,7 @@ var linkedin_ingest_default = app37;
17538
17778
 
17539
17779
  // server/routes/admin/post-turn-context.ts
17540
17780
  import neo4j3 from "neo4j-driver";
17541
- var TAG27 = "[post-turn-context]";
17781
+ var TAG28 = "[post-turn-context]";
17542
17782
  var STRIPPED_PROPERTIES2 = /* @__PURE__ */ new Set([
17543
17783
  "embedding",
17544
17784
  "passwordHash",
@@ -17546,7 +17786,7 @@ var STRIPPED_PROPERTIES2 = /* @__PURE__ */ new Set([
17546
17786
  "otpCode",
17547
17787
  "cacheKey"
17548
17788
  ]);
17549
- function isLoopbackAddr2(addr) {
17789
+ function isLoopbackAddr3(addr) {
17550
17790
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17551
17791
  }
17552
17792
  function convertValue(value) {
@@ -17579,17 +17819,17 @@ function pruneProperties(raw) {
17579
17819
  var app38 = new Hono();
17580
17820
  app38.get("/", async (c) => {
17581
17821
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17582
- if (!isLoopbackAddr2(remoteAddr)) {
17583
- console.error(`${TAG27} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17822
+ if (!isLoopbackAddr3(remoteAddr)) {
17823
+ console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17584
17824
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
17585
17825
  }
17586
17826
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17587
17827
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17588
17828
  if (xffRaw.length > 0) {
17589
17829
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17590
- const offender = tokens.find((t) => !isLoopbackAddr2(t));
17830
+ const offender = tokens.find((t) => !isLoopbackAddr3(t));
17591
17831
  if (offender !== void 0) {
17592
- 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)}`);
17593
17833
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
17594
17834
  }
17595
17835
  }
@@ -17621,7 +17861,7 @@ app38.get("/", async (c) => {
17621
17861
  writes.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
17622
17862
  const total = Date.now() - started;
17623
17863
  console.log(
17624
- `${TAG27} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
17864
+ `${TAG28} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
17625
17865
  );
17626
17866
  return c.json({
17627
17867
  writes: writes.map(({ elementId, labels, properties }) => ({ elementId, labels, properties }))
@@ -17630,7 +17870,7 @@ app38.get("/", async (c) => {
17630
17870
  const elapsed = Date.now() - started;
17631
17871
  const message = err instanceof Error ? err.message : String(err);
17632
17872
  console.error(
17633
- `${TAG27} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
17873
+ `${TAG28} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
17634
17874
  );
17635
17875
  return c.json({ error: `post-turn-context unavailable: ${message}` }, 503);
17636
17876
  } finally {
@@ -17743,24 +17983,24 @@ function formatPreviousContext(writes) {
17743
17983
  }
17744
17984
 
17745
17985
  // server/routes/admin/public-session-context.ts
17746
- var TAG28 = "[public-session-context]";
17747
- function isLoopbackAddr3(addr) {
17986
+ var TAG29 = "[public-session-context]";
17987
+ function isLoopbackAddr4(addr) {
17748
17988
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17749
17989
  }
17750
17990
  var app39 = new Hono();
17751
17991
  app39.get("/", async (c) => {
17752
17992
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17753
- if (!isLoopbackAddr3(remoteAddr)) {
17754
- console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17993
+ if (!isLoopbackAddr4(remoteAddr)) {
17994
+ console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17755
17995
  return c.json({ error: "public-session-context-loopback-only" }, 403);
17756
17996
  }
17757
17997
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17758
17998
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17759
17999
  if (xffRaw.length > 0) {
17760
18000
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17761
- const offender = tokens.find((t) => !isLoopbackAddr3(t));
18001
+ const offender = tokens.find((t) => !isLoopbackAddr4(t));
17762
18002
  if (offender !== void 0) {
17763
- 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)}`);
17764
18004
  return c.json({ error: "public-session-context-loopback-only" }, 403);
17765
18005
  }
17766
18006
  }
@@ -17774,14 +18014,14 @@ app39.get("/", async (c) => {
17774
18014
  const writes = await fetchSliceWrites(session, sliceToken, accountId);
17775
18015
  const total = Date.now() - started;
17776
18016
  console.log(
17777
- `${TAG28} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
18017
+ `${TAG29} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
17778
18018
  );
17779
18019
  return c.json({ writes });
17780
18020
  } catch (err) {
17781
18021
  const elapsed = Date.now() - started;
17782
18022
  const message = err instanceof Error ? err.message : String(err);
17783
18023
  console.error(
17784
- `${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}"`
17785
18025
  );
17786
18026
  return c.json({ error: `public-session-context unavailable: ${message}` }, 503);
17787
18027
  } finally {
@@ -17800,24 +18040,24 @@ function getWebchatGateway() {
17800
18040
  }
17801
18041
 
17802
18042
  // server/routes/admin/public-session-exit.ts
17803
- var TAG29 = "[public-session-exit-route]";
17804
- function isLoopbackAddr4(addr) {
18043
+ var TAG30 = "[public-session-exit-route]";
18044
+ function isLoopbackAddr5(addr) {
17805
18045
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17806
18046
  }
17807
18047
  var app40 = new Hono();
17808
18048
  app40.post("/", async (c) => {
17809
18049
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17810
- if (!isLoopbackAddr4(remoteAddr)) {
17811
- console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18050
+ if (!isLoopbackAddr5(remoteAddr)) {
18051
+ console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17812
18052
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
17813
18053
  }
17814
18054
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17815
18055
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17816
18056
  if (xffRaw.length > 0) {
17817
18057
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17818
- const offender = tokens.find((t) => !isLoopbackAddr4(t));
18058
+ const offender = tokens.find((t) => !isLoopbackAddr5(t));
17819
18059
  if (offender !== void 0) {
17820
- 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)}`);
17821
18061
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
17822
18062
  }
17823
18063
  }
@@ -17831,7 +18071,7 @@ app40.post("/", async (c) => {
17831
18071
  if (!sessionId) return c.json({ error: "sessionId required" }, 400);
17832
18072
  const gateway = getWebchatGateway();
17833
18073
  if (!gateway) {
17834
- 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)}`);
17835
18075
  return c.json({ error: "webchat-gateway-unset" }, 503);
17836
18076
  }
17837
18077
  gateway.handlePublicSessionExit(sessionId);
@@ -17840,24 +18080,24 @@ app40.post("/", async (c) => {
17840
18080
  var public_session_exit_default = app40;
17841
18081
 
17842
18082
  // server/routes/admin/access-session-evict.ts
17843
- var TAG30 = "[access-session-evict]";
17844
- function isLoopbackAddr5(addr) {
18083
+ var TAG31 = "[access-session-evict]";
18084
+ function isLoopbackAddr6(addr) {
17845
18085
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17846
18086
  }
17847
18087
  var app41 = new Hono();
17848
18088
  app41.post("/", async (c) => {
17849
18089
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17850
- if (!isLoopbackAddr5(remoteAddr)) {
17851
- console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18090
+ if (!isLoopbackAddr6(remoteAddr)) {
18091
+ console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17852
18092
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
17853
18093
  }
17854
18094
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17855
18095
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17856
18096
  if (xffRaw.length > 0) {
17857
18097
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17858
- const offender = tokens.find((t) => !isLoopbackAddr5(t));
18098
+ const offender = tokens.find((t) => !isLoopbackAddr6(t));
17859
18099
  if (offender !== void 0) {
17860
- 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)}`);
17861
18101
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
17862
18102
  }
17863
18103
  }
@@ -17870,31 +18110,31 @@ app41.post("/", async (c) => {
17870
18110
  const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
17871
18111
  if (!grantId) return c.json({ error: "grantId required" }, 400);
17872
18112
  const dropped = evictAccessSessionsByGrant(grantId);
17873
- console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
18113
+ console.log(`${TAG31} grantId=${grantId} dropped=${dropped}`);
17874
18114
  return c.json({ ok: true, dropped });
17875
18115
  });
17876
18116
  var access_session_evict_default = app41;
17877
18117
 
17878
18118
  // server/routes/admin/enrol-person.ts
17879
- var TAG31 = "[enrol]";
17880
- function isLoopbackAddr6(addr) {
18119
+ var TAG32 = "[enrol]";
18120
+ function isLoopbackAddr7(addr) {
17881
18121
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
17882
18122
  }
17883
18123
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17884
18124
  var app42 = new Hono();
17885
18125
  app42.post("/", async (c) => {
17886
18126
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
17887
- if (!isLoopbackAddr6(remoteAddr)) {
17888
- console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
18127
+ if (!isLoopbackAddr7(remoteAddr)) {
18128
+ console.error(`${TAG32} reject reason=non-loopback remoteAddr=${remoteAddr}`);
17889
18129
  return c.json({ error: "enrol-person-loopback-only" }, 403);
17890
18130
  }
17891
18131
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
17892
18132
  const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
17893
18133
  if (xffRaw.length > 0) {
17894
18134
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
17895
- const offender = tokens.find((t) => !isLoopbackAddr6(t));
18135
+ const offender = tokens.find((t) => !isLoopbackAddr7(t));
17896
18136
  if (offender !== void 0) {
17897
- 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)}`);
17898
18138
  return c.json({ error: "enrol-person-loopback-only" }, 403);
17899
18139
  }
17900
18140
  }
@@ -17919,7 +18159,7 @@ app42.post("/", async (c) => {
17919
18159
  }
17920
18160
  const accountId = process.env.ACCOUNT_ID ?? "";
17921
18161
  if (!accountId) {
17922
- console.error(`${TAG31} op=person result=no-account-id`);
18162
+ console.error(`${TAG32} op=person result=no-account-id`);
17923
18163
  return c.json({ error: "no-account-id" }, 500);
17924
18164
  }
17925
18165
  let personId;
@@ -17927,7 +18167,7 @@ app42.post("/", async (c) => {
17927
18167
  personId = await enrolPerson({ accountId, phone, email, agentSlug });
17928
18168
  } catch (err) {
17929
18169
  console.error(
17930
- `${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)}"`
17931
18171
  );
17932
18172
  return c.json({ error: "enrol-failed" }, 500);
17933
18173
  }
@@ -17937,11 +18177,11 @@ app42.post("/", async (c) => {
17937
18177
  if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
17938
18178
  } catch (err) {
17939
18179
  console.error(
17940
- `${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)}"`
17941
18181
  );
17942
18182
  }
17943
18183
  console.log(
17944
- `${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)}`
17945
18185
  );
17946
18186
  return c.json({ personId, grant }, 200);
17947
18187
  });
@@ -18510,7 +18750,7 @@ app45.route("/calendar", calendar_default);
18510
18750
  var admin_default = app45;
18511
18751
 
18512
18752
  // server/routes/access/verify-token.ts
18513
- var TAG32 = "[access-verify]";
18753
+ var TAG33 = "[access-verify]";
18514
18754
  var MINT_TAG = "[access-session-mint]";
18515
18755
  var COOKIE_NAME = "__access_session";
18516
18756
  var app46 = new Hono();
@@ -18529,39 +18769,39 @@ app46.post("/", async (c) => {
18529
18769
  }
18530
18770
  const rateMsg = checkAccessRateLimit(ip, agentSlug);
18531
18771
  if (rateMsg) {
18532
- console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
18772
+ console.error(`${TAG33} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
18533
18773
  return c.json({ error: rateMsg }, 429);
18534
18774
  }
18535
18775
  const grant = await findGrantByMagicToken(token);
18536
18776
  if (!grant) {
18537
18777
  recordAccessFailedAttempt(ip, agentSlug);
18538
- console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
18778
+ console.error(`${TAG33} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
18539
18779
  return c.json({ error: "invalid-or-expired-link" }, 401);
18540
18780
  }
18541
18781
  if (grant.agentSlug !== agentSlug) {
18542
18782
  recordAccessFailedAttempt(ip, agentSlug);
18543
18783
  console.error(
18544
- `${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}`
18545
18785
  );
18546
18786
  return c.json({ error: "invalid-or-expired-link" }, 401);
18547
18787
  }
18548
18788
  if (grant.status === "expired" || grant.status === "revoked") {
18549
18789
  recordAccessFailedAttempt(ip, agentSlug);
18550
18790
  console.error(
18551
- `${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}`
18552
18792
  );
18553
18793
  return c.json({ error: "access-no-longer-valid" }, 401);
18554
18794
  }
18555
18795
  if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
18556
18796
  recordAccessFailedAttempt(ip, agentSlug);
18557
18797
  console.error(
18558
- `${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}`
18559
18799
  );
18560
18800
  return c.json({ error: "access-no-longer-valid" }, 401);
18561
18801
  }
18562
18802
  if (!grant.sliceToken) {
18563
18803
  console.error(
18564
- `${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`
18565
18805
  );
18566
18806
  return c.json({ error: "grant-misconfigured" }, 500);
18567
18807
  }
@@ -18577,12 +18817,12 @@ app46.post("/", async (c) => {
18577
18817
  await consumeMagicTokenAndActivate(grant.grantId);
18578
18818
  } catch (err) {
18579
18819
  console.error(
18580
- `${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)}"`
18581
18821
  );
18582
18822
  return c.json({ error: "verification-failed" }, 500);
18583
18823
  }
18584
18824
  clearAccessRateLimit(ip, agentSlug);
18585
- 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}`);
18586
18826
  console.log(
18587
18827
  `${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
18588
18828
  );
@@ -18658,7 +18898,7 @@ async function sendMagicLinkEmail(payload) {
18658
18898
  }
18659
18899
 
18660
18900
  // server/routes/access/request-magic-link.ts
18661
- var TAG33 = "[access-request-link]";
18901
+ var TAG34 = "[access-request-link]";
18662
18902
  var app47 = new Hono();
18663
18903
  var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
18664
18904
  app47.post("/", async (c) => {
@@ -18675,18 +18915,18 @@ app47.post("/", async (c) => {
18675
18915
  }
18676
18916
  const rateMsg = checkRequestLinkRateLimit(contactValue);
18677
18917
  if (rateMsg) {
18678
- console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
18918
+ console.error(`${TAG34} contactValue=${maskContact(contactValue)} result=rate-limited`);
18679
18919
  return c.json({ error: rateMsg }, 429);
18680
18920
  }
18681
18921
  recordRequestLinkAttempt(contactValue);
18682
18922
  const accountId = process.env.ACCOUNT_ID ?? "";
18683
18923
  if (!accountId) {
18684
- console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
18924
+ console.error(`${TAG34} contactValue=${maskContact(contactValue)} result=no-account-id`);
18685
18925
  return c.json({ message: VISITOR_MESSAGE }, 200);
18686
18926
  }
18687
18927
  const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
18688
18928
  if (!grant) {
18689
- console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
18929
+ console.log(`${TAG34} contactValue=${maskContact(contactValue)} result=notfound`);
18690
18930
  return c.json({ message: VISITOR_MESSAGE }, 200);
18691
18931
  }
18692
18932
  let token;
@@ -18694,7 +18934,7 @@ app47.post("/", async (c) => {
18694
18934
  token = await generateNewMagicToken(grant.grantId);
18695
18935
  } catch (err) {
18696
18936
  console.error(
18697
- `${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)}"`
18698
18938
  );
18699
18939
  return c.json({ message: VISITOR_MESSAGE }, 200);
18700
18940
  }
@@ -18726,12 +18966,12 @@ app47.post("/", async (c) => {
18726
18966
  });
18727
18967
  if (!sendResult.ok) {
18728
18968
  console.error(
18729
- `${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18969
+ `${TAG34} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
18730
18970
  );
18731
18971
  return c.json({ message: VISITOR_MESSAGE }, 200);
18732
18972
  }
18733
18973
  console.log(
18734
- `${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18974
+ `${TAG34} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
18735
18975
  );
18736
18976
  return c.json({ message: VISITOR_MESSAGE }, 200);
18737
18977
  });
@@ -20749,6 +20989,7 @@ var WaGateway = class {
20749
20989
  const willDeliverNow = this.hub.isReady(input.senderId);
20750
20990
  const openable = input.media.filter((m) => m.type !== "audio");
20751
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";
20752
20993
  this.replies.set(input.senderId, input.reply);
20753
20994
  this.docContexts.set(input.senderId, { accountId: input.accountId });
20754
20995
  this.hub.deliver(
@@ -20756,12 +20997,13 @@ var WaGateway = class {
20756
20997
  senderId: input.senderId,
20757
20998
  text: input.text,
20758
20999
  waMessageId: `wa-${++this.seq}`,
20759
- media: input.media
21000
+ media: input.media,
21001
+ source
20760
21002
  },
20761
21003
  Date.now()
20762
21004
  );
20763
21005
  console.error(
20764
- `[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"}`
20765
21007
  );
20766
21008
  if (!hadSubscriber && !this.spawning.has(input.senderId)) {
20767
21009
  this.spawning.add(input.senderId);
@@ -21698,7 +21940,7 @@ function makeFileDelivery(opts) {
21698
21940
  }
21699
21941
 
21700
21942
  // app/lib/whatsapp/inbound/file-delivery-bridge.ts
21701
- var TAG34 = "[whatsapp-adaptor]";
21943
+ var TAG35 = "[whatsapp-adaptor]";
21702
21944
  var SEND_USER_FILE2 = "SendUserFile";
21703
21945
  var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
21704
21946
  function platformRoot() {
@@ -21711,7 +21953,7 @@ function makeWhatsAppSendFile(entry) {
21711
21953
  maxyAccountId = resolvePlatformAccountId();
21712
21954
  } catch (err) {
21713
21955
  console.error(
21714
- `${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)}`
21715
21957
  );
21716
21958
  return { ok: false, error: "account-unresolved" };
21717
21959
  }
@@ -21725,7 +21967,7 @@ function makeWhatsAppSendFile(entry) {
21725
21967
  });
21726
21968
  if (result.ok) return { ok: true };
21727
21969
  console.error(
21728
- `${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}`
21729
21971
  );
21730
21972
  return { ok: false, error: result.error };
21731
21973
  };
@@ -21733,7 +21975,7 @@ function makeWhatsAppSendFile(entry) {
21733
21975
  function makeWhatsAppFileDelivery(entry) {
21734
21976
  const shared = makeFileDelivery({
21735
21977
  entry,
21736
- tag: TAG34,
21978
+ tag: TAG35,
21737
21979
  channel: "whatsapp",
21738
21980
  sendFile: makeWhatsAppSendFile(entry)
21739
21981
  });
@@ -21768,7 +22010,7 @@ function makeWhatsAppFileDelivery(entry) {
21768
22010
  if (!delivered) {
21769
22011
  const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
21770
22012
  console.error(
21771
- `${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}`
21772
22014
  );
21773
22015
  }
21774
22016
  }
@@ -22077,13 +22319,14 @@ var TelegramGateway = class {
22077
22319
  const bytes = Buffer.byteLength(input.text, "utf8");
22078
22320
  const hadSubscriber = this.hub.hasSubscriber(input.senderId);
22079
22321
  const willDeliverNow = this.hub.isReady(input.senderId);
22322
+ const source = input.source ?? "user";
22080
22323
  this.replies.set(input.senderId, input.reply);
22081
22324
  this.hub.deliver(
22082
- { key: input.senderId, text: input.text, messageId: `tg-${++this.seq}` },
22325
+ { key: input.senderId, text: input.text, messageId: `tg-${++this.seq}`, source },
22083
22326
  Date.now()
22084
22327
  );
22085
22328
  console.error(
22086
- `[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"}`
22087
22330
  );
22088
22331
  if (!hadSubscriber && !this.spawning.has(input.senderId)) {
22089
22332
  this.spawning.add(input.senderId);
@@ -22133,7 +22376,7 @@ function buildTelegramSpawnRequest(input) {
22133
22376
  import { realpathSync as realpathSync7 } from "fs";
22134
22377
  import { readFile as readFile7, stat as stat7 } from "fs/promises";
22135
22378
  import { resolve as resolve30, basename as basename11 } from "path";
22136
- var TAG35 = "[telegram:outbound]";
22379
+ var TAG36 = "[telegram:outbound]";
22137
22380
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
22138
22381
  async function sendTelegramDocument(input) {
22139
22382
  const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
@@ -22149,16 +22392,16 @@ async function sendTelegramDocument(input) {
22149
22392
  resolvedPath = realpathSync7(filePath);
22150
22393
  const accountResolved = realpathSync7(accountDir);
22151
22394
  if (!resolvedPath.startsWith(accountResolved + "/")) {
22152
- console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
22395
+ console.error(`${TAG36} document REJECTED reason=outside_account_directory`);
22153
22396
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
22154
22397
  }
22155
22398
  } catch (err) {
22156
22399
  const code = err.code;
22157
22400
  if (code === "ENOENT") {
22158
- console.error(`${TAG35} document ENOENT path=${filePath}`);
22401
+ console.error(`${TAG36} document ENOENT path=${filePath}`);
22159
22402
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
22160
22403
  }
22161
- console.error(`${TAG35} document path error: ${String(err)}`);
22404
+ console.error(`${TAG36} document path error: ${String(err)}`);
22162
22405
  return { ok: false, status: 500, error: String(err) };
22163
22406
  }
22164
22407
  const fileStat = await stat7(resolvedPath);
@@ -22191,13 +22434,13 @@ async function sendTelegramDocument(input) {
22191
22434
  error = e instanceof Error ? e.message : String(e);
22192
22435
  }
22193
22436
  console.error(
22194
- `${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}` : "")
22195
22438
  );
22196
22439
  return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
22197
22440
  }
22198
22441
 
22199
22442
  // app/lib/telegram/outbound/file-delivery.ts
22200
- var TAG36 = "[telegram:outbound]";
22443
+ var TAG37 = "[telegram:outbound]";
22201
22444
  function platformRoot2() {
22202
22445
  return process.env.MAXY_PLATFORM_ROOT || "";
22203
22446
  }
@@ -22209,11 +22452,11 @@ function makeTelegramSendFile(entry) {
22209
22452
  botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
22210
22453
  }
22211
22454
  if (!botToken) {
22212
- 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}`);
22213
22456
  return { ok: false, error: "no-bot-token" };
22214
22457
  }
22215
22458
  if (entry.replyTarget == null) {
22216
- 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}`);
22217
22460
  return { ok: false, error: "no-reply-target" };
22218
22461
  }
22219
22462
  let maxyAccountId;
@@ -22221,7 +22464,7 @@ function makeTelegramSendFile(entry) {
22221
22464
  maxyAccountId = resolvePlatformAccountId();
22222
22465
  } catch (err) {
22223
22466
  console.error(
22224
- `${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)}`
22225
22468
  );
22226
22469
  return { ok: false, error: "account-unresolved" };
22227
22470
  }
@@ -22239,7 +22482,7 @@ function makeTelegramSendFile(entry) {
22239
22482
  function makeTelegramFileDelivery(entry) {
22240
22483
  return makeFileDelivery({
22241
22484
  entry,
22242
- tag: TAG36,
22485
+ tag: TAG37,
22243
22486
  channel: "telegram",
22244
22487
  sendFile: makeTelegramSendFile(entry)
22245
22488
  });
@@ -22289,7 +22532,7 @@ function telegramServerPath() {
22289
22532
 
22290
22533
  // app/lib/channel-pty-bridge/public-session-end-review.ts
22291
22534
  import { randomUUID as randomUUID13 } from "crypto";
22292
- var TAG37 = "[public-session-review]";
22535
+ var TAG38 = "[public-session-review]";
22293
22536
  var CONSUMED_CAP = 500;
22294
22537
  var consumed = /* @__PURE__ */ new Set();
22295
22538
  function consumeOnce(sessionId) {
@@ -22316,7 +22559,7 @@ async function fetchJsonl(sessionId) {
22316
22559
  return await res.text();
22317
22560
  } catch (err) {
22318
22561
  console.error(
22319
- `${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)}"`
22320
22563
  );
22321
22564
  return null;
22322
22565
  }
@@ -22340,7 +22583,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
22340
22583
  const res = await fetch(url);
22341
22584
  if (!res.ok) {
22342
22585
  console.error(
22343
- `${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}`
22344
22587
  );
22345
22588
  return [];
22346
22589
  }
@@ -22348,7 +22591,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
22348
22591
  return Array.isArray(body.writes) ? body.writes : [];
22349
22592
  } catch (err) {
22350
22593
  console.error(
22351
- `${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)}"`
22352
22595
  );
22353
22596
  return [];
22354
22597
  }
@@ -22377,7 +22620,7 @@ function composeInitialMessage(input) {
22377
22620
  }
22378
22621
  async function dispatchReviewer(input, initialMessage) {
22379
22622
  const sessionId = randomUUID13();
22380
- 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)}`);
22381
22624
  const spawned = await managerRcSpawn({
22382
22625
  sessionId,
22383
22626
  initialMessage,
@@ -22397,21 +22640,21 @@ async function firePublicSessionEndReview(input) {
22397
22640
  const dispatchedAt = Date.now();
22398
22641
  if (consumed.has(input.sessionId)) {
22399
22642
  console.log(
22400
- `${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`
22401
22644
  );
22402
22645
  return;
22403
22646
  }
22404
22647
  const jsonl = await fetchJsonl(input.sessionId);
22405
22648
  if (!jsonl) {
22406
22649
  console.log(
22407
- `${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`
22408
22651
  );
22409
22652
  return;
22410
22653
  }
22411
22654
  const operatorTurns = countOperatorTurns(jsonl);
22412
22655
  if (operatorTurns === 0) {
22413
22656
  console.log(
22414
- `${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`
22415
22658
  );
22416
22659
  return;
22417
22660
  }
@@ -22425,23 +22668,34 @@ async function firePublicSessionEndReview(input) {
22425
22668
  });
22426
22669
  if (!consumeOnce(input.sessionId)) {
22427
22670
  console.log(
22428
- `${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`
22429
22672
  );
22430
22673
  return;
22431
22674
  }
22432
22675
  const dispatched = await dispatchReviewer(input, initialMessage);
22433
22676
  if (!dispatched.ok) {
22434
22677
  console.error(
22435
- `${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}`
22436
22679
  );
22437
22680
  return;
22438
22681
  }
22439
22682
  console.log(
22440
- `${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}`
22441
22684
  );
22442
22685
  }
22443
22686
 
22444
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
+ }
22445
22699
  function findChannelAdminBindingDrift(adminPhones, admins, users) {
22446
22700
  const drift = [];
22447
22701
  for (const phone of adminPhones) {
@@ -22464,6 +22718,7 @@ function warnOnChannelAdminBindingDrift() {
22464
22718
  } catch {
22465
22719
  return;
22466
22720
  }
22721
+ const validAccountIds = accounts.map((a) => a.accountId);
22467
22722
  for (const acct of accounts) {
22468
22723
  const adminPhones = readAdminPhones(acct.accountDir);
22469
22724
  const drift = findChannelAdminBindingDrift(adminPhones, acct.config.admins ?? [], users);
@@ -22473,6 +22728,12 @@ function warnOnChannelAdminBindingDrift() {
22473
22728
  `[admin-identity] binding-drift accountId=${acct.accountId} phone=${d.phone} reason=${d.reason}${tail}`
22474
22729
  );
22475
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
+ }
22476
22737
  }
22477
22738
  }
22478
22739
 
@@ -22837,8 +23098,14 @@ var waGateway = new WaGateway({
22837
23098
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
22838
23099
  },
22839
23100
  ensureChannelSession: async ({ accountId, senderId, role, personId, gatewayUrl, serverPath }) => {
22840
- const req = buildWaSpawnRequest({ accountId, senderId, role, personId, gatewayUrl, serverPath });
22841
- 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;
22842
23109
  if (role === "admin") {
22843
23110
  console.error(`[whatsapp-native] op=admin-identity senderId=${senderId} userId=${userId ?? "owner-fallback"}`);
22844
23111
  }
@@ -22984,6 +23251,22 @@ var chatRoutes = createChatRoutes({
22984
23251
  handleInbound: (input) => webchatGateway.handleInbound(input),
22985
23252
  awaitReply: (key, timeoutMs) => webchatGateway.awaitReply(key, timeoutMs)
22986
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);
22987
23270
  app55.use("*", clientIpMiddleware);
22988
23271
  function allowsSameOriginFraming(path2) {
22989
23272
  return path2 === "/vnc-viewer.html" || path2.startsWith("/api/admin/attachment/") || path2.startsWith("/api/public-reader/attachment/") || path2 === "/api/admin/files/download";