@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
@@ -12,12 +12,27 @@ import {
12
12
  } from '../../skills/data-portal/template/functions/api/_lib/authorize'
13
13
  import type { D1Database } from '../../skills/data-portal/template/functions/api/_lib/types'
14
14
 
15
- // Models the real resolveSession query: sessions JOIN people, requiring the
16
- // session's pcCheck to still match substr(people.hash, 1, 16). A fake that
17
- // skipped the join would let the revocation tests below pass vacuously.
15
+ // The two tenants every cross-account case below is written against. The store
16
+ // is shared, so these are rows in ONE database, not two databases.
17
+ const ACC = 'acct-a'
18
+ const OTHER = 'acct-b'
19
+
20
+ // Models the real resolveSession query: the session row is scoped by account,
21
+ // and it JOINs people on BOTH ownerId and accountId, requiring the session's
22
+ // pcCheck to still match substr(people.hash, 1, 16). A fake that joined on
23
+ // ownerId alone would let the revocation AND the cross-tenant tests below pass
24
+ // vacuously.
18
25
  function fakeDb(
19
- seed: Array<{ sessionId: string; ownerId: string; pcCheck: string; expiresAt: number }> = [],
20
- people: Array<{ ownerId: string; hash: string }> = [{ ownerId: 'alice', hash: 'HASH-v1-padded00' }],
26
+ seed: Array<{
27
+ sessionId: string
28
+ ownerId: string
29
+ accountId: string
30
+ pcCheck: string
31
+ expiresAt: number
32
+ }> = [],
33
+ people: Array<{ ownerId: string; accountId: string; hash: string }> = [
34
+ { ownerId: 'alice', accountId: ACC, hash: 'HASH-v1-padded00' },
35
+ ],
21
36
  ) {
22
37
  const rows = [...seed]
23
38
  const peopleRows = [...people]
@@ -31,13 +46,25 @@ function fakeDb(
31
46
  },
32
47
  async run() {
33
48
  if (/INSERT INTO sessions/i.test(query)) {
34
- const [sessionId, ownerId, pcCheck, expiresAt] = bound as [string, string, string, number]
35
- rows.push({ sessionId, ownerId, pcCheck, expiresAt })
49
+ const [sessionId, ownerId, accountId, pcCheck, expiresAt] = bound as [
50
+ string,
51
+ string,
52
+ string,
53
+ string,
54
+ number,
55
+ ]
56
+ rows.push({ sessionId, ownerId, accountId, pcCheck, expiresAt })
36
57
  }
37
58
  if (/DELETE FROM sessions/i.test(query)) {
38
- const [ownerId, nowMs] = bound as [string, number]
59
+ const [ownerId, accountId, nowMs] = bound as [string, string, number]
39
60
  for (let i = rows.length - 1; i >= 0; i--) {
40
- if (rows[i].ownerId === ownerId && rows[i].expiresAt <= nowMs) rows.splice(i, 1)
61
+ if (
62
+ rows[i].ownerId === ownerId &&
63
+ rows[i].accountId === accountId &&
64
+ rows[i].expiresAt <= nowMs
65
+ ) {
66
+ rows.splice(i, 1)
67
+ }
41
68
  }
42
69
  }
43
70
  return { meta: { changes: 1 } }
@@ -46,13 +73,17 @@ function fakeDb(
46
73
  return { results: [] }
47
74
  },
48
75
  async first<T>() {
49
- const [sessionId, nowMs] = bound as [string, number]
50
- const hit = rows.find((r) => r.sessionId === sessionId && r.expiresAt > nowMs)
76
+ const [sessionId, accountId, nowMs] = bound as [string, string, number]
77
+ const hit = rows.find(
78
+ (r) => r.sessionId === sessionId && r.accountId === accountId && r.expiresAt > nowMs,
79
+ )
51
80
  if (!hit) return null as T | null
52
- const person = peopleRows.find((p) => p.ownerId === hit.ownerId)
81
+ const person = peopleRows.find(
82
+ (p) => p.ownerId === hit.ownerId && p.accountId === hit.accountId,
83
+ )
53
84
  if (!person) return null as T | null // unenrolled -> the JOIN drops it
54
85
  if (person.hash.slice(0, 16) !== hit.pcCheck) return null as T | null // rotated
55
- return { ownerId: hit.ownerId } as T
86
+ return { ownerId: hit.ownerId, accountId: person.accountId } as T
56
87
  },
57
88
  }
58
89
  return stmt
@@ -66,28 +97,82 @@ const PC = 'HASH-v1-padded00'.slice(0, 16)
66
97
  describe('sessions', () => {
67
98
  it('mints a session resolvable to its owner', async () => {
68
99
  const { db } = fakeDb()
69
- await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
70
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '', folders: [] })
100
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
101
+ expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toEqual({
102
+ ownerId: 'alice',
103
+ accountId: ACC,
104
+ folders: [],
105
+ })
71
106
  })
72
107
 
73
108
  it('does not resolve an expired session', async () => {
74
109
  const { db } = fakeDb()
75
- await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
76
- expect(await resolveSession(db, 'sess-1', 1_000_000 + SESSION_TTL_MS + 1)).toBeNull()
110
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
111
+ expect(await resolveSession(db, 'sess-1', ACC, 1_000_000 + SESSION_TTL_MS + 1)).toBeNull()
77
112
  })
78
113
 
79
114
  it('does not resolve an unknown session', async () => {
80
115
  const { db } = fakeDb()
81
- expect(await resolveSession(db, 'nope', 1_000_000)).toBeNull()
116
+ expect(await resolveSession(db, 'nope', ACC, 1_000_000)).toBeNull()
82
117
  })
83
118
 
84
119
  it('prunes that owner expired rows on mint, so sessions do not grow forever', async () => {
85
120
  const { db, rows } = fakeDb([
86
- { sessionId: 'old', ownerId: 'alice', pcCheck: PC, expiresAt: 1 },
121
+ { sessionId: 'old', ownerId: 'alice', accountId: ACC, pcCheck: PC, expiresAt: 1 },
87
122
  ])
88
- await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
123
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
89
124
  expect(rows.map((r) => r.sessionId)).toEqual(['sess-1'])
90
125
  })
126
+
127
+ it('prunes only its own tenant expired rows', async () => {
128
+ // The GC deletes by owner, and on a shared store two tenants may hold the
129
+ // same owner id. Unscoped, one portal's routine mint would silently delete
130
+ // another portal's session rows.
131
+ const { db, rows } = fakeDb([
132
+ { sessionId: 'mine', ownerId: 'alice', accountId: ACC, pcCheck: PC, expiresAt: 1 },
133
+ { sessionId: 'theirs', ownerId: 'alice', accountId: OTHER, pcCheck: PC, expiresAt: 1 },
134
+ ])
135
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
136
+ expect(rows.map((r) => r.sessionId).sort()).toEqual(['sess-1', 'theirs'])
137
+ })
138
+ })
139
+
140
+ describe('cross-tenant sessions — the shared-store property', () => {
141
+ // Two clients each enrol someone called 'alice'. Nothing but accountId tells
142
+ // them apart, so every one of these would have been a real leak under the
143
+ // per-owner join the pre-2099 template used.
144
+ function twoTenants() {
145
+ return fakeDb(
146
+ [],
147
+ [
148
+ { ownerId: 'alice', accountId: ACC, hash: 'HASH-v1-padded00' },
149
+ { ownerId: 'alice', accountId: OTHER, hash: 'OTHER-v1-pad0000' },
150
+ ],
151
+ )
152
+ }
153
+
154
+ it('does not resolve another tenant session id', async () => {
155
+ const { db } = twoTenants()
156
+ await mintSession(db, 'alice', OTHER, 'sess-other', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
157
+ expect(await resolveSession(db, 'sess-other', ACC, 1_000_000)).toBeNull()
158
+ })
159
+
160
+ it('resolves each tenant own session to its own account', async () => {
161
+ const { db } = twoTenants()
162
+ await mintSession(db, 'alice', ACC, 'sess-a', 1_000_000, PC)
163
+ await mintSession(db, 'alice', OTHER, 'sess-b', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
164
+ expect((await resolveSession(db, 'sess-a', ACC, 1_000_000))?.accountId).toBe(ACC)
165
+ expect((await resolveSession(db, 'sess-b', OTHER, 1_000_000))?.accountId).toBe(OTHER)
166
+ })
167
+
168
+ it('does not let one tenant passcode rotation revoke the other sessions', async () => {
169
+ const { db, peopleRows } = twoTenants()
170
+ await mintSession(db, 'alice', ACC, 'sess-a', 1_000_000, PC)
171
+ await mintSession(db, 'alice', OTHER, 'sess-b', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
172
+ peopleRows[1].hash = 'OTHER-v2-rotated' // the other client rotates their alice
173
+ expect(await resolveSession(db, 'sess-a', ACC, 1_000_000)).not.toBeNull()
174
+ expect(await resolveSession(db, 'sess-b', OTHER, 1_000_000)).toBeNull()
175
+ })
91
176
  })
92
177
 
93
178
  describe('session revocation — the property the design claims', () => {
@@ -96,19 +181,23 @@ describe('session revocation — the property the design claims', () => {
96
181
  // people.hash, so pcCheck no longer matches and the live session dies —
97
182
  // by construction, not by remembering to delete anything.
98
183
  const { db, peopleRows } = fakeDb()
99
- await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
100
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '', folders: [] })
184
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
185
+ expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toEqual({
186
+ ownerId: 'alice',
187
+ accountId: ACC,
188
+ folders: [],
189
+ })
101
190
 
102
191
  peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
103
192
 
104
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toBeNull()
193
+ expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toBeNull()
105
194
  })
106
195
 
107
196
  it('stops resolving once the person is unenrolled', async () => {
108
197
  const { db, peopleRows } = fakeDb()
109
- await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
198
+ await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
110
199
  peopleRows.length = 0
111
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toBeNull()
200
+ expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toBeNull()
112
201
  })
113
202
  })
114
203
 
@@ -144,34 +233,34 @@ describe('readSessionCookie', () => {
144
233
  })
145
234
 
146
235
  describe('authorizeKey — the isolation rule', () => {
147
- it('allows a key under the owner prefix', () => {
148
- expect(authorizeKey('alice', 'alice/invoice.pdf')).toBe(true)
236
+ it('allows a key under the account and owner prefix', () => {
237
+ expect(authorizeKey(ACC, 'alice', `${ACC}/alice/invoice.pdf`)).toBe(true)
149
238
  })
150
239
 
151
240
  it('denies another owner prefix', () => {
152
- expect(authorizeKey('alice', 'bob/invoice.pdf')).toBe(false)
241
+ expect(authorizeKey(ACC, 'alice', `${ACC}/bob/invoice.pdf`)).toBe(false)
153
242
  })
154
243
 
155
244
  it('denies a traversal escape', () => {
156
- expect(authorizeKey('alice', 'alice/../bob/invoice.pdf')).toBe(false)
245
+ expect(authorizeKey(ACC, 'alice', `${ACC}/alice/../bob/invoice.pdf`)).toBe(false)
157
246
  })
158
247
 
159
248
  it('denies a prefix that merely starts with the owner id', () => {
160
249
  // 'alice' must not authorize 'alice-evil/...': a startsWith check on the
161
250
  // bare id would pass this. The separator is part of the rule.
162
- expect(authorizeKey('alice', 'alice-evil/invoice.pdf')).toBe(false)
251
+ expect(authorizeKey(ACC, 'alice', `${ACC}/alice-evil/invoice.pdf`)).toBe(false)
163
252
  })
164
253
 
165
254
  it('denies a bare key with no prefix', () => {
166
- expect(authorizeKey('alice', 'invoice.pdf')).toBe(false)
255
+ expect(authorizeKey(ACC, 'alice', 'invoice.pdf')).toBe(false)
167
256
  })
168
257
 
169
258
  it('denies an absolute-looking key', () => {
170
- expect(authorizeKey('alice', '/alice/invoice.pdf')).toBe(false)
259
+ expect(authorizeKey(ACC, 'alice', `/${ACC}/alice/invoice.pdf`)).toBe(false)
171
260
  })
172
261
 
173
262
  it('denies an empty key', () => {
174
- expect(authorizeKey('alice', '')).toBe(false)
263
+ expect(authorizeKey(ACC, 'alice', '')).toBe(false)
175
264
  })
176
265
 
177
266
  // authorizeKey's correctness silently depends on ownerId containing no
@@ -180,22 +269,49 @@ describe('authorizeKey — the isolation rule', () => {
180
269
  // person's files. Enrolment is an agent writing rows, so the invariant
181
270
  // cannot rest on the skill's prose — enforce it at the gate.
182
271
  it('denies when the ownerId itself contains a separator', () => {
183
- expect(authorizeKey('alice/2024', 'alice/2024/invoice.pdf')).toBe(false)
272
+ expect(authorizeKey(ACC, 'alice/2024', `${ACC}/alice/2024/invoice.pdf`)).toBe(false)
184
273
  })
185
274
 
186
275
  it('denies an ownerId outside [a-z0-9-], failing closed', () => {
187
276
  for (const bad of ['alice/2024', 'Alice', 'alice..', 'alice bob', '../alice', 'alice%2f']) {
188
- expect(authorizeKey(bad, `${bad}/invoice.pdf`)).toBe(false)
277
+ expect(authorizeKey(ACC, bad, `${ACC}/${bad}/invoice.pdf`)).toBe(false)
189
278
  }
190
279
  })
191
280
 
192
281
  it('still allows a well-formed ownerId', () => {
193
- expect(authorizeKey('alice-smith-2', 'alice-smith-2/invoice.pdf')).toBe(true)
282
+ expect(authorizeKey(ACC, 'alice-smith-2', `${ACC}/alice-smith-2/invoice.pdf`)).toBe(true)
283
+ })
284
+ })
285
+
286
+ describe('authorizeKey — the tenant half of the rule', () => {
287
+ // The store is shared, so the account segment is the only thing separating
288
+ // one client's objects from another's. Every key on the delete, download and
289
+ // multipart paths arrives from the client, so this is checked, never assumed.
290
+ it('denies another tenant prefix even with the right owner', () => {
291
+ expect(authorizeKey(ACC, 'alice', `${OTHER}/alice/invoice.pdf`)).toBe(false)
292
+ })
293
+
294
+ it('denies a bare owner prefix that omits the account', () => {
295
+ expect(authorizeKey(ACC, 'alice', 'alice/invoice.pdf')).toBe(false)
296
+ })
297
+
298
+ it('denies a tenant that merely starts with the account id', () => {
299
+ expect(authorizeKey(ACC, 'alice', `${ACC}-evil/alice/invoice.pdf`)).toBe(false)
300
+ })
301
+
302
+ it('denies an accountId outside [a-z0-9-], failing closed', () => {
303
+ for (const bad of ['acct/a', 'Acct', 'acct..', 'acct a', '../acct']) {
304
+ expect(authorizeKey(bad, 'alice', `${bad}/alice/invoice.pdf`)).toBe(false)
305
+ }
306
+ })
307
+
308
+ it('denies an empty accountId rather than degrading to an owner-only rule', () => {
309
+ expect(authorizeKey('', 'alice', 'alice/invoice.pdf')).toBe(false)
194
310
  })
195
311
  })
196
312
 
197
313
  describe('ownerPrefix', () => {
198
- it('is the owner id plus a separator', () => {
199
- expect(ownerPrefix('alice')).toBe('alice/')
314
+ it('is the account and the owner id, each with a separator', () => {
315
+ expect(ownerPrefix(ACC, 'alice')).toBe('acct-a/alice/')
200
316
  })
201
317
  })
@@ -4,18 +4,36 @@ import { processDownload } from '../../skills/data-portal/template/functions/api
4
4
  import { processDelete } from '../../skills/data-portal/template/functions/api/delete'
5
5
  import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
6
6
 
7
- // alice's session is 'sess-a'; bob owns 'bob/secret.pdf'.
7
+ // The portal is account 'acct-a'; alice's session is 'sess-a'; bob owns
8
+ // 'acct-a/bob/secret.pdf'. The store is shared by every portal (Task 2099), so
9
+ // the account is the first key segment and a column on every row, and the fake
10
+ // below filters on it exactly as the real queries do.
8
11
  function makeEnv() {
9
12
  const manifest = [
10
- { fileId: 'f1', ownerId: 'alice', filename: 'a.pdf', objectKey: 'alice/a.pdf', size: 10 },
11
- { fileId: 'f2', ownerId: 'bob', filename: 'secret.pdf', objectKey: 'bob/secret.pdf', size: 20 },
13
+ {
14
+ fileId: 'f1',
15
+ accountId: 'acct-a',
16
+ ownerId: 'alice',
17
+ filename: 'a.pdf',
18
+ objectKey: 'acct-a/alice/a.pdf',
19
+ size: 10,
20
+ },
21
+ {
22
+ fileId: 'f2',
23
+ accountId: 'acct-a',
24
+ ownerId: 'bob',
25
+ filename: 'secret.pdf',
26
+ objectKey: 'acct-a/bob/secret.pdf',
27
+ size: 20,
28
+ },
12
29
  ]
13
30
  const objects = new Map<string, string>([
14
- ['alice/a.pdf', 'A'],
15
- ['bob/secret.pdf', 'B'],
31
+ ['acct-a/alice/a.pdf', 'A'],
32
+ ['acct-a/bob/secret.pdf', 'B'],
16
33
  ])
17
34
  const deleted: string[] = []
18
35
  const env = {
36
+ PORTAL_ACCOUNT_ID: 'acct-a',
19
37
  DB: {
20
38
  prepare(query: string) {
21
39
  let bound: unknown[] = []
@@ -25,23 +43,48 @@ function makeEnv() {
25
43
  return stmt
26
44
  },
27
45
  async run() {
46
+ // Every single-row manifest statement binds (objectKey, accountId):
47
+ // objectKey is unique per account, not globally.
28
48
  if (/DELETE FROM manifest/i.test(query)) {
29
- const i = manifest.findIndex((m) => m.objectKey === bound[0])
49
+ const i = manifest.findIndex(
50
+ (m) => m.objectKey === bound[0] && m.accountId === bound[1],
51
+ )
30
52
  if (i >= 0) manifest.splice(i, 1)
31
53
  }
32
54
  return { meta: { changes: 1 } }
33
55
  },
34
56
  async all<T>() {
35
- const owner = bound[0] as string
36
- return { results: manifest.filter((m) => m.ownerId === owner) as T[] }
57
+ // The account's published folder index is empty in these fixtures:
58
+ // this file covers upload rows and key isolation, not the tree.
59
+ if (/FROM directory/i.test(query)) {
60
+ return { results: [] as T[] }
61
+ }
62
+ // The manifest list binds (accountId, ownerId): the owner says
63
+ // which files, the account whose.
64
+ const [account, owner] = bound as string[]
65
+ return {
66
+ results: manifest.filter(
67
+ (m) => m.accountId === account && m.ownerId === owner,
68
+ ) as T[],
69
+ }
37
70
  },
38
71
  async first<T>() {
39
72
  if (/FROM sessions/i.test(query)) {
40
- const map: Record<string, string> = { 'sess-a': 'alice', 'sess-b': 'bob' }
41
- const owner = map[bound[0] as string]
42
- return (owner ? { ownerId: owner } : null) as T | null
73
+ // Bound (sessionId, accountId, nowMs). The join is on BOTH the
74
+ // session's account and the people row's, so a session resolves
75
+ // only against its own portal's account and the resolved
76
+ // people.accountId has to come back matching.
77
+ const map: Record<string, { ownerId: string; accountId: string }> = {
78
+ 'sess-a': { ownerId: 'alice', accountId: 'acct-a' },
79
+ 'sess-b': { ownerId: 'bob', accountId: 'acct-a' },
80
+ }
81
+ const row = map[bound[0] as string]
82
+ return (row && row.accountId === bound[1]
83
+ ? { ownerId: row.ownerId, accountId: row.accountId, folders: '' }
84
+ : null) as T | null
43
85
  }
44
- return (manifest.find((m) => m.objectKey === bound[0]) ?? null) as T | null
86
+ return (manifest.find((m) => m.objectKey === bound[0] && m.accountId === bound[1]) ??
87
+ null) as T | null
45
88
  },
46
89
  }
47
90
  return stmt
@@ -74,7 +117,7 @@ describe('processFiles', () => {
74
117
  const res = await processFiles('sess-a', env, () => {}, 1_000_000)
75
118
  expect(res.status).toBe(200)
76
119
  const files = res.payload.files as Array<{ objectKey: string }>
77
- expect(files.map((f) => f.objectKey)).toEqual(['alice/a.pdf'])
120
+ expect(files.map((f) => f.objectKey)).toEqual(['acct-a/alice/a.pdf'])
78
121
  })
79
122
 
80
123
  it("never includes another owner's file", async () => {
@@ -93,56 +136,72 @@ describe('processFiles', () => {
93
136
  describe('processDownload — isolation', () => {
94
137
  it("allows the owner's own key", async () => {
95
138
  const { env } = makeEnv()
96
- expect((await processDownload('sess-a', 'alice/a.pdf', env, () => {}, 1_000_000)).status).toBe(
97
- 200,
98
- )
139
+ expect(
140
+ (await processDownload('sess-a', 'acct-a/alice/a.pdf', env, () => {}, 1_000_000)).status,
141
+ ).toBe(200)
99
142
  })
100
143
 
101
144
  it("denies person A's session on person B's key", async () => {
102
145
  const { env } = makeEnv()
103
- const res = await processDownload('sess-a', 'bob/secret.pdf', env, () => {}, 1_000_000)
146
+ const res = await processDownload('sess-a', 'acct-a/bob/secret.pdf', env, () => {}, 1_000_000)
104
147
  expect(res.status).toBe(403)
105
148
  })
106
149
 
107
150
  it('denies a traversal escape', async () => {
108
151
  const { env } = makeEnv()
109
- const res = await processDownload('sess-a', 'alice/../bob/secret.pdf', env, () => {}, 1_000_000)
152
+ const res = await processDownload(
153
+ 'sess-a',
154
+ 'acct-a/alice/../bob/secret.pdf',
155
+ env,
156
+ () => {},
157
+ 1_000_000,
158
+ )
110
159
  expect(res.status).toBe(403)
111
160
  })
112
161
 
113
162
  it('denies an unauthenticated caller', async () => {
114
163
  const { env } = makeEnv()
115
- expect((await processDownload('', 'alice/a.pdf', env, () => {}, 1_000_000)).status).toBe(401)
164
+ expect((await processDownload('', 'acct-a/alice/a.pdf', env, () => {}, 1_000_000)).status).toBe(
165
+ 401,
166
+ )
116
167
  })
117
168
  })
118
169
 
119
170
  describe('processDelete — isolation and both-sides', () => {
120
171
  it('removes object and manifest row together', async () => {
121
172
  const { env, deleted, manifest } = makeEnv()
122
- const res = await processDelete('sess-a', 'alice/a.pdf', env, () => {}, () => 1_000_000)
173
+ const res = await processDelete('sess-a', 'acct-a/alice/a.pdf', env, () => {}, () => 1_000_000)
123
174
  expect(res.status).toBe(200)
124
- expect(deleted).toEqual(['alice/a.pdf'])
125
- expect(manifest.find((m) => m.objectKey === 'alice/a.pdf')).toBeUndefined()
175
+ expect(deleted).toEqual(['acct-a/alice/a.pdf'])
176
+ expect(manifest.find((m) => m.objectKey === 'acct-a/alice/a.pdf')).toBeUndefined()
126
177
  })
127
178
 
128
179
  it("denies person A's session on person B's key, and deletes nothing", async () => {
129
180
  const { env, deleted, manifest } = makeEnv()
130
- const res = await processDelete('sess-a', 'bob/secret.pdf', env, () => {}, () => 1_000_000)
181
+ const res = await processDelete(
182
+ 'sess-a',
183
+ 'acct-a/bob/secret.pdf',
184
+ env,
185
+ () => {},
186
+ () => 1_000_000,
187
+ )
131
188
  expect(res.status).toBe(403)
132
189
  expect(deleted).toEqual([])
133
- expect(manifest.find((m) => m.objectKey === 'bob/secret.pdf')).toBeDefined()
190
+ expect(manifest.find((m) => m.objectKey === 'acct-a/bob/secret.pdf')).toBeDefined()
134
191
  })
135
192
 
136
193
  it('denies an unauthenticated caller', async () => {
137
194
  const { env, deleted } = makeEnv()
138
- expect((await processDelete('', 'alice/a.pdf', env, () => {}, () => 1_000_000)).status).toBe(401)
195
+ expect(
196
+ (await processDelete('', 'acct-a/alice/a.pdf', env, () => {}, () => 1_000_000)).status,
197
+ ).toBe(401)
139
198
  expect(deleted).toEqual([])
140
199
  })
141
200
 
142
201
  it('emits a per-step lifeline, not one bare line', async () => {
143
202
  const { env } = makeEnv()
144
203
  const lines: string[] = []
145
- await processDelete('sess-a', 'alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
204
+ await processDelete('sess-a', 'acct-a/alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
146
205
  const joined = lines.join('\n')
147
206
  expect(joined).toContain('op=r2-delete')
148
207
  expect(joined).toContain('op=manifest-delete')
@@ -157,12 +216,18 @@ describe('processDelete — isolation and both-sides', () => {
157
216
  throw new Error('r2 unavailable')
158
217
  }
159
218
  const lines: string[] = []
160
- const res = await processDelete('sess-a', 'alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
219
+ const res = await processDelete(
220
+ 'sess-a',
221
+ 'acct-a/alice/a.pdf',
222
+ env,
223
+ (l) => lines.push(l),
224
+ () => 1_000_000,
225
+ )
161
226
  expect(res.status).toBe(502)
162
227
  expect(lines.join('\n')).toContain('op=r2-delete')
163
228
  expect(lines.join('\n')).toContain('result=failed')
164
229
  // The row must survive an object that was never deleted.
165
- expect(manifest.find((m) => m.objectKey === 'alice/a.pdf')).toBeDefined()
230
+ expect(manifest.find((m) => m.objectKey === 'acct-a/alice/a.pdf')).toBeDefined()
166
231
  })
167
232
 
168
233
  it('derives result from the post-condition rather than hardcoding ok', async () => {
@@ -171,7 +236,13 @@ describe('processDelete — isolation and both-sides', () => {
171
236
  const { env } = makeEnv()
172
237
  ;(env.BUCKET as unknown as { delete: () => Promise<void> }).delete = async () => {}
173
238
  const lines: string[] = []
174
- const res = await processDelete('sess-a', 'alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
239
+ const res = await processDelete(
240
+ 'sess-a',
241
+ 'acct-a/alice/a.pdf',
242
+ env,
243
+ (l) => lines.push(l),
244
+ () => 1_000_000,
245
+ )
175
246
  expect(res.status).toBe(500)
176
247
  expect(lines.join('\n')).toMatch(/op=complete .*result=incomplete objectPresent=true/)
177
248
  })