@rubytech/create-maxy-code 0.1.533 → 0.1.535

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 (234) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
  4. package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
  5. package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js +40 -1
  6. package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js.map +1 -1
  7. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
  8. package/payload/platform/plugins/email/mcp/dist/lib/compose.js +4 -1
  9. package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
  10. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +27 -0
  11. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
  12. package/payload/platform/plugins/email/mcp/dist/lib/signature.js +62 -2
  13. package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
  14. package/payload/platform/plugins/memory/skills/preference-audit/SKILL.md +11 -1
  15. package/payload/platform/plugins/whatsapp/PLUGIN.md +10 -2
  16. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +41 -3
  17. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +17 -1
  19. package/payload/platform/scripts/__tests__/vnc-cdp-liveness.test.sh +14 -12
  20. package/payload/platform/scripts/vnc.sh +24 -19
  21. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  23. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  25. package/payload/platform/services/claude-session-manager/dist/http-server.js +35 -1
  26. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/index.js +13 -4
  28. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -1
  31. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js +46 -3
  32. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +9 -0
  34. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -0
  36. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts +12 -0
  38. package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts.map +1 -0
  39. package/payload/platform/services/claude-session-manager/dist/specialist-roster.js +75 -0
  40. package/payload/platform/services/claude-session-manager/dist/specialist-roster.js.map +1 -0
  41. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +5 -0
  42. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +10 -2
  44. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  45. package/payload/server/{chunk-AJC3SQSF.js → chunk-4QCYFTAI.js} +198 -8
  46. package/payload/server/{manager-PLRHVALC.js → manager-ZNHQQCED.js} +1 -1
  47. package/payload/server/public/activity.html +5 -5
  48. package/payload/server/public/agents.html +4 -4
  49. package/payload/server/public/assets/AdminLoginScreens-DxjuTFjL.js +1 -0
  50. package/payload/server/public/assets/AdminLoginScreens-DxjuTFjL.js.br +0 -0
  51. package/payload/server/public/assets/AdminLoginScreens-DxjuTFjL.js.gz +0 -0
  52. package/payload/server/public/assets/AdminShell-B6H3J5C2.js +2 -0
  53. package/payload/server/public/assets/AdminShell-B6H3J5C2.js.br +0 -0
  54. package/payload/server/public/assets/AdminShell-B6H3J5C2.js.gz +0 -0
  55. package/payload/server/public/assets/CdpScreencastSurface-BRSv89v-.js +1 -0
  56. package/payload/server/public/assets/CdpScreencastSurface-BRSv89v-.js.br +0 -0
  57. package/payload/server/public/assets/CdpScreencastSurface-BRSv89v-.js.gz +0 -0
  58. package/payload/server/public/assets/{activity-0IcMRF5a.js → activity-DKeRUjQ9.js} +1 -1
  59. package/payload/server/public/assets/activity-DKeRUjQ9.js.br +0 -0
  60. package/payload/server/public/assets/activity-DKeRUjQ9.js.gz +0 -0
  61. package/payload/server/public/assets/admin-BxRWjeyZ.js +1 -0
  62. package/payload/server/public/assets/admin-BxRWjeyZ.js.br +0 -0
  63. package/payload/server/public/assets/admin-BxRWjeyZ.js.gz +0 -0
  64. package/payload/server/public/assets/{agents-xjw-fJjo.js → agents-CrR_OMe8.js} +1 -1
  65. package/payload/server/public/assets/agents-CrR_OMe8.js.br +0 -0
  66. package/payload/server/public/assets/agents-CrR_OMe8.js.gz +0 -0
  67. package/payload/server/public/assets/{browser-ByWKTnUQ.js → browser-sfwnP1kH.js} +1 -1
  68. package/payload/server/public/assets/browser-sfwnP1kH.js.br +0 -0
  69. package/payload/server/public/assets/browser-sfwnP1kH.js.gz +0 -0
  70. package/payload/server/public/assets/{calendar-DX2tE1B2.js → calendar-ByLH0Q5p.js} +1 -1
  71. package/payload/server/public/assets/calendar-ByLH0Q5p.js.br +0 -0
  72. package/payload/server/public/assets/calendar-ByLH0Q5p.js.gz +0 -0
  73. package/payload/server/public/assets/chat-C3xYZlGo.js +1 -0
  74. package/payload/server/public/assets/chat-C3xYZlGo.js.br +2 -0
  75. package/payload/server/public/assets/chat-C3xYZlGo.js.gz +0 -0
  76. package/payload/server/public/assets/chevron-left-C0HG8MBj.js +1 -0
  77. package/payload/server/public/assets/chevron-right-B49Xpptm.js +1 -0
  78. package/payload/server/public/assets/chevron-right-B49Xpptm.js.br +0 -0
  79. package/payload/server/public/assets/clock-CJq9vvt8.js +1 -0
  80. package/payload/server/public/assets/clock-CJq9vvt8.js.br +0 -0
  81. package/payload/server/public/assets/clock-CJq9vvt8.js.gz +0 -0
  82. package/payload/server/public/assets/data-DmeTKd-P.js +1 -0
  83. package/payload/server/public/assets/data-DmeTKd-P.js.br +0 -0
  84. package/payload/server/public/assets/data-DmeTKd-P.js.gz +0 -0
  85. package/payload/server/public/assets/{file-download-ChX5qAB2.js → file-download-BWe9Muoh.js} +1 -1
  86. package/payload/server/public/assets/file-download-BWe9Muoh.js.br +0 -0
  87. package/payload/server/public/assets/file-download-BWe9Muoh.js.gz +0 -0
  88. package/payload/server/public/assets/{file-text-DziWy8hF.js → file-text-5osg-EVb.js} +1 -1
  89. package/payload/server/public/assets/file-text-5osg-EVb.js.br +0 -0
  90. package/payload/server/public/assets/file-text-5osg-EVb.js.gz +0 -0
  91. package/payload/server/public/assets/{graph-Ci8RmSp3.js → graph-CAUo1OTY.js} +2 -2
  92. package/payload/server/public/assets/graph-CAUo1OTY.js.br +0 -0
  93. package/payload/server/public/assets/graph-CAUo1OTY.js.gz +0 -0
  94. package/payload/server/public/assets/{graph-labels-JkRVzaVL.js → graph-labels-TJ-3ljQo.js} +1 -1
  95. package/payload/server/public/assets/graph-labels-TJ-3ljQo.js.br +0 -0
  96. package/payload/server/public/assets/graph-labels-TJ-3ljQo.js.gz +0 -0
  97. package/payload/server/public/assets/{maximize-2-DnINkTp0.js → maximize-2-S9W3q1O4.js} +1 -1
  98. package/payload/server/public/assets/maximize-2-S9W3q1O4.js.br +0 -0
  99. package/payload/server/public/assets/maximize-2-S9W3q1O4.js.gz +0 -0
  100. package/payload/server/public/assets/operator-CzOJfank.js +1 -0
  101. package/payload/server/public/assets/operator-CzOJfank.js.br +0 -0
  102. package/payload/server/public/assets/operator-CzOJfank.js.gz +0 -0
  103. package/payload/server/public/assets/page-BUFW-PWF.js +32 -0
  104. package/payload/server/public/assets/page-BUFW-PWF.js.br +0 -0
  105. package/payload/server/public/assets/page-BUFW-PWF.js.gz +0 -0
  106. package/payload/server/public/assets/page-jT4queFM.js +1 -0
  107. package/payload/server/public/assets/page-jT4queFM.js.br +0 -0
  108. package/payload/server/public/assets/page-jT4queFM.js.gz +0 -0
  109. package/payload/server/public/assets/public-Cx2ddU9a.js +1 -0
  110. package/payload/server/public/assets/public-Cx2ddU9a.js.br +0 -0
  111. package/payload/server/public/assets/public-Cx2ddU9a.js.gz +0 -0
  112. package/payload/server/public/assets/{rotate-ccw-CfaIgRAI.js → rotate-ccw-C2qSSBFA.js} +1 -1
  113. package/payload/server/public/assets/rotate-ccw-C2qSSBFA.js.br +0 -0
  114. package/payload/server/public/assets/rotate-ccw-C2qSSBFA.js.gz +0 -0
  115. package/payload/server/public/assets/{routines-BIlcD8sn.js → routines-80uEwjng.js} +1 -1
  116. package/payload/server/public/assets/routines-80uEwjng.js.br +0 -0
  117. package/payload/server/public/assets/routines-80uEwjng.js.gz +0 -0
  118. package/payload/server/public/assets/{skills-DGID2WqF.js → skills-CmsYXVd3.js} +1 -1
  119. package/payload/server/public/assets/skills-CmsYXVd3.js.br +0 -0
  120. package/payload/server/public/assets/skills-CmsYXVd3.js.gz +0 -0
  121. package/payload/server/public/assets/tasks-CFfvn-KE.js +3 -0
  122. package/payload/server/public/assets/tasks-CFfvn-KE.js.br +0 -0
  123. package/payload/server/public/assets/tasks-CFfvn-KE.js.gz +0 -0
  124. package/payload/server/public/assets/{time-entry-format-DNl35lg9.js → time-entry-format-BD99oR05.js} +1 -1
  125. package/payload/server/public/assets/time-entry-format-BD99oR05.js.br +0 -0
  126. package/payload/server/public/assets/time-entry-format-BD99oR05.js.gz +0 -0
  127. package/payload/server/public/assets/{triangle-alert-DI_VASJO.js → triangle-alert-CuRAK3jk.js} +1 -1
  128. package/payload/server/public/assets/triangle-alert-CuRAK3jk.js.br +0 -0
  129. package/payload/server/public/assets/triangle-alert-CuRAK3jk.js.gz +0 -0
  130. package/payload/server/public/assets/{useCopyFeedback-Bu0ZQZQM.js → useCopyFeedback-BGzzIsSD.js} +1 -1
  131. package/payload/server/public/assets/useCopyFeedback-BGzzIsSD.js.br +0 -0
  132. package/payload/server/public/assets/useCopyFeedback-BGzzIsSD.js.gz +0 -0
  133. package/payload/server/public/assets/{useSubAccountSwitcher-CZBZ3iV8.css → useSubAccountSwitcher-BDQ8Wsdt.css} +1 -1
  134. package/payload/server/public/assets/useSubAccountSwitcher-BDQ8Wsdt.css.br +0 -0
  135. package/payload/server/public/assets/{useSubAccountSwitcher-CZBZ3iV8.css.gz → useSubAccountSwitcher-BDQ8Wsdt.css.gz} +0 -0
  136. package/payload/server/public/assets/useSubAccountSwitcher-D1VtIEp2.js +14 -0
  137. package/payload/server/public/assets/useSubAccountSwitcher-D1VtIEp2.js.br +0 -0
  138. package/payload/server/public/assets/useSubAccountSwitcher-D1VtIEp2.js.gz +0 -0
  139. package/payload/server/public/assets/useVoiceRecorder-fpwWw2VW.js +2 -0
  140. package/payload/server/public/assets/useVoiceRecorder-fpwWw2VW.js.br +0 -0
  141. package/payload/server/public/assets/useVoiceRecorder-fpwWw2VW.js.gz +0 -0
  142. package/payload/server/public/assets/{wrench-p4gW_ImW.js → wrench-DxSsCrue.js} +1 -1
  143. package/payload/server/public/assets/wrench-DxSsCrue.js.br +2 -0
  144. package/payload/server/public/assets/wrench-DxSsCrue.js.gz +0 -0
  145. package/payload/server/public/browser.html +5 -5
  146. package/payload/server/public/calendar.html +7 -7
  147. package/payload/server/public/chat.html +13 -13
  148. package/payload/server/public/data.html +12 -12
  149. package/payload/server/public/graph.html +9 -9
  150. package/payload/server/public/index.html +16 -16
  151. package/payload/server/public/operator.html +15 -15
  152. package/payload/server/public/public.html +13 -13
  153. package/payload/server/public/routines.html +7 -7
  154. package/payload/server/public/skills.html +5 -5
  155. package/payload/server/public/tasks.html +6 -6
  156. package/payload/server/server.js +460 -28
  157. package/payload/server/public/assets/AdminLoginScreens-CGRty7xT.js +0 -1
  158. package/payload/server/public/assets/AdminLoginScreens-CGRty7xT.js.br +0 -0
  159. package/payload/server/public/assets/AdminLoginScreens-CGRty7xT.js.gz +0 -0
  160. package/payload/server/public/assets/AdminShell-BQy9Ob22.js +0 -2
  161. package/payload/server/public/assets/AdminShell-BQy9Ob22.js.br +0 -0
  162. package/payload/server/public/assets/AdminShell-BQy9Ob22.js.gz +0 -0
  163. package/payload/server/public/assets/CdpScreencastSurface-BrDZji8F.js +0 -1
  164. package/payload/server/public/assets/CdpScreencastSurface-BrDZji8F.js.br +0 -0
  165. package/payload/server/public/assets/CdpScreencastSurface-BrDZji8F.js.gz +0 -0
  166. package/payload/server/public/assets/activity-0IcMRF5a.js.br +0 -0
  167. package/payload/server/public/assets/activity-0IcMRF5a.js.gz +0 -0
  168. package/payload/server/public/assets/admin-CKy-bmrs.js +0 -1
  169. package/payload/server/public/assets/admin-CKy-bmrs.js.br +0 -0
  170. package/payload/server/public/assets/admin-CKy-bmrs.js.gz +0 -0
  171. package/payload/server/public/assets/agents-xjw-fJjo.js.br +0 -0
  172. package/payload/server/public/assets/agents-xjw-fJjo.js.gz +0 -0
  173. package/payload/server/public/assets/browser-ByWKTnUQ.js.br +0 -0
  174. package/payload/server/public/assets/browser-ByWKTnUQ.js.gz +0 -0
  175. package/payload/server/public/assets/calendar-DX2tE1B2.js.br +0 -0
  176. package/payload/server/public/assets/calendar-DX2tE1B2.js.gz +0 -0
  177. package/payload/server/public/assets/chat-DkNDthRg.js +0 -1
  178. package/payload/server/public/assets/chat-DkNDthRg.js.br +0 -0
  179. package/payload/server/public/assets/chat-DkNDthRg.js.gz +0 -0
  180. package/payload/server/public/assets/chevron-left-CPwk14nY.js +0 -1
  181. package/payload/server/public/assets/chevron-right-Bz6INVTF.js +0 -1
  182. package/payload/server/public/assets/chevron-right-Bz6INVTF.js.br +0 -0
  183. package/payload/server/public/assets/clock-3YLNejcB.js +0 -1
  184. package/payload/server/public/assets/clock-3YLNejcB.js.br +0 -0
  185. package/payload/server/public/assets/clock-3YLNejcB.js.gz +0 -0
  186. package/payload/server/public/assets/data-DiDeaOUN.js +0 -1
  187. package/payload/server/public/assets/data-DiDeaOUN.js.br +0 -0
  188. package/payload/server/public/assets/data-DiDeaOUN.js.gz +0 -0
  189. package/payload/server/public/assets/file-download-ChX5qAB2.js.br +0 -0
  190. package/payload/server/public/assets/file-download-ChX5qAB2.js.gz +0 -0
  191. package/payload/server/public/assets/file-text-DziWy8hF.js.br +0 -0
  192. package/payload/server/public/assets/file-text-DziWy8hF.js.gz +0 -0
  193. package/payload/server/public/assets/graph-Ci8RmSp3.js.br +0 -0
  194. package/payload/server/public/assets/graph-Ci8RmSp3.js.gz +0 -0
  195. package/payload/server/public/assets/graph-labels-JkRVzaVL.js.br +0 -0
  196. package/payload/server/public/assets/graph-labels-JkRVzaVL.js.gz +0 -0
  197. package/payload/server/public/assets/maximize-2-DnINkTp0.js.br +0 -0
  198. package/payload/server/public/assets/maximize-2-DnINkTp0.js.gz +0 -0
  199. package/payload/server/public/assets/operator-CFOJBXCH.js +0 -1
  200. package/payload/server/public/assets/operator-CFOJBXCH.js.br +0 -0
  201. package/payload/server/public/assets/operator-CFOJBXCH.js.gz +0 -0
  202. package/payload/server/public/assets/page-BGv8bLm5.js +0 -32
  203. package/payload/server/public/assets/page-BGv8bLm5.js.br +0 -0
  204. package/payload/server/public/assets/page-BGv8bLm5.js.gz +0 -0
  205. package/payload/server/public/assets/page-DXTCBZRa.js +0 -1
  206. package/payload/server/public/assets/page-DXTCBZRa.js.br +0 -0
  207. package/payload/server/public/assets/page-DXTCBZRa.js.gz +0 -0
  208. package/payload/server/public/assets/public-yBY2wTJN.js +0 -1
  209. package/payload/server/public/assets/public-yBY2wTJN.js.br +0 -0
  210. package/payload/server/public/assets/public-yBY2wTJN.js.gz +0 -0
  211. package/payload/server/public/assets/rotate-ccw-CfaIgRAI.js.br +0 -0
  212. package/payload/server/public/assets/rotate-ccw-CfaIgRAI.js.gz +0 -0
  213. package/payload/server/public/assets/routines-BIlcD8sn.js.br +0 -0
  214. package/payload/server/public/assets/routines-BIlcD8sn.js.gz +0 -0
  215. package/payload/server/public/assets/skills-DGID2WqF.js.br +0 -0
  216. package/payload/server/public/assets/skills-DGID2WqF.js.gz +0 -0
  217. package/payload/server/public/assets/tasks-BukjwR7A.js +0 -3
  218. package/payload/server/public/assets/tasks-BukjwR7A.js.br +0 -0
  219. package/payload/server/public/assets/tasks-BukjwR7A.js.gz +0 -0
  220. package/payload/server/public/assets/time-entry-format-DNl35lg9.js.br +0 -0
  221. package/payload/server/public/assets/time-entry-format-DNl35lg9.js.gz +0 -0
  222. package/payload/server/public/assets/triangle-alert-DI_VASJO.js.br +0 -0
  223. package/payload/server/public/assets/triangle-alert-DI_VASJO.js.gz +0 -0
  224. package/payload/server/public/assets/useCopyFeedback-Bu0ZQZQM.js.br +0 -0
  225. package/payload/server/public/assets/useCopyFeedback-Bu0ZQZQM.js.gz +0 -0
  226. package/payload/server/public/assets/useSubAccountSwitcher-B6Ns2Z43.js +0 -14
  227. package/payload/server/public/assets/useSubAccountSwitcher-B6Ns2Z43.js.br +0 -0
  228. package/payload/server/public/assets/useSubAccountSwitcher-B6Ns2Z43.js.gz +0 -0
  229. package/payload/server/public/assets/useSubAccountSwitcher-CZBZ3iV8.css.br +0 -0
  230. package/payload/server/public/assets/useVoiceRecorder-CbVwXEjY.js +0 -2
  231. package/payload/server/public/assets/useVoiceRecorder-CbVwXEjY.js.br +0 -0
  232. package/payload/server/public/assets/useVoiceRecorder-CbVwXEjY.js.gz +0 -0
  233. package/payload/server/public/assets/wrench-p4gW_ImW.js.br +0 -0
  234. package/payload/server/public/assets/wrench-p4gW_ImW.js.gz +0 -0
@@ -67,6 +67,7 @@ import {
67
67
  MAX_DATA_UPLOAD_BYTES,
68
68
  MAX_FILES_PER_MESSAGE,
69
69
  MAX_FILE_SIZE_BYTES,
70
+ PARTY_LABEL_PATTERN,
70
71
  STORE_ROOT,
71
72
  SUPPORTED_MIME_TYPES,
72
73
  WHATSAPP_DOCUMENT_MAX_BYTES,
@@ -156,6 +157,7 @@ import {
156
157
  recordAccessFailedAttempt,
157
158
  recordRequestLinkAttempt,
158
159
  registerLoginSocket,
160
+ registeredPartyFor,
159
161
  reloadConfig,
160
162
  resolveAccountConfig,
161
163
  resolveClaudeUploadsPath,
@@ -190,7 +192,7 @@ import {
190
192
  waitForConnection,
191
193
  warnNonUuidAccount,
192
194
  writeChatMetadataMerged
193
- } from "./chunk-AJC3SQSF.js";
195
+ } from "./chunk-4QCYFTAI.js";
194
196
  import {
195
197
  ACCOUNTS_DIR,
196
198
  BIN_DIR,
@@ -5019,7 +5021,7 @@ async function defaultHttpGet(url) {
5019
5021
  }
5020
5022
  }
5021
5023
  async function defaultResolve(accountId) {
5022
- const { resolveSocket: resolveSocket2 } = await import("./manager-PLRHVALC.js");
5024
+ const { resolveSocket: resolveSocket2 } = await import("./manager-ZNHQQCED.js");
5023
5025
  return resolveSocket2(accountId);
5024
5026
  }
5025
5027
  async function fetchAvatar(accountId, jid, deps = {}) {
@@ -5404,6 +5406,10 @@ function addAdminPhone(accountDir, phone) {
5404
5406
  error: `${targetId} does not own the WhatsApp paired socket, so its admin phones are never consulted. To route a phone to a sub-account, bind it as an account manager (add-account-manager) instead.`
5405
5407
  };
5406
5408
  }
5409
+ if (registeredPartyFor(readRegisteredPartiesFrom(config), normalized)) {
5410
+ console.error(`${TAG10} add-admin-phone rejected phone=${normalized} reason=party-registration-shadowed`);
5411
+ return { ok: false, reason: "party-registration-shadowed", error: partyShadowMessage(normalized, "registeredParties") };
5412
+ }
5407
5413
  if (!config.whatsapp || typeof config.whatsapp !== "object") {
5408
5414
  config.whatsapp = {};
5409
5415
  }
@@ -5605,6 +5611,10 @@ function setAccountManager(accountDir, phone, subAccountId, mode = "active") {
5605
5611
  error: `The account-manager routing table is read from ${houseId}'s account.json only, so a binding stored on ${targetId} is never consulted. Bind ${normalized} on ${houseId} instead.`
5606
5612
  };
5607
5613
  }
5614
+ if (registeredPartyFor(readRegisteredPartiesFrom(config), normalized)) {
5615
+ console.error(`${TAG10} set-account-manager rejected phone=${normalized} reason=party-registration-shadowed`);
5616
+ return { ok: false, reason: "party-registration-shadowed", error: partyShadowMessage(normalized, "registeredParties") };
5617
+ }
5608
5618
  if (!config.whatsapp || typeof config.whatsapp !== "object") {
5609
5619
  config.whatsapp = {};
5610
5620
  }
@@ -5633,6 +5643,200 @@ function setAccountManager(accountDir, phone, subAccountId, mode = "active") {
5633
5643
  return { ok: false, error: msg };
5634
5644
  }
5635
5645
  }
5646
+ function readRegisteredPartiesFrom(config) {
5647
+ const wa = config.whatsapp;
5648
+ const map = wa?.registeredParties;
5649
+ if (!map || typeof map !== "object" || Array.isArray(map)) return {};
5650
+ const out = {};
5651
+ for (const [k, v] of Object.entries(map)) {
5652
+ if (v && typeof v === "object" && typeof v.account === "string" && typeof v.party === "string") {
5653
+ out[k] = { account: v.account, party: v.party };
5654
+ }
5655
+ }
5656
+ return out;
5657
+ }
5658
+ function partyShadowedBy(wa, phone) {
5659
+ const adminPhones = Array.isArray(wa.adminPhones) ? wa.adminPhones.filter((p) => typeof p === "string") : [];
5660
+ if (isAdminPhone(phone, adminPhones)) return "adminPhones";
5661
+ const managers = wa.accountManagers && typeof wa.accountManagers === "object" && !Array.isArray(wa.accountManagers) ? wa.accountManagers : {};
5662
+ if (managedAccountFor(managers, phone)) return "accountManagers";
5663
+ return null;
5664
+ }
5665
+ function partyShadowMessage(phone, shadowedBy) {
5666
+ if (shadowedBy === "registeredParties") {
5667
+ return `${phone} is already a registered party. Arms 1-4 of the inbound gate all precede the registered-party arm, so adding this registration would silently disable the party binding rather than layering on it. Remove the party registration first (remove-registered-party) if that is what you intend.`;
5668
+ }
5669
+ const surface = shadowedBy === "adminPhones" ? "an admin phone" : "an account manager";
5670
+ const remedy = shadowedBy === "adminPhones" ? "remove-admin-phone" : "remove-account-manager";
5671
+ return `${phone} is already ${surface}. Arms 1-4 of the inbound gate all precede the registered-party arm, so this registration would never be reached. Remove the existing registration first (${remedy}) if you want this phone treated as a counterparty rather than an operator.`;
5672
+ }
5673
+ function setRegisteredParty(accountDir, phone, account, party) {
5674
+ const normalized = phone.trim();
5675
+ if (!E164_PATTERN.test(normalized)) {
5676
+ return { ok: false, error: `Invalid phone format "${normalized}". Expected E.164 (e.g. +441234567890).` };
5677
+ }
5678
+ const label = party.trim();
5679
+ if (!PARTY_LABEL_PATTERN.test(label)) {
5680
+ return {
5681
+ ok: false,
5682
+ reason: "invalid-party-label",
5683
+ error: `Invalid party label "${label}". Use 1-64 characters of lowercase letters, digits and hyphens (e.g. "driver").`
5684
+ };
5685
+ }
5686
+ const acct = account.trim();
5687
+ if (!acct) return { ok: false, error: "Missing account id." };
5688
+ let validIds;
5689
+ try {
5690
+ validIds = listValidAccounts().map((a) => a.accountId);
5691
+ } catch (err) {
5692
+ return { ok: false, error: `Could not read the account registry to validate the account: ${err instanceof Error ? err.message : String(err)}. Try again.` };
5693
+ }
5694
+ if (!validIds.includes(acct)) {
5695
+ return { ok: false, error: `Account "${acct}" is not in this install's account registry. Check the id and try again.` };
5696
+ }
5697
+ let houseId;
5698
+ try {
5699
+ houseId = resolveHouseOrSoleAccountId(dirname2(resolve5(accountDir)));
5700
+ } catch (err) {
5701
+ return { ok: false, error: `Could not resolve the house account to validate the registration: ${err instanceof Error ? err.message : String(err)}. Try again.` };
5702
+ }
5703
+ if (acct === houseId) {
5704
+ console.error(`${TAG10} set-registered-party rejected phone=${normalized} reason=house-account-binding`);
5705
+ return {
5706
+ ok: false,
5707
+ reason: "house-account-binding",
5708
+ error: `${acct} is the house account. A registered party is admitted only on the account's OWN number, and on the house number they take the house DM policy instead, so a house registration would never be consulted.`
5709
+ };
5710
+ }
5711
+ try {
5712
+ const config = readConfig(accountDir);
5713
+ const targetId = typeof config.accountId === "string" && config.accountId ? config.accountId : basename(resolve5(accountDir));
5714
+ if (targetId !== houseId) {
5715
+ console.error(`${TAG10} set-registered-party rejected phone=${normalized} reason=not-socket-account`);
5716
+ return {
5717
+ ok: false,
5718
+ reason: "not-socket-account",
5719
+ error: `The registered-party table is read from ${houseId}'s account.json only, so a registration stored on ${targetId} is never consulted. Register ${normalized} on ${houseId} instead.`
5720
+ };
5721
+ }
5722
+ if (!config.whatsapp || typeof config.whatsapp !== "object") {
5723
+ config.whatsapp = {};
5724
+ }
5725
+ const wa = config.whatsapp;
5726
+ const shadow = partyShadowedBy(wa, normalized);
5727
+ if (shadow) {
5728
+ console.error(`${TAG10} set-registered-party rejected phone=${normalized} reason=party-registration-shadowed shadowedBy=${shadow}`);
5729
+ return { ok: false, reason: "party-registration-shadowed", error: partyShadowMessage(normalized, shadow) };
5730
+ }
5731
+ if (!wa.registeredParties || typeof wa.registeredParties !== "object") {
5732
+ wa.registeredParties = {};
5733
+ }
5734
+ wa.registeredParties[normalized] = { account: acct, party: label };
5735
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
5736
+ if (!parsed.success) {
5737
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
5738
+ return { ok: false, error: `Validation failed: ${msg}` };
5739
+ }
5740
+ config.whatsapp = parsed.data;
5741
+ writeConfig(accountDir, config);
5742
+ console.error(`${TAG10} registered party phone=${normalized} account=${acct} party=${label}`);
5743
+ reloadManagerConfig(accountDir);
5744
+ return {
5745
+ ok: true,
5746
+ message: `Registered ${normalized} as "${label}" for account ${acct}. Their WhatsApp messages to that account's own number reach its public agent carrying the label; they are not granted admin, and on the house number they take the house DM policy.`
5747
+ };
5748
+ } catch (err) {
5749
+ const msg = err instanceof Error ? err.message : String(err);
5750
+ console.error(`${TAG10} setRegisteredParty failed: ${msg}`);
5751
+ return { ok: false, error: msg };
5752
+ }
5753
+ }
5754
+ function clearRegisteredParty(accountDir, phone) {
5755
+ const normalized = phone.trim();
5756
+ try {
5757
+ const config = readConfig(accountDir);
5758
+ const wa = config.whatsapp;
5759
+ if (!wa || typeof wa !== "object" || !wa.registeredParties || typeof wa.registeredParties !== "object") {
5760
+ return { ok: true, message: `No registered-party registration for ${normalized}.` };
5761
+ }
5762
+ const map = wa.registeredParties;
5763
+ const key = Object.keys(map).find((k) => phonesMatch(k, normalized));
5764
+ if (!key) {
5765
+ return { ok: true, message: `No registered-party registration for ${normalized}.` };
5766
+ }
5767
+ delete map[key];
5768
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
5769
+ if (!parsed.success) {
5770
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
5771
+ return { ok: false, error: `Validation failed: ${msg}` };
5772
+ }
5773
+ config.whatsapp = parsed.data;
5774
+ writeConfig(accountDir, config);
5775
+ console.error(`${TAG10} cleared registered party phone=${normalized}`);
5776
+ reloadManagerConfig(accountDir);
5777
+ return { ok: true, message: `Cleared the registered-party registration for ${normalized}.` };
5778
+ } catch (err) {
5779
+ const msg = err instanceof Error ? err.message : String(err);
5780
+ console.error(`${TAG10} clearRegisteredParty failed: ${msg}`);
5781
+ return { ok: false, error: msg };
5782
+ }
5783
+ }
5784
+ function readRegisteredParties(accountDir) {
5785
+ try {
5786
+ return readRegisteredPartiesFrom(readConfig(accountDir));
5787
+ } catch {
5788
+ return {};
5789
+ }
5790
+ }
5791
+ function purgeNonSocketRegisteredParties(accountDir, socketOwnerId) {
5792
+ try {
5793
+ const config = readConfig(accountDir);
5794
+ const wa = config.whatsapp;
5795
+ if (!wa || typeof wa !== "object") return { removed: [] };
5796
+ const targetId = typeof config.accountId === "string" && config.accountId ? config.accountId : basename(resolve5(accountDir));
5797
+ if (targetId === socketOwnerId) return { removed: [] };
5798
+ const map = wa.registeredParties;
5799
+ if (!map || typeof map !== "object" || Array.isArray(map)) return { removed: [] };
5800
+ const removed = Object.entries(map).map(([phone, entry]) => ({
5801
+ phone,
5802
+ account: entry && typeof entry === "object" && typeof entry.account === "string" ? entry.account : "",
5803
+ party: entry && typeof entry === "object" && typeof entry.party === "string" ? entry.party : ""
5804
+ }));
5805
+ if (removed.length === 0) return { removed: [] };
5806
+ delete wa.registeredParties;
5807
+ writeConfig(accountDir, config);
5808
+ for (const { phone, account, party } of removed) {
5809
+ console.error(`[admin-identity] op=purge-nonsocket-registeredparty accountId=${targetId} phone=${phone} account=${account} party=${party} removed=true`);
5810
+ }
5811
+ return { removed };
5812
+ } catch (err) {
5813
+ console.error(`[admin-identity] purge-nonsocket-registeredparty failed dir=${accountDir}: ${String(err)}`);
5814
+ return { removed: [] };
5815
+ }
5816
+ }
5817
+ function purgeNonSocketRegisteredPartiesAtBoot() {
5818
+ let socketOwner;
5819
+ try {
5820
+ socketOwner = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
5821
+ } catch (err) {
5822
+ console.error(`[admin-identity] purge-nonsocket-registeredparty skipped \u2014 cannot resolve socket owner: ${String(err)}`);
5823
+ return;
5824
+ }
5825
+ const configOwner = resolveAccount()?.accountId ?? null;
5826
+ if (configOwner !== socketOwner) {
5827
+ console.error(`[admin-identity] purge-nonsocket-registeredparty skipped \u2014 resolver disagreement house=${socketOwner} configLoadedFrom=${configOwner ?? "unresolved"}`);
5828
+ return;
5829
+ }
5830
+ let accounts;
5831
+ try {
5832
+ accounts = listValidAccounts();
5833
+ } catch {
5834
+ return;
5835
+ }
5836
+ for (const acct of accounts) {
5837
+ purgeNonSocketRegisteredParties(acct.accountDir, socketOwner);
5838
+ }
5839
+ }
5636
5840
  function setDispatchInbound(accountDir, accountId, value, ctx) {
5637
5841
  const id = accountId.trim();
5638
5842
  if (!id) return { ok: false, reason: "missing-account-id", error: "Missing account id." };
@@ -6299,6 +6503,60 @@ function reconcileConnectionAccountStamps(input) {
6299
6503
  return { ok: mismatches.length === 0, mismatches };
6300
6504
  }
6301
6505
 
6506
+ // app/lib/whatsapp/person-identity-reconcile.ts
6507
+ function bare(value) {
6508
+ return value.startsWith("+") ? value.slice(1) : value;
6509
+ }
6510
+ function reconcilePersonIdentities(rows) {
6511
+ const perAccount = /* @__PURE__ */ new Map();
6512
+ for (const row of rows) {
6513
+ let entry = perAccount.get(row.accountId);
6514
+ if (!entry) {
6515
+ entry = { seen: /* @__PURE__ */ new Map(), divergent: 0 };
6516
+ perAccount.set(row.accountId, entry);
6517
+ }
6518
+ const p = row.phone ? bare(row.phone) : null;
6519
+ const t = row.telephone ? bare(row.telephone) : null;
6520
+ if (p && t && p !== t) entry.divergent += 1;
6521
+ const key = p ?? t;
6522
+ if (key) entry.seen.set(key, (entry.seen.get(key) ?? 0) + 1);
6523
+ }
6524
+ const accounts = [];
6525
+ let duplicates = 0;
6526
+ let divergent = 0;
6527
+ for (const [accountId, entry] of perAccount) {
6528
+ let dup = 0;
6529
+ for (const count of entry.seen.values()) if (count > 1) dup += count - 1;
6530
+ duplicates += dup;
6531
+ divergent += entry.divergent;
6532
+ if (dup > 0 || entry.divergent > 0) {
6533
+ accounts.push({ accountId, duplicates: dup, divergent: entry.divergent });
6534
+ }
6535
+ }
6536
+ return {
6537
+ ok: duplicates === 0 && divergent === 0,
6538
+ checked: rows.length,
6539
+ duplicates,
6540
+ divergent,
6541
+ accounts
6542
+ };
6543
+ }
6544
+
6545
+ // app/lib/whatsapp/notify-manager-recipients.ts
6546
+ function recipientsForAccount(managers, accountId) {
6547
+ const out = [];
6548
+ for (const [phone, value] of Object.entries(managers ?? {})) {
6549
+ let bound = null;
6550
+ if (typeof value === "string") {
6551
+ bound = value;
6552
+ } else if (value && typeof value === "object" && typeof value.managesAccount === "string") {
6553
+ bound = value.managesAccount;
6554
+ }
6555
+ if (bound !== null && bound === accountId) out.push(phone);
6556
+ }
6557
+ return out;
6558
+ }
6559
+
6302
6560
  // app/lib/whatsapp/self-phone-admin-reconcile.ts
6303
6561
  var HOUSE_CREDENTIAL_DIRNAME = "default";
6304
6562
  function reconcileSelfPhoneAdmins(input) {
@@ -6712,6 +6970,22 @@ function authorizeRecallRead(input) {
6712
6970
  };
6713
6971
  }
6714
6972
 
6973
+ // app/lib/whatsapp/registered-party-reconcile.ts
6974
+ function buildRegisteredPartyReconcile(params) {
6975
+ const rows = [];
6976
+ const entries2 = Object.entries(params.registeredParties);
6977
+ for (const [phone, entry] of entries2) {
6978
+ const shadowedBy = isAdminPhone(phone, params.adminPhones) ? "adminPhones" : managedAccountFor(params.accountManagers, phone) ? "accountManagers" : null;
6979
+ const unresolved2 = !params.isValidAccount(entry.account);
6980
+ if (shadowedBy || unresolved2) {
6981
+ rows.push({ phone, account: entry.account, party: entry.party, shadowedBy, unresolved: unresolved2 });
6982
+ }
6983
+ }
6984
+ const shadowed = rows.filter((r) => r.shadowedBy !== null).length;
6985
+ const unresolved = rows.filter((r) => r.unresolved).length;
6986
+ return { ok: shadowed === 0 && unresolved === 0, checked: entries2.length, shadowed, unresolved, rows };
6987
+ }
6988
+
6715
6989
  // server/routes/whatsapp.ts
6716
6990
  var TAG15 = "[whatsapp:api]";
6717
6991
  var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT || "";
@@ -6748,10 +7022,11 @@ function evaluateCallerScope(c, requestedAccountId) {
6748
7022
  const decision = authorizeRecallRead({ requestedAccountId, callerAccountId, houseAdminScope });
6749
7023
  return { decision, callerAccountId, houseAdminScope };
6750
7024
  }
6751
- function callerScopeDenial(c, requestedAccountId) {
7025
+ function callerScopeDenial(c, requestedAccountId, targetSource) {
6752
7026
  const { decision, callerAccountId, houseAdminScope } = evaluateCallerScope(c, requestedAccountId);
6753
7027
  if (decision.authorized) return { denial: null, callerAccountId };
6754
- console.error(`[whatsapp-read-tool] op=authz-deny requested=${requestedAccountId} caller=${callerAccountId ?? "none"} houseAdmin=${houseAdminScope ? "y" : "n"}`);
7028
+ const source = targetSource ? ` targetSource=${targetSource}` : "";
7029
+ console.error(`[whatsapp-read-tool] op=authz-deny requested=${requestedAccountId}${source} caller=${callerAccountId ?? "none"} houseAdmin=${houseAdminScope ? "y" : "n"}`);
6755
7030
  return { denial: c.json({ error: decision.reason }, 403), callerAccountId };
6756
7031
  }
6757
7032
  function houseSocketScopeDenial(c, op) {
@@ -6860,6 +7135,44 @@ app3.get("/status", async (c) => {
6860
7135
  if (!outboundSocketReconcile.ok) {
6861
7136
  console.warn(`${TAG15} outbound-caller-unsigned-drift unsigned=${outboundSocketReconcile.unsignedFallbacks} of=${outboundSocketReconcile.checked} \u2014 at least one agent send carried no caller identity and left on the house socket; if it came from a sub-account session the recipient saw the wrong sender number`);
6862
7137
  }
7138
+ const registeredPartyReconcile = buildRegisteredPartyReconcile({
7139
+ registeredParties: accountDir ? readRegisteredParties(accountDir) : {},
7140
+ adminPhones: accountDir ? readAdminPhones(accountDir) : [],
7141
+ accountManagers: accountDir ? readAccountManagers(accountDir) : {},
7142
+ isValidAccount: (id) => isProvisionedAccount(join15(MAXY_DIR, "data", "accounts"), id)
7143
+ });
7144
+ console.error(`${TAG15} op=registered-party-reconcile checked=${registeredPartyReconcile.checked} shadowed=${registeredPartyReconcile.shadowed} unresolved=${registeredPartyReconcile.unresolved} ok=${registeredPartyReconcile.ok}`);
7145
+ for (const row of registeredPartyReconcile.rows) {
7146
+ if (row.shadowedBy) {
7147
+ console.warn(`${TAG15} registered-party-shadowed phone=${row.phone} shadowedBy=${row.shadowedBy} \u2014 this counterparty is registered on this account but an earlier gate arm wins, so they reach the agent as an operator, not a party; remove the ${row.shadowedBy === "adminPhones" ? "admin phone" : "account-manager binding"} or the party registration`);
7148
+ }
7149
+ if (row.unresolved) {
7150
+ console.warn(`${TAG15} registered-party-unresolved phone=${row.phone} account=${row.account} \u2014 this registration names an account that is not in the registry, so every message from them on that number is hard-rejected`);
7151
+ }
7152
+ }
7153
+ let personIdentityReconcile = { ok: true, checked: 0, duplicates: 0, divergent: 0, accounts: [] };
7154
+ try {
7155
+ const identitySession = getSession();
7156
+ try {
7157
+ const identityRes = await identitySession.run(
7158
+ `MATCH (p:Person) WHERE p.accountId IS NOT NULL
7159
+ RETURN p.accountId AS accountId, p.phone AS phone, p.telephone AS telephone`
7160
+ );
7161
+ const identityRows = identityRes.records.map((r) => ({
7162
+ accountId: String(r.get("accountId")),
7163
+ phone: r.get("phone") ?? null,
7164
+ telephone: r.get("telephone") ?? null
7165
+ }));
7166
+ personIdentityReconcile = reconcilePersonIdentities(identityRows);
7167
+ } finally {
7168
+ await identitySession.close();
7169
+ }
7170
+ } catch (err) {
7171
+ personIdentityReconcile = { ok: false, checked: 0, duplicates: 0, divergent: 0, accounts: [], reason: String(err) };
7172
+ }
7173
+ if (!personIdentityReconcile.ok) {
7174
+ console.warn(`${TAG15} person-identity-drift duplicates=${personIdentityReconcile.duplicates} divergent=${personIdentityReconcile.divergent} of=${personIdentityReconcile.checked} \u2014 a WhatsApp sender may resolve to the wrong :Person or to none`);
7175
+ }
6863
7176
  let routineScopeReconcile = null;
6864
7177
  let neo = null;
6865
7178
  try {
@@ -6916,7 +7229,7 @@ app3.get("/status", async (c) => {
6916
7229
  const inheritanceSummary = policyInheritance.rows.map((r) => `${r.accountId}:${r.dmPolicy}/${r.adminPhones}/${r.allowFrom}`).join(", ");
6917
7230
  console.error(`${TAG15} op=policy-inheritance rows=${policyInheritance.rows.length} inheriting=${policyInheritance.inheritedCount} [${inheritanceSummary}]`);
6918
7231
  console.error(`${TAG15} op=status-projection caller=${callerAccountId?.slice(0, 8) ?? "none"} scope=house rows=${accounts.length}`);
6919
- return c.json({ accounts, storeKeyReconcile, connectionAccountReconcile, selfPhoneAdminReconcile, outboundSocketReconcile, routineScopeReconcile, bindingAccountDrift, policyInheritance });
7232
+ return c.json({ accounts, storeKeyReconcile, connectionAccountReconcile, selfPhoneAdminReconcile, outboundSocketReconcile, registeredPartyReconcile, routineScopeReconcile, bindingAccountDrift, policyInheritance, personIdentityReconcile });
6920
7233
  } catch (err) {
6921
7234
  console.error(`${TAG15} status error: ${String(err)}`);
6922
7235
  return c.json({ error: String(err) }, 500);
@@ -6999,7 +7312,7 @@ app3.post("/reconnect", async (c) => {
6999
7312
  app3.post("/config", async (c) => {
7000
7313
  try {
7001
7314
  const body = await c.req.json().catch(() => ({}));
7002
- const { action, phone, slug, fields, accountId, managesAccount, mode, policy } = body;
7315
+ const { action, phone, slug, fields, accountId, managesAccount, mode, policy, party, account: partyAccount } = body;
7003
7316
  if (!action || typeof action !== "string") {
7004
7317
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
7005
7318
  }
@@ -7083,6 +7396,46 @@ app3.post("/config", async (c) => {
7083
7396
  console.error(`${TAG15} config action=remove-account-manager phone=${phone} boundTo=${bound?.account ?? "none"} caller=${callerAccountId || "none"} ok=${result.ok}`);
7084
7397
  return c.json(result, result.ok ? 200 : 400);
7085
7398
  }
7399
+ if (action === "add-registered-party" || action === "remove-registered-party" || action === "list-registered-parties") {
7400
+ const acct = resolveAccount();
7401
+ if (!acct) return c.json({ ok: false, error: "No account configured." }, 500);
7402
+ const callerAccountId = c.req.header("x-maxy-caller-account")?.trim() || "";
7403
+ const houseAdminScope = c.req.header("x-maxy-house-admin-scope")?.trim() || null;
7404
+ const houseAdmin = !!houseAdminScope || !!callerAccountId && callerAccountId === acct.accountId;
7405
+ if (action === "list-registered-parties") {
7406
+ const listDenial = bindingScopeDenial(c, callerAccountId, "list-registered-parties");
7407
+ if (listDenial) return listDenial;
7408
+ const all = readRegisteredParties(acct.accountDir);
7409
+ const registeredParties = houseAdmin ? all : Object.fromEntries(Object.entries(all).filter(([, entry]) => entry.account === callerAccountId));
7410
+ console.error(`${TAG15} config action=list-registered-parties caller=${callerAccountId || "none"} houseAdmin=${houseAdmin ? "y" : "n"} count=${Object.keys(registeredParties).length} of=${Object.keys(all).length}`);
7411
+ return c.json({ ok: true, registeredParties });
7412
+ }
7413
+ if (!phone || typeof phone !== "string") {
7414
+ return c.json({ ok: false, error: 'Missing required field "phone" (E.164 format, e.g. +441234567890).' }, 400);
7415
+ }
7416
+ if (action === "add-registered-party") {
7417
+ if (!partyAccount || typeof partyAccount !== "string") {
7418
+ return c.json({ ok: false, error: 'Missing required field "account" (the account whose own number this phone is a counterparty on).' }, 400);
7419
+ }
7420
+ if (!party || typeof party !== "string") {
7421
+ return c.json({ ok: false, error: 'Missing required field "party" (the label for this counterparty, e.g. "driver").' }, 400);
7422
+ }
7423
+ const addDenial = bindingScopeDenial(c, partyAccount, "add-registered-party");
7424
+ if (addDenial) return addDenial;
7425
+ const result2 = setRegisteredParty(acct.accountDir, phone, partyAccount, party);
7426
+ const partyReasonTail = !result2.ok && result2.reason ? ` reason=${result2.reason}` : "";
7427
+ console.error(`${TAG15} config action=add-registered-party phone=${phone} account=${partyAccount} caller=${callerAccountId || "none"} ok=${result2.ok}${partyReasonTail}`);
7428
+ return c.json(result2, result2.ok ? 200 : 400);
7429
+ }
7430
+ const registered = registeredPartyFor(readRegisteredParties(acct.accountDir), phone);
7431
+ if (!houseAdmin) {
7432
+ const removePartyDenial = bindingScopeDenial(c, registered?.account ?? "", "remove-registered-party");
7433
+ if (removePartyDenial) return removePartyDenial;
7434
+ }
7435
+ const result = clearRegisteredParty(acct.accountDir, phone);
7436
+ console.error(`${TAG15} config action=remove-registered-party phone=${phone} registeredTo=${registered?.account ?? "none"} caller=${callerAccountId || "none"} ok=${result.ok}`);
7437
+ return c.json(result, result.ok ? 200 : 400);
7438
+ }
7086
7439
  const denial = houseSocketScopeDenial(c, `config action=${action}`);
7087
7440
  if (denial) return denial;
7088
7441
  const account = resolveAccount();
@@ -7245,14 +7598,15 @@ app3.post("/send-admin", async (c) => {
7245
7598
  const adminPhones = readAdminPhones(account.accountDir);
7246
7599
  const isAdmin = isAdminPhone(phone, adminPhones);
7247
7600
  const managerMatch = isAdmin ? null : managedAccountFor(readAccountManagers(account.accountDir), phone);
7248
- if (!isAdmin && !managerMatch) {
7601
+ const partyMatch = isAdmin || managerMatch ? null : registeredPartyFor(readRegisteredParties(account.accountDir), phone);
7602
+ if (!isAdmin && !managerMatch && !partyMatch) {
7249
7603
  console.error(`[whatsapp:outbound] op=send-refused reason=not-admin-phone to=${phone} account=${sendAccountId}`);
7250
7604
  return c.json(
7251
- { error: `Refused: ${phone} is not a registered admin phone or account manager for this install. Only a number registered with whatsapp-config (add-admin-phone or add-account-manager) can receive an agent-initiated send.` },
7605
+ { error: `Refused: ${phone} is not a registered admin phone, account manager or registered party for this install. Only a number registered with whatsapp-config (add-admin-phone, add-account-manager or add-registered-party) can receive an agent-initiated send.` },
7252
7606
  403
7253
7607
  );
7254
7608
  }
7255
- const via = isAdmin ? "admin-phone" : "account-manager";
7609
+ const via = isAdmin ? "admin-phone" : managerMatch ? "account-manager" : "registered-party";
7256
7610
  const res = resolveSocket(sendAccountId);
7257
7611
  if (!res.ok) {
7258
7612
  if (res.reason === "key-miss") {
@@ -7273,6 +7627,59 @@ app3.post("/send-admin", async (c) => {
7273
7627
  return c.json({ error: String(err) }, 500);
7274
7628
  }
7275
7629
  });
7630
+ app3.post("/notify-manager", async (c) => {
7631
+ try {
7632
+ const body = await c.req.json().catch(() => ({}));
7633
+ const { senderDisplay, summary, draftLocation } = body;
7634
+ for (const [field2, value] of [["senderDisplay", senderDisplay], ["summary", summary], ["draftLocation", draftLocation]]) {
7635
+ if (typeof value !== "string" || value.trim().length === 0) {
7636
+ return c.json({ error: `Missing required field "${field2}".` }, 400);
7637
+ }
7638
+ }
7639
+ const account = resolveAccount();
7640
+ if (!account) return c.json({ error: "No account configured." }, 500);
7641
+ const callerAccountId = c.req.header("x-maxy-caller-account")?.trim() || null;
7642
+ if (!callerAccountId) {
7643
+ console.error(`[whatsapp:outbound] op=notify-manager-refused reason=caller-unsigned`);
7644
+ return c.json({ error: "Refused: this request carried no caller identity, so there is no account whose managers to notify.", reason: "caller-unsigned" }, 403);
7645
+ }
7646
+ const notifyAccountId = callerAccountId;
7647
+ const recipients = recipientsForAccount(readAccountManagers(account.accountDir), notifyAccountId);
7648
+ if (recipients.length === 0) {
7649
+ console.error(`[whatsapp:outbound] op=notify-manager-refused reason=no-registered-manager account=${notifyAccountId}`);
7650
+ return c.json({ error: `Refused: no account manager is registered for account "${notifyAccountId}". Register one with whatsapp-config add-account-manager.`, reason: "no-registered-manager" }, 403);
7651
+ }
7652
+ const res = resolveSocket(notifyAccountId);
7653
+ if (!res.ok) {
7654
+ if (res.reason === "key-miss") {
7655
+ console.error(`[whatsapp:outbound] op=socket-key-miss accountId=${notifyAccountId} presentKeys=${res.presentKeys.join(",") || "none"}`);
7656
+ return c.json({ error: `No WhatsApp socket is registered for account "${notifyAccountId}" (present: ${res.presentKeys.join(", ") || "none"}). This is an account-keying mismatch, not a dropped connection.` }, 503);
7657
+ }
7658
+ return c.json({ error: `WhatsApp account "${notifyAccountId}" is registered but its socket is disconnected.` }, 503);
7659
+ }
7660
+ const text = `New WhatsApp message from ${senderDisplay}.
7661
+
7662
+ ${summary}
7663
+
7664
+ A reply has been drafted: ${draftLocation}`;
7665
+ const delivered = [];
7666
+ const failed = [];
7667
+ for (const phone of recipients) {
7668
+ const sent = await sendTextMessage(res.sock, normalizeE164(phone), text, { accountId: res.accountId });
7669
+ if (sent.success) delivered.push(phone);
7670
+ else failed.push(phone);
7671
+ }
7672
+ recordOutboundSend(callerAccountId, notifyAccountId);
7673
+ console.error(`[whatsapp:outbound] op=notify-manager-ok account=${notifyAccountId} socketAccount=${res.accountId} delivered=${delivered.length} failed=${failed.length}`);
7674
+ if (delivered.length === 0) {
7675
+ return c.json({ error: "Notification failed for every registered manager.", failed }, 502);
7676
+ }
7677
+ return c.json({ success: true, delivered, failed });
7678
+ } catch (err) {
7679
+ console.error(`${TAG15} notify-manager error: ${String(err)}`);
7680
+ return c.json({ error: String(err) }, 500);
7681
+ }
7682
+ });
7276
7683
  app3.post("/reply", async (c) => {
7277
7684
  const sendId = process.hrtime.bigint().toString(36).slice(-8);
7278
7685
  try {
@@ -7349,8 +7756,8 @@ app3.get("/activity", (c) => {
7349
7756
  });
7350
7757
  app3.get("/conversations", (c) => {
7351
7758
  try {
7352
- const requestedAccountId = validateAccountId(c.req.query("accountId") ?? null);
7353
- const { denial, callerAccountId } = callerScopeDenial(c, requestedAccountId);
7759
+ const { accountId: requestedAccountId, targetSource } = resolveLifecycleAccountId(c, c.req.query("accountId") ?? null);
7760
+ const { denial, callerAccountId } = callerScopeDenial(c, requestedAccountId, targetSource);
7354
7761
  if (denial) return denial;
7355
7762
  const projection = projectionFor(requestedAccountId);
7356
7763
  const storeAccountId = resolveReadStoreKey(projection.storeAccountId);
@@ -7371,7 +7778,7 @@ app3.get("/conversations", (c) => {
7371
7778
  };
7372
7779
  });
7373
7780
  conversations.sort((a, b) => (b.lastMessageTimestamp ?? 0) - (a.lastMessageTimestamp ?? 0));
7374
- console.error(`${TAG15} conversations requested=${requestedAccountId} caller=${callerAccountId ?? "none"} account=${storeAccountId} projected=${projecting} storeAccount=${projecting ? storeAccountId : "self"} managerPhonesBound=${managerPhonesBound} storeKeyFound=${allConversations.size > 0} count=${conversations.length}`);
7781
+ console.error(`${TAG15} conversations requested=${requestedAccountId} targetSource=${targetSource} caller=${callerAccountId ?? "none"} account=${storeAccountId} projected=${projecting} storeAccount=${projecting ? storeAccountId : "self"} managerPhonesBound=${managerPhonesBound} storeKeyFound=${allConversations.size > 0} count=${conversations.length}`);
7375
7782
  return c.json({ conversations });
7376
7783
  } catch (err) {
7377
7784
  console.error(`${TAG15} conversations error: ${String(err)}`);
@@ -7380,8 +7787,8 @@ app3.get("/conversations", (c) => {
7380
7787
  });
7381
7788
  app3.get("/messages", (c) => {
7382
7789
  try {
7383
- const requestedAccountId = validateAccountId(c.req.query("accountId") ?? null);
7384
- const { denial, callerAccountId } = callerScopeDenial(c, requestedAccountId);
7790
+ const { accountId: requestedAccountId, targetSource } = resolveLifecycleAccountId(c, c.req.query("accountId") ?? null);
7791
+ const { denial, callerAccountId } = callerScopeDenial(c, requestedAccountId, targetSource);
7385
7792
  if (denial) return denial;
7386
7793
  const jid = c.req.query("jid");
7387
7794
  if (!jid) {
@@ -7427,7 +7834,7 @@ app3.get("/messages", (c) => {
7427
7834
  ...reactions && reactions.length > 0 ? { reactions } : {}
7428
7835
  };
7429
7836
  });
7430
- console.error(`${TAG15} messages requested=${requestedAccountId} caller=${callerAccountId ?? "none"} account=${storeAccountId} jid=${jid} projected=${projecting} storeAccount=${projecting ? storeAccountId : "self"} managerPhonesBound=${managerPhonesBound} storeKeyFound=${storeMessages.length > 0} limit=${effectiveLimit ?? "all"} returned=${messages.length} withAttachments=${withAttachments}`);
7837
+ console.error(`${TAG15} messages requested=${requestedAccountId} targetSource=${targetSource} caller=${callerAccountId ?? "none"} account=${storeAccountId} jid=${jid} projected=${projecting} storeAccount=${projecting ? storeAccountId : "self"} managerPhonesBound=${managerPhonesBound} storeKeyFound=${storeMessages.length > 0} limit=${effectiveLimit ?? "all"} returned=${messages.length} withAttachments=${withAttachments}`);
7431
7838
  return c.json({ messages });
7432
7839
  } catch (err) {
7433
7840
  console.error(`${TAG15} messages error: ${String(err)}`);
@@ -7537,19 +7944,21 @@ app3.get("/conversation-graph-state", async (c) => {
7537
7944
  }
7538
7945
  });
7539
7946
  app3.get("/group-info", async (c) => {
7540
- const accountId = validateAccountId(c.req.query("accountId") ?? null);
7947
+ const { accountId, targetSource } = resolveLifecycleAccountId(c, c.req.query("accountId") ?? null);
7948
+ const callerAccountId = c.req.header("x-maxy-caller-account")?.trim() || null;
7949
+ const scope = `account=${accountId} targetSource=${targetSource} caller=${callerAccountId ?? "none"}`;
7541
7950
  const jid = c.req.query("jid");
7542
7951
  try {
7543
7952
  if (!jid) {
7544
7953
  return c.json({ error: "Missing required parameter: jid" }, 400);
7545
7954
  }
7546
7955
  if (!isGroupJid(jid)) {
7547
- console.error(`${TAG15} group-info error="not a group JID" jid=${jid} account=${accountId}`);
7956
+ console.error(`${TAG15} group-info error="not a group JID" jid=${jid} ${scope}`);
7548
7957
  return c.json({ error: `"${jid}" is not a group JID. Group JIDs end with @g.us.` }, 400);
7549
7958
  }
7550
7959
  const res = resolveSocket(accountId);
7551
7960
  if (!res.ok) {
7552
- console.error(`${TAG15} group-info error="${formatSocketResolutionReason(res)}" account=${accountId}`);
7961
+ console.error(`${TAG15} group-info error="${formatSocketResolutionReason(res)}" ${scope}`);
7553
7962
  return c.json({ error: formatSocketResolutionMessage(res, accountId) }, 400);
7554
7963
  }
7555
7964
  const sock = res.sock;
@@ -7563,10 +7972,10 @@ app3.get("/group-info", async (c) => {
7563
7972
  participantCount: meta.participants.length,
7564
7973
  participants: meta.participants.map((p) => ({ jid: p.id, admin: p.admin ?? null }))
7565
7974
  };
7566
- console.error(`${TAG15} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} account=${accountId}`);
7975
+ console.error(`${TAG15} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} ${scope}`);
7567
7976
  return c.json(result);
7568
7977
  } catch (err) {
7569
- console.error(`${TAG15} group-info error="${String(err)}" jid=${jid} account=${accountId}`);
7978
+ console.error(`${TAG15} group-info error="${String(err)}" jid=${jid} ${scope}`);
7570
7979
  return c.json({ error: `Failed to fetch group info: ${String(err)}` }, 500);
7571
7980
  }
7572
7981
  });
@@ -9437,8 +9846,8 @@ var MAX_FILE_BYTES = 50 * 1024 * 1024;
9437
9846
 
9438
9847
  // app/lib/audio-attachment-mime.ts
9439
9848
  function isMimeAdmitted(mimeType, supportedSet) {
9440
- const bare = mimeType.split(";")[0].trim().toLowerCase();
9441
- return supportedSet.has(bare);
9849
+ const bare2 = mimeType.split(";")[0].trim().toLowerCase();
9850
+ return supportedSet.has(bare2);
9442
9851
  }
9443
9852
 
9444
9853
  // server/lib/stream-offset.ts
@@ -16305,7 +16714,19 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
16305
16714
  "device-url:malformed",
16306
16715
  // Task 2074 — the client receipt proving a notification card actually
16307
16716
  // painted. The only signal that distinguishes "delivered" from "rendered".
16308
- "wa-notify:rendered"
16717
+ "wa-notify:rendered",
16718
+ // Task 2138 — the desktop-notification surface.
16719
+ //
16720
+ // `desktop-capability` is posted once per AdminShell mount and is the only
16721
+ // signal that does not need a message to arrive: on a plain-HTTP LAN origin
16722
+ // the Notification API is unavailable and `requestPermission()` returns
16723
+ // "denied" with no prompt, so without this line that install reports nothing
16724
+ // and reads as "no messages yet".
16725
+ //
16726
+ // `desktop-outcome` carries one verdict per message — `shown`, a skip reason,
16727
+ // or `construct-failed` — so a missing banner is always explained.
16728
+ "wa-notify:desktop-capability",
16729
+ "wa-notify:desktop-outcome"
16309
16730
  ]);
16310
16731
  var app24 = new Hono();
16311
16732
  app24.post("/", async (c) => {
@@ -21797,7 +22218,8 @@ async function managerRcSpawn(opts) {
21797
22218
  role: opts.role,
21798
22219
  channel: opts.channel,
21799
22220
  senderId: opts.senderId,
21800
- targetAccountId: opts.targetAccountId
22221
+ targetAccountId: opts.targetAccountId,
22222
+ party: opts.party
21801
22223
  })
21802
22224
  }).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
21803
22225
  if ("__throw" in res) {
@@ -28034,7 +28456,13 @@ var WaGateway = class {
28034
28456
  serverPath: this.deps.serverPath,
28035
28457
  // Task 1575 — the gate's owner-self verdict; drives owner attribution
28036
28458
  // for an unbound owner self-phone at the spawn site.
28037
- ownerSelfPhone: input.ownerSelfPhone
28459
+ ownerSelfPhone: input.ownerSelfPhone,
28460
+ // Task 2234 — the registered-party label, forwarded verbatim so the
28461
+ // spawn env carries WA_SENDER_PARTY. Undefined for every other sender.
28462
+ // Only this call site takes it: the passive branch above hardcodes
28463
+ // role:'admin', and a registered party is public, so the combination
28464
+ // cannot arise there.
28465
+ party: input.party
28038
28466
  });
28039
28467
  } finally {
28040
28468
  this.spawning.delete(senderId);
@@ -30549,7 +30977,7 @@ var waGateway = new WaGateway({
30549
30977
  });
30550
30978
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
30551
30979
  },
30552
- ensureChannelSession: async ({ accountId, effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath, passive, text, msgId, ownerSelfPhone, replyContext }) => {
30980
+ ensureChannelSession: async ({ accountId, effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath, passive, text, msgId, ownerSelfPhone, replyContext, party }) => {
30553
30981
  warnNonUuidAccount("channel-spawn", effectiveAccountId);
30554
30982
  if (passive) {
30555
30983
  const phoneHash = createHash7("sha256").update(senderId).digest("hex").slice(0, 12);
@@ -30634,7 +31062,7 @@ var waGateway = new WaGateway({
30634
31062
  const sourceSuffix = graphOwnerResolved ? " source=graph-owner" : "";
30635
31063
  console.error(`[whatsapp-native] op=admin-identity senderId=${senderId} userId=${attribution}${sourceSuffix}`);
30636
31064
  }
30637
- const result = await managerRcSpawn({ ...req, userId });
31065
+ const result = await managerRcSpawn({ ...req, userId, party });
30638
31066
  if ("error" in result) {
30639
31067
  console.error(`[whatsapp-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
30640
31068
  return;
@@ -32209,6 +32637,7 @@ for (const acct of listValidAccounts()) {
32209
32637
  }
32210
32638
  purgeNonSocketAdminPhonesAtBoot();
32211
32639
  purgeNonSocketAccountManagersAtBoot();
32640
+ purgeNonSocketRegisteredPartiesAtBoot();
32212
32641
  void preloadEmbedModel();
32213
32642
  warnOnChannelAdminBindingDrift();
32214
32643
  var bootEnabled = Array.isArray(bootAccountConfig?.enabledPlugins) ? bootAccountConfig.enabledPlugins : [];
@@ -32337,7 +32766,10 @@ init({
32337
32766
  // always captured it and manager.ts assigns it here, but this argument
32338
32767
  // omitted it, so the agent received "this booking" with nothing to
32339
32768
  // resolve it against.
32340
- replyContext: msg.replyContext
32769
+ replyContext: msg.replyContext,
32770
+ // Task 2234 — the gate's registered-party label, carried to the spawn so
32771
+ // the session env has WA_SENDER_PARTY. Undefined for every other sender.
32772
+ party: msg.party
32341
32773
  });
32342
32774
  } catch (err) {
32343
32775
  console.error(