@rubytech/create-sitedesk-code 0.1.518 → 0.1.520

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 (346) hide show
  1. package/dist/__tests__/edge-bind-template-wiring.test.js +37 -0
  2. package/dist/__tests__/samba-provision.test.js +103 -1
  3. package/dist/index.js +19 -4
  4. package/dist/samba-provision.js +72 -0
  5. package/package.json +1 -1
  6. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +65 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +69 -19
  9. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  10. package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
  11. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
  12. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
  13. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
  14. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
  15. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
  16. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
  17. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
  18. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
  19. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
  20. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
  28. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
  30. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
  37. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
  38. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
  39. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
  40. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
  41. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
  42. package/payload/platform/plugins/docs/references/admin-ui.md +43 -5
  43. package/payload/platform/plugins/docs/references/samba.md +2 -0
  44. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  45. package/payload/platform/plugins/email/PLUGIN.md +20 -1
  46. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
  47. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
  48. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
  49. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
  50. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
  51. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
  52. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
  53. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
  54. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
  55. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
  56. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
  57. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
  58. package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
  59. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  60. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
  61. package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
  62. package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
  63. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -0
  64. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  65. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +76 -0
  66. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  67. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
  68. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
  69. package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
  70. package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
  71. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
  72. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
  73. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
  74. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
  75. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
  76. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
  77. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
  78. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
  79. package/payload/platform/plugins/email/references/email-reference.md +14 -2
  80. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
  81. package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
  82. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
  84. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  85. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
  86. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
  88. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
  91. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
  92. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
  94. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  95. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
  98. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
  99. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
  100. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
  101. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
  102. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
  103. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
  104. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
  106. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
  107. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
  108. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
  109. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
  110. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
  111. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
  112. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
  113. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
  114. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
  115. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  116. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
  117. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  118. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
  119. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
  120. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
  121. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
  122. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  123. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
  124. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  125. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -3
  126. package/payload/platform/scripts/check-no-esm-require.mjs +5 -3
  127. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  128. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  129. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  130. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
  131. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +5 -3
  132. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
  133. package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
  134. package/payload/platform/services/telegram-channel/dist/instructions.js +13 -10
  135. package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
  136. package/payload/platform/services/webchat-channel/dist/instructions.d.ts +6 -2
  137. package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
  138. package/payload/platform/services/webchat-channel/dist/instructions.js +66 -32
  139. package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
  140. package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
  141. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  142. package/payload/platform/services/webchat-channel/dist/notification.js +11 -4
  143. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  144. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  145. package/payload/platform/services/webchat-channel/dist/server.js +30 -14
  146. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  147. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  148. package/payload/platform/services/webchat-channel/dist/turn-follow.js +11 -5
  149. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  150. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -3
  151. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  152. package/payload/platform/services/whatsapp-channel/dist/notification.js +9 -5
  153. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  154. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  155. package/payload/platform/services/whatsapp-channel/dist/server.js +6 -1
  156. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  157. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  158. package/payload/platform/services/whatsapp-channel/dist/targets.js +8 -2
  159. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  160. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  161. package/payload/platform/templates/systemd/edge.service.template +1 -1
  162. package/payload/server/chunk-PXNY2TAP.js +1980 -0
  163. package/payload/server/chunk-TIIWMF5G.js +4837 -0
  164. package/payload/server/{chunk-S6HYTIU3.js → chunk-UX2FRNO7.js} +72 -2031
  165. package/payload/server/manager-B6OFOXSW.js +61 -0
  166. package/payload/server/maxy-edge.js +5 -3
  167. package/payload/server/package.json +2 -1
  168. package/payload/server/public/activity.html +5 -5
  169. package/payload/server/public/agents.html +4 -4
  170. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
  171. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
  172. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
  173. package/payload/server/public/assets/AdminShell-Djg6ndne.js +2 -0
  174. package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
  175. package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
  176. package/payload/server/public/assets/{activity-BE9gv5xL.js → activity-sqbyX0pX.js} +1 -1
  177. package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
  178. package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
  179. package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
  180. package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
  181. package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
  182. package/payload/server/public/assets/{agents-B-x0Ai2v.js → agents-BFYM5EDA.js} +1 -1
  183. package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
  184. package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
  185. package/payload/server/public/assets/{browser-ZEQUkPfs.js → browser-DP5GZe4G.js} +1 -1
  186. package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
  187. package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
  188. package/payload/server/public/assets/calendar-BKaemLDG.js +1 -0
  189. package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
  190. package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
  191. package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
  192. package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
  193. package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
  194. package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
  195. package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
  196. package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
  197. package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
  198. package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
  199. package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
  200. package/payload/server/public/assets/data-CfjttTvR.js +1 -0
  201. package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
  202. package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
  203. package/payload/server/public/assets/{file-text-BTV6EdBv.js → file-text-BKK7R-k_.js} +1 -1
  204. package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
  205. package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
  206. package/payload/server/public/assets/{graph-hD_gPbbL.js → graph-FuxYySjK.js} +3 -3
  207. package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
  208. package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
  209. package/payload/server/public/assets/{graph-labels-CY1T19PY.js → graph-labels-Cip6fmKK.js} +1 -1
  210. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
  211. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
  212. package/payload/server/public/assets/{maximize-2-BFV9VKHO.js → maximize-2-D79AhVDo.js} +1 -1
  213. package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
  214. package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
  215. package/payload/server/public/assets/operator-BPAfgFhP.js +1 -0
  216. package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
  217. package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
  218. package/payload/server/public/assets/page--78CbdFs.js +32 -0
  219. package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
  220. package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
  221. package/payload/server/public/assets/page-BqEyS0W4.js +1 -0
  222. package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
  223. package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
  224. package/payload/server/public/assets/{public-D6Tnj5LU.js → public-D1xs3FzD.js} +1 -1
  225. package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
  226. package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
  227. package/payload/server/public/assets/{rotate-ccw-C-RfQUza.js → rotate-ccw-CcU_XD8B.js} +1 -1
  228. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
  229. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
  230. package/payload/server/public/assets/{routines-NIMElUzV.js → routines-CA7D0myr.js} +1 -1
  231. package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
  232. package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
  233. package/payload/server/public/assets/{skills-N0ZlHosx.js → skills-CfVPPSxO.js} +1 -1
  234. package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
  235. package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
  236. package/payload/server/public/assets/tasks-DgieyZqz.js +1 -0
  237. package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
  238. package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
  239. package/payload/server/public/assets/{time-entry-format-jQxlRPoI.js → time-entry-format-IFqinQJj.js} +1 -1
  240. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
  241. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
  242. package/payload/server/public/assets/{triangle-alert-wUS4rpzv.js → triangle-alert-C5vH__sq.js} +1 -1
  243. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
  244. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
  245. package/payload/server/public/assets/{useCopyFeedback-BRix_wIl.js → useCopyFeedback-BeL2gPl6.js} +1 -1
  246. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
  247. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
  248. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +14 -0
  249. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
  250. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
  251. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +1 -0
  252. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
  253. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
  254. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js +2 -0
  255. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
  256. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
  257. package/payload/server/public/assets/{wrench-sDivAjhf.js → wrench-BBEup3hd.js} +1 -1
  258. package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
  259. package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
  260. package/payload/server/public/browser.html +4 -4
  261. package/payload/server/public/calendar.html +7 -7
  262. package/payload/server/public/chat.html +13 -13
  263. package/payload/server/public/data.html +11 -11
  264. package/payload/server/public/graph.html +9 -9
  265. package/payload/server/public/index.html +14 -14
  266. package/payload/server/public/operator.html +14 -14
  267. package/payload/server/public/public.html +13 -13
  268. package/payload/server/public/routines.html +6 -6
  269. package/payload/server/public/skills.html +5 -5
  270. package/payload/server/public/tasks.html +6 -6
  271. package/payload/server/server.js +8176 -11914
  272. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js +0 -1
  273. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.br +0 -0
  274. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.gz +0 -0
  275. package/payload/server/public/assets/AdminShell-TSa8KgID.js +0 -2
  276. package/payload/server/public/assets/AdminShell-TSa8KgID.js.br +0 -0
  277. package/payload/server/public/assets/AdminShell-TSa8KgID.js.gz +0 -0
  278. package/payload/server/public/assets/activity-BE9gv5xL.js.br +0 -0
  279. package/payload/server/public/assets/activity-BE9gv5xL.js.gz +0 -0
  280. package/payload/server/public/assets/admin-Cb0BZriv.js +0 -1
  281. package/payload/server/public/assets/admin-Cb0BZriv.js.br +0 -0
  282. package/payload/server/public/assets/admin-Cb0BZriv.js.gz +0 -0
  283. package/payload/server/public/assets/agents-B-x0Ai2v.js.br +0 -0
  284. package/payload/server/public/assets/agents-B-x0Ai2v.js.gz +0 -0
  285. package/payload/server/public/assets/browser-ZEQUkPfs.js.br +0 -0
  286. package/payload/server/public/assets/browser-ZEQUkPfs.js.gz +0 -0
  287. package/payload/server/public/assets/calendar-DsRxrW0G.js +0 -1
  288. package/payload/server/public/assets/calendar-DsRxrW0G.js.br +0 -0
  289. package/payload/server/public/assets/calendar-DsRxrW0G.js.gz +0 -0
  290. package/payload/server/public/assets/chat-BS9B9lDo.js +0 -1
  291. package/payload/server/public/assets/chat-BS9B9lDo.js.br +0 -1
  292. package/payload/server/public/assets/chat-BS9B9lDo.js.gz +0 -0
  293. package/payload/server/public/assets/chevron-left-DLtGuH6K.js +0 -1
  294. package/payload/server/public/assets/chevron-right-CY_trg5L.js +0 -1
  295. package/payload/server/public/assets/chevron-right-CY_trg5L.js.br +0 -0
  296. package/payload/server/public/assets/clock-oM5driJm.js +0 -1
  297. package/payload/server/public/assets/clock-oM5driJm.js.br +0 -0
  298. package/payload/server/public/assets/clock-oM5driJm.js.gz +0 -0
  299. package/payload/server/public/assets/data-Cio6PO0F.js +0 -1
  300. package/payload/server/public/assets/data-Cio6PO0F.js.br +0 -2
  301. package/payload/server/public/assets/data-Cio6PO0F.js.gz +0 -0
  302. package/payload/server/public/assets/file-text-BTV6EdBv.js.br +0 -0
  303. package/payload/server/public/assets/file-text-BTV6EdBv.js.gz +0 -0
  304. package/payload/server/public/assets/graph-hD_gPbbL.js.br +0 -0
  305. package/payload/server/public/assets/graph-hD_gPbbL.js.gz +0 -0
  306. package/payload/server/public/assets/graph-labels-CY1T19PY.js.br +0 -0
  307. package/payload/server/public/assets/graph-labels-CY1T19PY.js.gz +0 -0
  308. package/payload/server/public/assets/maximize-2-BFV9VKHO.js.br +0 -0
  309. package/payload/server/public/assets/maximize-2-BFV9VKHO.js.gz +0 -0
  310. package/payload/server/public/assets/operator-Dxze8CwV.js +0 -1
  311. package/payload/server/public/assets/operator-Dxze8CwV.js.br +0 -0
  312. package/payload/server/public/assets/operator-Dxze8CwV.js.gz +0 -0
  313. package/payload/server/public/assets/page-Bi86QF6_.js +0 -32
  314. package/payload/server/public/assets/page-Bi86QF6_.js.br +0 -0
  315. package/payload/server/public/assets/page-Bi86QF6_.js.gz +0 -0
  316. package/payload/server/public/assets/page-BnXly9aN.js +0 -1
  317. package/payload/server/public/assets/page-BnXly9aN.js.br +0 -0
  318. package/payload/server/public/assets/page-BnXly9aN.js.gz +0 -0
  319. package/payload/server/public/assets/public-D6Tnj5LU.js.br +0 -0
  320. package/payload/server/public/assets/public-D6Tnj5LU.js.gz +0 -0
  321. package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.br +0 -0
  322. package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.gz +0 -0
  323. package/payload/server/public/assets/routines-NIMElUzV.js.br +0 -0
  324. package/payload/server/public/assets/routines-NIMElUzV.js.gz +0 -0
  325. package/payload/server/public/assets/skills-N0ZlHosx.js.br +0 -0
  326. package/payload/server/public/assets/skills-N0ZlHosx.js.gz +0 -0
  327. package/payload/server/public/assets/tasks-BsB_0duv.js +0 -1
  328. package/payload/server/public/assets/tasks-BsB_0duv.js.br +0 -0
  329. package/payload/server/public/assets/tasks-BsB_0duv.js.gz +0 -0
  330. package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.br +0 -0
  331. package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.gz +0 -0
  332. package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.br +0 -0
  333. package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.gz +0 -0
  334. package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.br +0 -0
  335. package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.gz +0 -0
  336. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js +0 -14
  337. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.br +0 -0
  338. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.gz +0 -0
  339. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css +0 -1
  340. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
  341. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
  342. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js +0 -2
  343. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.br +0 -0
  344. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.gz +0 -0
  345. package/payload/server/public/assets/wrench-sDivAjhf.js.br +0 -0
  346. package/payload/server/public/assets/wrench-sDivAjhf.js.gz +0 -0
@@ -0,0 +1,37 @@
1
+ // Nothing else in the repo ties the edge unit template's placeholders to the
2
+ // substitutions in index.ts, so either half can be edited alone and the whole
3
+ // suite stays green. Two silent failures this closes:
4
+ //
5
+ // - substitution added, template left at a literal value → the replace is a
6
+ // no-op, the unit still binds 0.0.0.0, and the Task 2082 security fix ships
7
+ // inert with every pickEdgeBindDecision test passing;
8
+ // - template placeholder added, substitution forgotten → the unit ships a
9
+ // literal `Environment=EDGE_HOSTNAME=__EDGE_HOSTNAME__`, which edge.ts's
10
+ // `process.env.EDGE_HOSTNAME ?? '0.0.0.0'` does NOT catch, because the
11
+ // variable is present, just not an address, so server.listen() gets a
12
+ // non-address and the edge restart-loops.
13
+ //
14
+ // Both files are read from disk at runtime, the same pattern as
15
+ // macos-darwin-branch.test.ts and base-toolchain-deps.test.ts.
16
+ import test from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { readFileSync } from "node:fs";
19
+ import { fileURLToPath } from "node:url";
20
+ import { dirname, resolve } from "node:path";
21
+ // dist/__tests__/edge-bind-template-wiring.test.js → ../../src/index.ts
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ const INDEX_TS = resolve(here, "../../src/index.ts");
24
+ // dist/__tests__ → up four is the maxy-code root that holds platform/.
25
+ const EDGE_TEMPLATE = resolve(here, "../../../../platform/templates/systemd/edge.service.template");
26
+ const SRC = readFileSync(INDEX_TS, "utf-8");
27
+ const TEMPLATE = readFileSync(EDGE_TEMPLATE, "utf-8");
28
+ test("edge template: EDGE_HOSTNAME is a placeholder, not a hardcoded address", () => {
29
+ assert.match(TEMPLATE, /^Environment=EDGE_HOSTNAME=__EDGE_HOSTNAME__$/m, "edge.service.template must take EDGE_HOSTNAME from the installer's bind decision (Task 2082), not hardcode it");
30
+ });
31
+ test("edge template: every placeholder has a matching substitution in index.ts", () => {
32
+ const placeholders = [...new Set(TEMPLATE.match(/__[A-Z0-9_]+__/g) ?? [])].sort();
33
+ assert.ok(placeholders.includes("__EDGE_HOSTNAME__"), "template must carry __EDGE_HOSTNAME__");
34
+ for (const p of placeholders) {
35
+ assert.ok(SRC.includes(`.replace(/${p}/g,`), `${p} appears in edge.service.template but index.ts has no .replace(/${p}/g, ...) — the rendered unit would ship the literal placeholder`);
36
+ }
37
+ });
@@ -6,7 +6,7 @@
6
6
  // style and runs under `node --test dist/__tests__/*.test.js` after build.
7
7
  import test from "node:test";
8
8
  import assert from "node:assert/strict";
9
- import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
9
+ import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
10
10
  // ---------------------------------------------------------------------------
11
11
  // Fixtures
12
12
  // ---------------------------------------------------------------------------
@@ -49,6 +49,42 @@ const MIXED_PUBLIC_AND_PRIVATE = {
49
49
  eth0: [{ address: "178.105.251.82", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:02", internal: false, cidr: "178.105.251.82/32" }],
50
50
  wg0: [{ address: "10.8.0.3", netmask: "255.255.255.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: false, cidr: "10.8.0.3/24" }],
51
51
  };
52
+ // Hetzner Cloud shape: public eth0 plus a provider private network on ens10.
53
+ // pickBindDecision calls this `lan`; the edge must still bind loopback.
54
+ const HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET = {
55
+ lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
56
+ eth0: [{ address: "167.233.21.246", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:03", internal: false, cidr: "167.233.21.246/32" }],
57
+ ens10: [{ address: "10.0.0.2", netmask: "255.255.0.0", family: "IPv4", mac: "96:00:00:00:00:04", internal: false, cidr: "10.0.0.2/16" }],
58
+ };
59
+ // Private wlan0 plus a public address elsewhere. Reaches pickBindDecision's
60
+ // wlan0 early return, which MIXED_PUBLIC_AND_PRIVATE never does because its
61
+ // private address is on wg0.
62
+ const PRIVATE_WLAN0_PLUS_PUBLIC = {
63
+ lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
64
+ wlan0: [{ address: "192.168.1.50", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "192.168.1.50/24" }],
65
+ eth0: [{ address: "203.0.113.7", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:05", internal: false, cidr: "203.0.113.7/32" }],
66
+ };
67
+ // Private eth0 plus a public address elsewhere. Reaches the eth0 early return.
68
+ const PRIVATE_ETH0_PLUS_PUBLIC = {
69
+ lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
70
+ eth0: [{ address: "10.0.0.5", netmask: "255.255.255.0", family: "IPv4", mac: "11:11:11:11:11:11", internal: false, cidr: "10.0.0.5/24" }],
71
+ ens5: [{ address: "198.51.100.9", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:06", internal: false, cidr: "198.51.100.9/32" }],
72
+ };
73
+ // One interface carrying both a public and a private IPv4. No other fixture in
74
+ // this file has a multi-address interface.
75
+ const ONE_IFACE_PUBLIC_AND_PRIVATE = {
76
+ lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
77
+ eth0: [
78
+ { address: "192.0.2.44", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:07", internal: false, cidr: "192.0.2.44/32" },
79
+ { address: "172.16.4.9", netmask: "255.255.0.0", family: "IPv4", mac: "96:00:00:00:00:07", internal: false, cidr: "172.16.4.9/16" },
80
+ ],
81
+ };
82
+ // CGNAT-only host. isPrivateIPv4 counts 100.64/10 as private, so this binds
83
+ // 0.0.0.0. Pinned so the outcome is declared rather than incidental.
84
+ const CGNAT_ONLY = {
85
+ lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
86
+ eth0: [{ address: "100.100.3.14", netmask: "255.192.0.0", family: "IPv4", mac: "96:00:00:00:00:08", internal: false, cidr: "100.100.3.14/10" }],
87
+ };
52
88
  // ---------------------------------------------------------------------------
53
89
  // isPrivateIPv4 — RFC1918 / CGNAT / link-local classification
54
90
  // ---------------------------------------------------------------------------
@@ -107,6 +143,72 @@ test("pickBindDecision: none when the only non-loopback iface has no IPv4", () =
107
143
  assert.deepEqual(pickBindDecision(PI_IPV6_ONLY_LAN), { kind: "none" });
108
144
  });
109
145
  // ---------------------------------------------------------------------------
146
+ // pickEdgeBindDecision — 0.0.0.0 only when private exists and public does not
147
+ // ---------------------------------------------------------------------------
148
+ test("pickEdgeBindDecision: private-only hosts keep the public bind", () => {
149
+ for (const [name, ifaces] of [
150
+ ["PI_WLAN0_ONLY", PI_WLAN0_ONLY],
151
+ ["PI_BOTH_IFACES", PI_BOTH_IFACES],
152
+ ["PI_ETH0_ONLY", PI_ETH0_ONLY],
153
+ ["PI_USB0_FALLBACK", PI_USB0_FALLBACK],
154
+ ["CGNAT_ONLY", CGNAT_ONLY],
155
+ ]) {
156
+ assert.equal(pickEdgeBindDecision(ifaces).hostname, "0.0.0.0", name);
157
+ }
158
+ });
159
+ // The fail-open this task exists to close: pickBindDecision calls every one of
160
+ // these `lan`, so keeping its verdict verbatim would leave the edge public on a
161
+ // cloud box that happens to carry a provider private network.
162
+ test("pickEdgeBindDecision: a public IPv4 forces loopback even when a private one exists", () => {
163
+ for (const [name, ifaces] of [
164
+ ["MIXED_PUBLIC_AND_PRIVATE", MIXED_PUBLIC_AND_PRIVATE],
165
+ ["HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET", HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET],
166
+ ["PRIVATE_WLAN0_PLUS_PUBLIC", PRIVATE_WLAN0_PLUS_PUBLIC],
167
+ ["PRIVATE_ETH0_PLUS_PUBLIC", PRIVATE_ETH0_PLUS_PUBLIC],
168
+ ["ONE_IFACE_PUBLIC_AND_PRIVATE", ONE_IFACE_PUBLIC_AND_PRIVATE],
169
+ ]) {
170
+ const d = pickEdgeBindDecision(ifaces);
171
+ assert.equal(d.hostname, "127.0.0.1", name);
172
+ assert.equal(d.kind, "lan", `${name} still classifies as lan for smbd`);
173
+ }
174
+ });
175
+ test("pickEdgeBindDecision: public-only host binds loopback", () => {
176
+ const d = pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY);
177
+ assert.equal(d.hostname, "127.0.0.1");
178
+ assert.equal(d.kind, "loopback-only");
179
+ });
180
+ // Unlike the Samba call site, which throws on `none`, the edge binds loopback
181
+ // and lets the install finish: a loopback edge still serves the tunnel.
182
+ test("pickEdgeBindDecision: no non-loopback IPv4 binds loopback rather than throwing", () => {
183
+ for (const ifaces of [PI_LO_ONLY, PI_IPV6_ONLY_LAN]) {
184
+ const d = pickEdgeBindDecision(ifaces);
185
+ assert.equal(d.hostname, "127.0.0.1");
186
+ assert.equal(d.kind, "none");
187
+ assert.deepEqual(d.privateAddresses, []);
188
+ assert.deepEqual(d.publicAddresses, []);
189
+ }
190
+ });
191
+ test("pickEdgeBindDecision: reports the interface and addresses it decided from", () => {
192
+ assert.deepEqual(pickEdgeBindDecision(MIXED_PUBLIC_AND_PRIVATE), {
193
+ hostname: "127.0.0.1",
194
+ kind: "lan",
195
+ iface: "wg0",
196
+ publicAddresses: ["178.105.251.82"],
197
+ privateAddresses: ["10.8.0.3"],
198
+ });
199
+ assert.deepEqual(pickEdgeBindDecision(PI_WLAN0_ONLY), {
200
+ hostname: "0.0.0.0",
201
+ kind: "lan",
202
+ iface: "wlan0",
203
+ publicAddresses: [],
204
+ privateAddresses: ["192.168.1.50"],
205
+ });
206
+ });
207
+ test("formatEdgeBindDecision: carries both grep tokens and never leaves an empty field", () => {
208
+ assert.equal(formatEdgeBindDecision(pickEdgeBindDecision(PI_WLAN0_ONLY)), "[edge-bind-decision] kind=lan iface=wlan0 private=192.168.1.50 public=none EDGE_HOSTNAME=0.0.0.0");
209
+ assert.equal(formatEdgeBindDecision(pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY)), "[edge-bind-decision] kind=loopback-only iface=none private=none public=167.233.21.246 EDGE_HOSTNAME=127.0.0.1");
210
+ });
211
+ // ---------------------------------------------------------------------------
110
212
  // renderBrandStanza — exact spec match
111
213
  // ---------------------------------------------------------------------------
112
214
  test("renderBrandStanza emits the spec directives in order", () => {
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import { classifyPortHolder } from "./preflight-port-classifier.js";
29
29
  import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
30
30
  import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
31
31
  import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
32
- import { pickBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
32
+ import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
33
33
  import { networkInterfaces, userInfo, cpus } from "node:os";
34
34
  const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
35
35
  // Brand manifest — read from payload to derive all brand-specific installation values.
@@ -4493,11 +4493,20 @@ WantedBy=default.target
4493
4493
  // VNC_DISPLAY (defined above for the main brand unit) is also stamped into
4494
4494
  // the edge unit so both services agree on the X display. Per-brand display
4495
4495
  // closes the cross-brand cookie leak documented.
4496
+ // Task 2082 — the edge binds 0.0.0.0 only on a host with a private IPv4 and
4497
+ // no public one; anything else binds loopback so a rented server never puts
4498
+ // the admin edge on the internet. Logged with its inputs because a wrong
4499
+ // classification is otherwise invisible until an external probe finds it.
4500
+ // Linux-only by construction: this function returns at the darwin branch
4501
+ // above, and darwin runs no edge process at all.
4502
+ const edgeBind = pickEdgeBindDecision(networkInterfaces());
4503
+ logFile(` ${formatEdgeBindDecision(edgeBind)}`);
4496
4504
  const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/edge.service.template");
4497
4505
  if (existsSync(edgeTemplatePath)) {
4498
4506
  const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
4499
4507
  .replace(/__INSTALL_DIR__/g, INSTALL_DIR)
4500
4508
  .replace(/__EDGE_PORT__/g, String(PORT))
4509
+ .replace(/__EDGE_HOSTNAME__/g, edgeBind.hostname)
4501
4510
  .replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
4502
4511
  .replace(/__PERSIST_DIR__/g, persistDir)
4503
4512
  .replace(/__VNC_DISPLAY__/g, String(VNC_DISPLAY))
@@ -4505,7 +4514,7 @@ WantedBy=default.target
4505
4514
  .replace(/__CDP_PORT__/g, String(CDP_PORT_BRAND))
4506
4515
  .replace(/__RFB_PORT__/g, String(RFB_PORT));
4507
4516
  writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
4508
- logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} VNC_DISPLAY=:${VNC_DISPLAY} RFB_PORT=${RFB_PORT} WEBSOCKIFY_PORT=${WEBSOCKIFY_PORT_BRAND} CDP_PORT=${CDP_PORT_BRAND}`);
4517
+ logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} EDGE_HOSTNAME=${edgeBind.hostname} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} VNC_DISPLAY=:${VNC_DISPLAY} RFB_PORT=${RFB_PORT} WEBSOCKIFY_PORT=${WEBSOCKIFY_PORT_BRAND} CDP_PORT=${CDP_PORT_BRAND}`);
4509
4518
  }
4510
4519
  else {
4511
4520
  console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — VNC transport unavailable`);
@@ -5389,8 +5398,14 @@ try {
5389
5398
  console.log("================================================================");
5390
5399
  console.log("");
5391
5400
  if (isLinux()) {
5392
- console.log(` Same network (Pi / LAN): http://${DEVICE_HOSTNAME}.local:${PORT}`);
5393
- console.log("");
5401
+ // Only true when the edge binds 0.0.0.0. On a host classified to loopback
5402
+ // (Task 2082) nothing answers on the LAN address, so printing this line
5403
+ // there would be a false instruction; the SSH-forward recipe below is the
5404
+ // whole story for that host class.
5405
+ if (pickEdgeBindDecision(networkInterfaces()).hostname === "0.0.0.0") {
5406
+ console.log(` Same network (Pi / LAN): http://${DEVICE_HOSTNAME}.local:${PORT}`);
5407
+ console.log("");
5408
+ }
5394
5409
  console.log(` Remote access (Hetzner / cloud) — on your local machine run:`);
5395
5410
  console.log(` ssh -L ${PORT}:localhost:${PORT} -L ${BRAND.websockifyPort}:localhost:${BRAND.websockifyPort} admin@<server-ipv4>`);
5396
5411
  console.log(` Then open:`);
@@ -14,6 +14,11 @@
14
14
  // stanza intact (peer-brand isolation is the structural invariant).
15
15
  // 5. Report whether any brand stanza remains so the uninstall step can
16
16
  // decide whether to apt-purge samba.
17
+ //
18
+ // Despite the file name, the interface classification here serves two
19
+ // consumers, not one: `isPrivateIPv4` and `pickBindDecision` answer where smbd
20
+ // binds, and `pickEdgeBindDecision` (Task 2082) answers where the edge binds.
21
+ // The two deliberately disagree on a dual-homed host; see that function.
17
22
  // ---------------------------------------------------------------------------
18
23
  // Brand-scoped Samba stanza
19
24
  // ---------------------------------------------------------------------------
@@ -152,6 +157,73 @@ export function pickBindDecision(ifaces) {
152
157
  const hasNonLoopbackIPv4 = Object.keys(ifaces).some((name) => name !== "lo" && ipv4Addrs(name).length > 0);
153
158
  return hasNonLoopbackIPv4 ? { kind: "loopback-only" } : { kind: "none" };
154
159
  }
160
+ /**
161
+ * Decide how the edge should bind, from the same interface table
162
+ * `pickBindDecision` reads. The edge binds `0.0.0.0` only when the host has at
163
+ * least one private IPv4 AND zero public IPv4; every other shape binds
164
+ * `127.0.0.1`. Task 2082.
165
+ *
166
+ * Three properties are deliberate, not oversights:
167
+ *
168
+ * 1. This DISAGREES with `pickBindDecision` on a dual-homed host. For SMB, a
169
+ * private interface alongside a public one keeps the LAN bind (operator
170
+ * confirmation, Task 755 — see the MIXED_PUBLIC_AND_PRIVATE fixture). For
171
+ * the edge the operator decided the opposite: a cloud server carrying a
172
+ * provider private network (Hetzner Cloud's public `eth0` plus private
173
+ * `ens10`) must not silently keep a public bind. Both functions are right
174
+ * for their own consumer; this is not a rename of the other.
175
+ * 2. IPv6 is ignored. `pickBindDecision` already filters to IPv4, and
176
+ * `0.0.0.0` binds IPv4 only, so a public IPv6 address cannot be exposed by
177
+ * this bind under either outcome.
178
+ * 3. A cloud host presenting its public address by 1:1 NAT is NOT covered.
179
+ * Its guest NIC carries only a private address, so this reads a
180
+ * private-only host and binds `0.0.0.0` while the box is publicly
181
+ * reachable. The scoped signal is a public IPv4 alongside the private one;
182
+ * no host in `.docs/install/` behaves that way, and catching it would need
183
+ * an outbound probe rather than the interface table.
184
+ *
185
+ * `kind: "none"` binds `127.0.0.1` rather than throwing, unlike the Samba call
186
+ * site, which hard-fails: a share bound to nothing is useless, whereas a
187
+ * loopback edge still serves the Cloudflare tunnel, which dials
188
+ * `http://localhost:<port>`. Cost: a host that gains an interface later stays
189
+ * loopback-only until the installer is re-run, which the decision line makes
190
+ * visible in the install log.
191
+ */
192
+ export function pickEdgeBindDecision(ifaces) {
193
+ const bind = pickBindDecision(ifaces);
194
+ const publicAddresses = [];
195
+ const privateAddresses = [];
196
+ for (const [name, addrs] of Object.entries(ifaces)) {
197
+ if (name === "lo" || !addrs)
198
+ continue;
199
+ for (const a of addrs) {
200
+ if (a.family !== "IPv4" || a.internal)
201
+ continue;
202
+ if (isPrivateIPv4(a.address))
203
+ privateAddresses.push(a.address);
204
+ else
205
+ publicAddresses.push(a.address);
206
+ }
207
+ }
208
+ return {
209
+ hostname: privateAddresses.length > 0 && publicAddresses.length === 0 ? "0.0.0.0" : "127.0.0.1",
210
+ kind: bind.kind,
211
+ iface: bind.kind === "lan" ? bind.lanInterface : null,
212
+ publicAddresses,
213
+ privateAddresses,
214
+ };
215
+ }
216
+ /**
217
+ * The install-log line for an edge bind decision. The shape is load-bearing:
218
+ * the task's diagnostic path greps `bind-decision` and `EDGE_HOSTNAME`, so both
219
+ * tokens must survive any edit. Empty lists render as the literal `none` so a
220
+ * host with no addresses is distinguishable from a truncated line.
221
+ */
222
+ export function formatEdgeBindDecision(d) {
223
+ const list = (xs) => (xs.length === 0 ? "none" : xs.join(","));
224
+ return `[edge-bind-decision] kind=${d.kind} iface=${d.iface ?? "none"} ` +
225
+ `private=${list(d.privateAddresses)} public=${list(d.publicAddresses)} EDGE_HOSTNAME=${d.hostname}`;
226
+ }
155
227
  // ---------------------------------------------------------------------------
156
228
  // smb.conf merge / remove
157
229
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.518",
3
+ "version": "0.1.520",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"
@@ -0,0 +1,65 @@
1
+ # Task 2097 — `email-signature-set` implementation plan
2
+
3
+ Design: [`2026-07-28-task-2097-email-signature-set-design.md`](../specs/2026-07-28-task-2097-email-signature-set-design.md).
4
+
5
+ ## Registration surfaces — corrected against the tree
6
+
7
+ The task file names `ADMIN_CORE_TOOLS`. It does not exist: `grep -rn 'ADMIN_CORE_TOOLS' platform/` returns
8
+ nothing outside comments. The live surfaces, each confirmed by reading the file, are four:
9
+
10
+ | # | Surface | Evidence |
11
+ |---|---|---|
12
+ | 1 | `plugins/email/mcp/src/index.ts` | `eagerTool(server, "email-setup", …)` at line 124 |
13
+ | 2 | `plugins/email/PLUGIN.md` | frontmatter `description:`, `tools:` list, Capabilities, Skills table |
14
+ | 3 | `platform/templates/specialists/agents/personal-assistant.md` | the only template matching `grep -rl email-setup platform/templates/` |
15
+ | 4 | `services/claude-session-manager/src/canonical-tool-names.generated.ts` | header: generated from `PLUGIN.md` `tools:` blocks, `check-canonical-tool-names.mjs` fails the build on drift |
16
+
17
+ Surface 4 is not in the task file and would have broken the build. It is regenerated, never hand-edited:
18
+ `cd platform && npm run gen:canonical-tools`.
19
+
20
+ Three further files match `email-draft-send` and are deliberately **not** touched:
21
+ `plugins/admin/hooks/preference-consult-gate.sh`, `scripts/lib/provision-account-dir.sh`, and the
22
+ `plugins/admin/PLUGIN.md` prose describing them. All three are the outbound-send preference gate,
23
+ matching sends that carry attachments. `email-signature-set` sends nothing, so it is not a gate member.
24
+
25
+ ## Steps
26
+
27
+ 1. **`lib/signature.ts`** — add `SIGNATURE_BASENAME`, `signatureDirFor(accountId)`, and
28
+ `saveSignature(accountDir, parts)`. `loadSignature` / `applySignature` unchanged.
29
+ `signatureDirFor` is `accountDirFor(accountId) || process.env.ACCOUNT_DIR || undefined`, which is the
30
+ expression `compose.ts:112` already computes, with an empty-string env normalised to `undefined`
31
+ (both already load as no-signature, so behaviour is unchanged). No import cycle: `attachment-resolve.ts`
32
+ imports only `node:fs` and `node:path`.
33
+ 2. **`lib/compose.ts`** — replace the inline `sigDir` expression with `signatureDirFor(accountId)` and the
34
+ literal `"email-signature"` in `assetNote` with `SIGNATURE_BASENAME`. `accountDirFor` stays imported for
35
+ the attachment path at line 75.
36
+ 3. **`tools/email-signature-set.ts`** — refuse an all-absent call and an unresolvable directory before any
37
+ write, call `saveSignature`, log, return the resolved paths.
38
+ 4. **`index.ts`** — import and `eagerTool` register, placed after the `email-setup` block. Guard with
39
+ `refuseNoAccount`, wrap in try/catch like `email-setup`. The handler's local result variable is named
40
+ `message`, not `text`, so it cannot shadow the `text` parameter.
41
+ 5. **`PLUGIN.md`** — `tools:` entry (`riskClass: write_local`, both allowlists false, matching `email-setup`),
42
+ a Config clause in the frontmatter `description:`, a Capabilities bullet, a Skills-table row.
43
+ 6. **`personal-assistant.md`** — add `mcp__plugin_email_email__email-signature-set` after `email-setup`.
44
+ 7. **Regenerate** `canonical-tool-names.generated.ts`.
45
+ 8. **`skills/email-signature/SKILL.md`** — new skill.
46
+ 9. **`skills/email-composition/SKILL.md`** — one added rule, see below.
47
+ 10. **`__tests__/email-signature-set.test.ts`** — committed, not ephemeral: the plugin already carries 34
48
+ test files and the task file mandates a regression test.
49
+
50
+ ## Addition beyond the task file
51
+
52
+ The task specifies that the new `email-signature` skill carries the rule "never hand-build a signature
53
+ inside a draft body". That skill activates when the operator asks to set or change a signature. The
54
+ incident happened while **drafting**, when that skill is not loaded. `grep -in signature` over
55
+ `skills/email-composition/SKILL.md` returns zero hits, so the drafting surface currently says nothing
56
+ about signatures.
57
+
58
+ So the rule is also stated in one sentence in `email-composition`, which is the skill that is loaded while
59
+ drafting. Without it the doctrine lives only in a skill absent at the moment it is needed. Declared in Plan
60
+ Conformity as an addition.
61
+
62
+ ## Verification
63
+
64
+ `npm run build` and `npx vitest run` in `plugins/email/mcp`, then `npm run gen:canonical-tools` and the
65
+ `check-canonical-tool-names.mjs` gate in `platform/`. No browser surface, no Pi hook surface.
@@ -0,0 +1,155 @@
1
+ # Task 2097 — `email-signature-set` design
2
+
3
+ Date 2026-07-28. Lane *Platform / email · signature provisioning*.
4
+ Task file: [`.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md`](../../../../.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md).
5
+
6
+ ## Problem
7
+
8
+ `email-draft` appends an account's signature deterministically, with no model judgement, at
9
+ [`compose.ts:113`](../../../plugins/email/mcp/src/lib/compose.ts). It calls `loadSignature(sigDir)` then
10
+ `applySignature`, where `loadSignature` (Task 1952,
11
+ [`signature.ts:24`](../../../plugins/email/mcp/src/lib/signature.ts)) reads three fixed-convention files at the
12
+ account root: `email-signature.txt`, `email-signature.html`, `email-signature.font`.
13
+
14
+ Nothing in the platform writes those files. Task 1952 shipped the reader with no writer, so the
15
+ path-and-format convention lives only in code and no account can be provisioned. On the G.L. Smith
16
+ account none of the three exist, `applied` was false, no signature was appended, and a drafting
17
+ subagent with no file-read tool reconstructed the HTML from a memory node and pulled a stale logo
18
+ into a client draft.
19
+
20
+ The failure is not a model ignoring a rule. The deterministic path works; it had no files to read.
21
+
22
+ ## Outcome
23
+
24
+ An account's signature is installed into the loader's exact path and format through a sanctioned,
25
+ discoverable tool, and every subsequent draft carries it byte-for-byte with no model-authored
26
+ signature markup.
27
+
28
+ ## Architecture
29
+
30
+ `lib/signature.ts` becomes the single owner of the signature convention in both directions. Read and
31
+ write cannot drift because one file defines the base path, the three extensions, and the font
32
+ trimming for both.
33
+
34
+ | Unit | Responsibility | Depends on |
35
+ |---|---|---|
36
+ | `lib/signature.ts` → `signatureDirFor(accountId)` | Resolve the one directory an account's signature lives in | `attachment-resolve.ts` → `accountDirFor`, `process.env.ACCOUNT_DIR` |
37
+ | `lib/signature.ts` → `saveSignature(accountDir, parts)` | Write present parts, remove absent ones, report both | `node:fs` |
38
+ | `lib/signature.ts` → `loadSignature` / `applySignature` | Unchanged (Task 1952) | — |
39
+ | `tools/email-signature-set.ts` | Param refusals, call `saveSignature`, log, format the response | `lib/signature.ts` |
40
+ | `lib/compose.ts` | Swaps its inline dir expression for `signatureDirFor` | `lib/signature.ts` |
41
+
42
+ ### Directory resolution
43
+
44
+ `compose.ts:113` currently resolves the read directory inline as
45
+ `accountDirFor(accountId) || process.env.ACCOUNT_DIR`. The setter must write to the identical
46
+ directory in every environment, so that expression moves into `signatureDirFor(accountId)` and both
47
+ sides call it. Substituting the identical expression leaves `compose.ts` behaviour unchanged; the
48
+ point is that "which directory" now has one definition rather than two copies that can diverge.
49
+
50
+ Writing to `accountDirFor(accountId)` alone was rejected: on a spawn where `PLATFORM_ROOT` is absent
51
+ but `ACCOUNT_DIR` is set, the setter would refuse while drafts would have read the file fine.
52
+
53
+ ## Behaviour
54
+
55
+ Parameters, all optional strings: `text`, `html`, `font`. The account is the caller's `accountId`,
56
+ the same authority `email-draft` uses. There is no `mailbox` parameter — a signature is account-scoped,
57
+ because `loadSignature` reads one account root and knows nothing about mailboxes.
58
+
59
+ | Input | Effect on `email-signature.<ext>` |
60
+ |---|---|
61
+ | `text` present | `.txt` written byte-for-byte verbatim, no reflow, no re-encoding |
62
+ | `html` present | `.html` written byte-for-byte verbatim |
63
+ | `font` present | `.font` written trimmed, matching `loadSignature`'s trim-on-read |
64
+ | any one absent | that file removed if it exists; absence of the file is not an error |
65
+ | all three absent | refused, nothing on disk is touched |
66
+ | directory unresolvable | refused, nothing on disk is touched |
67
+
68
+ Absent and empty are different inputs. A parameter is absent only when it is not supplied; a supplied
69
+ empty string is present, so it writes an empty file and counts toward the all-three-absent check. That
70
+ follows from writing verbatim: the tool stores what it was given rather than second-guessing it. The
71
+ consequence is that an empty `.txt` makes the next draft report `applied` true while appending
72
+ nothing visible, which is why an empty string is a deliberate act and omission is the way to remove.
73
+
74
+ The removal rule is what makes a text-only call clear a stale `.html` rather than leave it to keep
75
+ appending. The all-three-absent refusal is what stops a mis-shaped call silently wiping a working
76
+ signature; removal stays reachable, but only as a side effect of setting something.
77
+
78
+ The response names the resolved paths written and the resolved paths removed, so the caller sees the
79
+ account root it landed in rather than trusting it.
80
+
81
+ ## Failure handling
82
+
83
+ Files are written in the fixed order `.txt`, `.html`, `.font`. A filesystem error surfaces verbatim
84
+ naming the offending path and the paths already written, so a half-applied state is reported rather
85
+ than hidden. There is no transactional rollback: the task does not ask for one, and reaching the
86
+ partial state requires a filesystem fault mid-call.
87
+
88
+ ## Observability
89
+
90
+ `[email-signature-set] op=write accountId=<first-8> files=<written> removed=<removed>` on every
91
+ successful write. The task specifies the line through `files=`; `removed=` is added because the task
92
+ mandates removal as a behaviour and mandates that provisioning be auditable, and an unlogged removal
93
+ is not auditable.
94
+
95
+ The existing `[email-draft] op=signature applied=<bool> asset=<path>` line stays the per-draft
96
+ signal. An account that has run the setter shows `applied=true` on its next draft. That pair is the
97
+ end-to-end evidence: a write line naming the files, then a draft line reporting them applied.
98
+
99
+ ## Registration
100
+
101
+ `ADMIN_CORE_TOOLS` is named in the task file but does not exist — `grep -rn 'ADMIN_CORE_TOOLS\s*[:=]'`
102
+ over `platform/` returns nothing, and the name survives only in comments. The live surfaces are:
103
+
104
+ 1. `mcp/src/index.ts` — `eagerTool(server, name, description, zodShape, handler)`.
105
+ 2. `PLUGIN.md` — a `tools:` entry (`riskClass: write_local`, `publicAllowlist: false`,
106
+ `adminAllowlist: false`, matching `email-setup`), a clause in the frontmatter `description:` prose,
107
+ an operator-docs bullet, and a skills-table row for the new skill.
108
+ 3. `platform/templates/specialists/agents/personal-assistant.md` — the `tools:` frontmatter grant, the
109
+ canonical per-tool permission surface since Task 453.
110
+
111
+ `riskClass: write_local` is correct because the tool writes account-local files and sends nothing
112
+ outbound, which is the same class `email-setup` carries.
113
+
114
+ ## Skill
115
+
116
+ `platform/plugins/email/skills/email-signature/SKILL.md`, activated when the operator asks to set or
117
+ change an email signature. It states the loader convention, and three standing rules:
118
+
119
+ - A signature is installed only through `email-signature-set`. Hand-writing the three files is not a
120
+ documented path.
121
+ - The agent never hand-builds a signature inside a draft body, because `email-draft` applies the
122
+ stored one automatically and a hand-built one both duplicates it and drifts from the asset.
123
+ - The signature content is read from the operator's own source of truth and passed through verbatim.
124
+ Reconstructing markup from memory or from stripped graph text is what put the stale logo in a
125
+ client draft.
126
+
127
+ Written as outcomes and constraints, not a step recipe, per `maxy-code/CLAUDE.md`.
128
+
129
+ ## Testing
130
+
131
+ Added to the plugin's existing vitest surface at `mcp/src/__tests__/`, not substituted for it.
132
+
133
+ - Round trip: set all three, `loadSignature` returns the exact values, `applySignature` reports
134
+ `applied` true.
135
+ - Verbatim: text and html land byte-identical, trailing newlines and all; font lands trimmed.
136
+ - Text-only call writes `.txt` and removes a pre-existing `.html` and `.font`, so a later draft
137
+ carries no stale HTML.
138
+ - All-three-absent call is refused and leaves pre-existing files untouched.
139
+ - Unresolvable directory is refused.
140
+ - A draft composed after a successful set carries body and html exactly equal to `applySignature`
141
+ output, with no model-authored signature text.
142
+ - `signatureDirFor` prefers `accountDirFor` and falls back to `ACCOUNT_DIR`.
143
+ - A supplied empty string writes an empty file and does not count as absent.
144
+ - Existing `signature.test.ts` stays green.
145
+
146
+ ## Out of scope
147
+
148
+ - Making `applied=false` a fail-closed refusal on client-facing drafts. Operator scoped this out on
149
+ 2026-07-28 as separate hardening; deliberately not filed.
150
+ - The one-off G.L. Smith provisioning and the stale memory-node deletion. That is a runtime action on
151
+ the live install, performed through the tool this task ships.
152
+ - Any change to `loadSignature` / `applySignature` behaviour (Task 1952). `signatureDirFor` moves an
153
+ expression out of `compose.ts`; neither function's behaviour changes.
154
+ - The drafts list/verify capability, which is
155
+ [Task 2098](../../../../.tasks/pending/2098-email-drafts-list-verify-tool-so-draft-presence-is-confirmable.md).