@rubytech/create-sitedesk-code 0.1.519 → 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 (289) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +65 -0
  3. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
  4. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  5. package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
  6. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
  7. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
  8. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
  9. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
  10. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
  11. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
  12. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
  13. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
  14. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
  15. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
  16. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
  17. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
  18. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
  19. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
  20. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
  24. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
  25. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
  26. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
  27. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
  28. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
  30. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
  37. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  38. package/payload/platform/plugins/email/PLUGIN.md +20 -1
  39. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
  40. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
  41. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
  42. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
  43. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
  44. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
  46. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
  47. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
  48. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
  49. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
  50. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
  51. package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
  52. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  53. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
  54. package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
  55. package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
  56. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -0
  57. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  58. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +76 -0
  59. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  60. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
  61. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
  62. package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
  63. package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
  64. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
  65. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
  66. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
  67. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
  68. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
  69. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
  70. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
  71. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
  72. package/payload/platform/plugins/email/references/email-reference.md +14 -2
  73. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
  74. package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
  75. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
  76. package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
  77. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
  80. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
  81. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
  82. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
  84. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
  85. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
  86. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
  87. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  88. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
  91. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
  92. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
  94. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
  95. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
  98. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
  99. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
  100. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
  101. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
  102. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
  103. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
  104. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
  105. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
  106. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
  107. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
  108. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  109. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
  110. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  111. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
  112. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
  113. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
  114. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
  115. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  116. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
  117. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  118. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  119. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  120. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  121. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  122. package/payload/server/public/activity.html +5 -5
  123. package/payload/server/public/agents.html +4 -4
  124. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
  125. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
  126. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
  127. package/payload/server/public/assets/{AdminShell-Bpo_wgxm.js → AdminShell-Djg6ndne.js} +1 -1
  128. package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
  129. package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
  130. package/payload/server/public/assets/{activity-DOsfKzT3.js → activity-sqbyX0pX.js} +1 -1
  131. package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
  132. package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
  133. package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
  134. package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
  135. package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
  136. package/payload/server/public/assets/{agents-DqDnZTH_.js → agents-BFYM5EDA.js} +1 -1
  137. package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
  138. package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
  139. package/payload/server/public/assets/{browser-FL17Dfp0.js → browser-DP5GZe4G.js} +1 -1
  140. package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
  141. package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
  142. package/payload/server/public/assets/{calendar-CLfLvsnV.js → calendar-BKaemLDG.js} +1 -1
  143. package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
  144. package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
  145. package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
  146. package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
  147. package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
  148. package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
  149. package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
  150. package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
  151. package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
  152. package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
  153. package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
  154. package/payload/server/public/assets/data-CfjttTvR.js +1 -0
  155. package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
  156. package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
  157. package/payload/server/public/assets/{file-text-DNm0AzUz.js → file-text-BKK7R-k_.js} +1 -1
  158. package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
  159. package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
  160. package/payload/server/public/assets/{graph-fAvmlVzh.js → graph-FuxYySjK.js} +1 -1
  161. package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
  162. package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
  163. package/payload/server/public/assets/{graph-labels-CwdkCxfs.js → graph-labels-Cip6fmKK.js} +1 -1
  164. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
  165. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
  166. package/payload/server/public/assets/{maximize-2-B6tAPhC2.js → maximize-2-D79AhVDo.js} +1 -1
  167. package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
  168. package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
  169. package/payload/server/public/assets/{operator-CTKvBVjk.js → operator-BPAfgFhP.js} +1 -1
  170. package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
  171. package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
  172. package/payload/server/public/assets/{page-BWoNW3sa.js → page--78CbdFs.js} +1 -1
  173. package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
  174. package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
  175. package/payload/server/public/assets/{page-O5HkHQw4.js → page-BqEyS0W4.js} +1 -1
  176. package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
  177. package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
  178. package/payload/server/public/assets/{public-BF1rnVPa.js → public-D1xs3FzD.js} +1 -1
  179. package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
  180. package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
  181. package/payload/server/public/assets/{rotate-ccw-B8TqfCy4.js → rotate-ccw-CcU_XD8B.js} +1 -1
  182. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
  183. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
  184. package/payload/server/public/assets/{routines-4R7BLZyH.js → routines-CA7D0myr.js} +1 -1
  185. package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
  186. package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
  187. package/payload/server/public/assets/{skills-CQ9Qs2tg.js → skills-CfVPPSxO.js} +1 -1
  188. package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
  189. package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
  190. package/payload/server/public/assets/{tasks-DePyOrRk.js → tasks-DgieyZqz.js} +1 -1
  191. package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
  192. package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
  193. package/payload/server/public/assets/{time-entry-format-D9gtnG01.js → time-entry-format-IFqinQJj.js} +1 -1
  194. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
  195. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
  196. package/payload/server/public/assets/{triangle-alert-DAtm2mLL.js → triangle-alert-C5vH__sq.js} +1 -1
  197. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
  198. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
  199. package/payload/server/public/assets/{useCopyFeedback-uMJT-p6i.js → useCopyFeedback-BeL2gPl6.js} +1 -1
  200. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
  201. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
  202. package/payload/server/public/assets/{useSubAccountSwitcher-iCzsV96x.css → useSubAccountSwitcher-DLqUQzOl.css} +1 -1
  203. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
  204. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
  205. package/payload/server/public/assets/{useVoiceRecorder-BFrPC-s7.js → useVoiceRecorder-Cpw3aOSt.js} +1 -1
  206. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
  207. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
  208. package/payload/server/public/assets/{wrench-CYgaN7K9.js → wrench-BBEup3hd.js} +1 -1
  209. package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
  210. package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
  211. package/payload/server/public/browser.html +4 -4
  212. package/payload/server/public/calendar.html +7 -7
  213. package/payload/server/public/chat.html +13 -13
  214. package/payload/server/public/data.html +11 -11
  215. package/payload/server/public/graph.html +9 -9
  216. package/payload/server/public/index.html +14 -14
  217. package/payload/server/public/operator.html +14 -14
  218. package/payload/server/public/public.html +13 -13
  219. package/payload/server/public/routines.html +6 -6
  220. package/payload/server/public/skills.html +5 -5
  221. package/payload/server/public/tasks.html +6 -6
  222. package/payload/server/server.js +454 -352
  223. package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js +0 -1
  224. package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.br +0 -0
  225. package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.gz +0 -0
  226. package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.br +0 -0
  227. package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.gz +0 -0
  228. package/payload/server/public/assets/activity-DOsfKzT3.js.br +0 -0
  229. package/payload/server/public/assets/activity-DOsfKzT3.js.gz +0 -0
  230. package/payload/server/public/assets/admin-COgD1z5a.js +0 -1
  231. package/payload/server/public/assets/admin-COgD1z5a.js.br +0 -0
  232. package/payload/server/public/assets/admin-COgD1z5a.js.gz +0 -0
  233. package/payload/server/public/assets/agents-DqDnZTH_.js.br +0 -0
  234. package/payload/server/public/assets/agents-DqDnZTH_.js.gz +0 -0
  235. package/payload/server/public/assets/browser-FL17Dfp0.js.br +0 -0
  236. package/payload/server/public/assets/browser-FL17Dfp0.js.gz +0 -0
  237. package/payload/server/public/assets/calendar-CLfLvsnV.js.br +0 -0
  238. package/payload/server/public/assets/calendar-CLfLvsnV.js.gz +0 -0
  239. package/payload/server/public/assets/chat-DYq4FxMG.js +0 -1
  240. package/payload/server/public/assets/chat-DYq4FxMG.js.br +0 -0
  241. package/payload/server/public/assets/chat-DYq4FxMG.js.gz +0 -0
  242. package/payload/server/public/assets/chevron-left-DWt5cdUo.js +0 -1
  243. package/payload/server/public/assets/chevron-right-B9suVrYy.js +0 -1
  244. package/payload/server/public/assets/chevron-right-B9suVrYy.js.br +0 -0
  245. package/payload/server/public/assets/clock-DW6qVXtv.js +0 -1
  246. package/payload/server/public/assets/clock-DW6qVXtv.js.br +0 -0
  247. package/payload/server/public/assets/clock-DW6qVXtv.js.gz +0 -0
  248. package/payload/server/public/assets/data-C8rbOgds.js +0 -1
  249. package/payload/server/public/assets/data-C8rbOgds.js.br +0 -0
  250. package/payload/server/public/assets/data-C8rbOgds.js.gz +0 -0
  251. package/payload/server/public/assets/file-text-DNm0AzUz.js.br +0 -0
  252. package/payload/server/public/assets/file-text-DNm0AzUz.js.gz +0 -0
  253. package/payload/server/public/assets/graph-fAvmlVzh.js.br +0 -0
  254. package/payload/server/public/assets/graph-fAvmlVzh.js.gz +0 -0
  255. package/payload/server/public/assets/graph-labels-CwdkCxfs.js.br +0 -0
  256. package/payload/server/public/assets/graph-labels-CwdkCxfs.js.gz +0 -0
  257. package/payload/server/public/assets/maximize-2-B6tAPhC2.js.br +0 -0
  258. package/payload/server/public/assets/maximize-2-B6tAPhC2.js.gz +0 -0
  259. package/payload/server/public/assets/operator-CTKvBVjk.js.br +0 -0
  260. package/payload/server/public/assets/operator-CTKvBVjk.js.gz +0 -0
  261. package/payload/server/public/assets/page-BWoNW3sa.js.br +0 -0
  262. package/payload/server/public/assets/page-BWoNW3sa.js.gz +0 -0
  263. package/payload/server/public/assets/page-O5HkHQw4.js.br +0 -0
  264. package/payload/server/public/assets/page-O5HkHQw4.js.gz +0 -0
  265. package/payload/server/public/assets/public-BF1rnVPa.js.br +0 -0
  266. package/payload/server/public/assets/public-BF1rnVPa.js.gz +0 -0
  267. package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.br +0 -0
  268. package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.gz +0 -0
  269. package/payload/server/public/assets/routines-4R7BLZyH.js.br +0 -0
  270. package/payload/server/public/assets/routines-4R7BLZyH.js.gz +0 -0
  271. package/payload/server/public/assets/skills-CQ9Qs2tg.js.br +0 -0
  272. package/payload/server/public/assets/skills-CQ9Qs2tg.js.gz +0 -0
  273. package/payload/server/public/assets/tasks-DePyOrRk.js.br +0 -0
  274. package/payload/server/public/assets/tasks-DePyOrRk.js.gz +0 -0
  275. package/payload/server/public/assets/time-entry-format-D9gtnG01.js.br +0 -0
  276. package/payload/server/public/assets/time-entry-format-D9gtnG01.js.gz +0 -0
  277. package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.br +0 -0
  278. package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.gz +0 -0
  279. package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.br +0 -0
  280. package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.gz +0 -0
  281. package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.br +0 -0
  282. package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.gz +0 -0
  283. package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.br +0 -0
  284. package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.gz +0 -0
  285. package/payload/server/public/assets/wrench-CYgaN7K9.js.br +0 -1
  286. package/payload/server/public/assets/wrench-CYgaN7K9.js.gz +0 -0
  287. /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js → useSubAccountSwitcher-CH9qm2Ad.js} +0 -0
  288. /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js.br → useSubAccountSwitcher-CH9qm2Ad.js.br} +0 -0
  289. /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js.gz → useSubAccountSwitcher-CH9qm2Ad.js.gz} +0 -0
@@ -37,6 +37,10 @@ function harness(opts: { folders?: string[] } = {}) {
37
37
  }
38
38
 
39
39
  const env = {
40
+ // This portal's own account, baked into wrangler at assemble time. Every
41
+ // object key and every query binds it, so the fake session row below has to
42
+ // carry the same value or resolveSession refuses.
43
+ PORTAL_ACCOUNT_ID: 'acct',
40
44
  BUCKET: {
41
45
  async createMultipartUpload(key: string) {
42
46
  const id = `r2-${++seq}`
@@ -69,7 +73,10 @@ function harness(opts: { folders?: string[] } = {}) {
69
73
  return null
70
74
  },
71
75
  async run() {
72
- rows.set(String(args[3]), { fileId: args[0], size: args[4] })
76
+ // The manifest upsert binds (fileId, accountId, ownerId,
77
+ // filename, objectKey, size, ...), so the key is arg 4 and the
78
+ // size arg 5.
79
+ rows.set(String(args[4]), { fileId: args[0], size: args[5] })
73
80
  },
74
81
  }
75
82
  },
@@ -105,7 +112,7 @@ describe('multipart create', () => {
105
112
  )
106
113
  expect(res.status).toBe(200)
107
114
  expect(res.payload.r2UploadId).toBe('r2-1')
108
- expect(res.payload.key).toBe('alice/big.zip')
115
+ expect(res.payload.key).toBe('acct/alice/big.zip')
109
116
  })
110
117
 
111
118
  it('applies the same target rule as a single-shot upload', async () => {
@@ -122,7 +129,7 @@ describe('multipart create', () => {
122
129
  const res = await processMultipartCreate(
123
130
  's', 'big.zip', 'jobs', 1024, h.env, h.log, h.newId, () => 1,
124
131
  )
125
- expect(res.payload.key).toBe('alice/jobs/big.zip')
132
+ expect(res.payload.key).toBe('acct/alice/jobs/big.zip')
126
133
  })
127
134
  })
128
135
 
@@ -131,17 +138,18 @@ describe('multipart part', () => {
131
138
  const h = harness()
132
139
  await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
133
140
  const res = await processMultipartPart(
134
- 's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(MAX_PART_BYTES + 1), h.env, h.log,
141
+ 's', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(MAX_PART_BYTES + 1), h.env, h.log,
135
142
  )
136
143
  expect(res.status).toBe(413)
137
144
  })
138
145
 
139
146
  it('refuses a part whose key is not the caller own', async () => {
140
147
  // The key arrives on EVERY part request, so it is re-authorized every time.
141
- // A session that may write alice/ must not be able to append to bob/.
148
+ // A session that may write acct/alice/ must not be able to append to
149
+ // acct/bob/, even though both live in this portal's own account.
142
150
  const h = harness()
143
151
  const res = await processMultipartPart(
144
- 's', 'bob/big.zip', 'r2-1', 1, new Uint8Array(4), h.env, h.log,
152
+ 's', 'acct/bob/big.zip', 'r2-1', 1, new Uint8Array(4), h.env, h.log,
145
153
  )
146
154
  expect(res.status).toBe(403)
147
155
  })
@@ -150,7 +158,7 @@ describe('multipart part', () => {
150
158
  const h = harness()
151
159
  await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
152
160
  const res = await processMultipartPart(
153
- 's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(8), h.env, h.log,
161
+ 's', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(8), h.env, h.log,
154
162
  )
155
163
  expect(res.status).toBe(200)
156
164
  expect(res.payload.etag).toBe('etag-1')
@@ -161,15 +169,15 @@ describe('multipart complete', () => {
161
169
  it('assembles the object and writes one manifest row', async () => {
162
170
  const h = harness()
163
171
  await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
164
- await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(60), h.env, h.log)
165
- await processMultipartPart('s', 'alice/big.zip', 'r2-1', 2, new Uint8Array(40), h.env, h.log)
172
+ await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(60), h.env, h.log)
173
+ await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 2, new Uint8Array(40), h.env, h.log)
166
174
  const res = await processMultipartComplete(
167
- 's', 'alice/big.zip', 'r2-1', '',
175
+ 's', 'acct/alice/big.zip', 'r2-1', '',
168
176
  [{ partNumber: 1, etag: 'etag-1' }, { partNumber: 2, etag: 'etag-2' }],
169
177
  h.env, h.log, h.newId, () => 1,
170
178
  )
171
179
  expect(res.status).toBe(200)
172
- expect(h.objects.get('alice/big.zip')).toBe(100)
180
+ expect(h.objects.get('acct/alice/big.zip')).toBe(100)
173
181
  expect(h.rows.size).toBe(1)
174
182
  })
175
183
 
@@ -179,7 +187,7 @@ describe('multipart complete', () => {
179
187
  const h = harness()
180
188
  await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
181
189
  const res = await processMultipartComplete(
182
- 's', 'alice/big.zip', 'r2-1', '',
190
+ 's', 'acct/alice/big.zip', 'r2-1', '',
183
191
  [{ partNumber: 1, etag: 'e', size: MAX_TOTAL_BYTES }, { partNumber: 2, etag: 'e', size: 1 }],
184
192
  h.env, h.log, h.newId, () => 1,
185
193
  )
@@ -191,7 +199,7 @@ describe('multipart complete', () => {
191
199
  it('refuses a complete for a key that is not the caller own', async () => {
192
200
  const h = harness()
193
201
  const res = await processMultipartComplete(
194
- 's', 'bob/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'e' }], h.env, h.log, h.newId, () => 1,
202
+ 's', 'acct/bob/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'e' }], h.env, h.log, h.newId, () => 1,
195
203
  )
196
204
  expect(res.status).toBe(403)
197
205
  })
@@ -199,9 +207,9 @@ describe('multipart complete', () => {
199
207
  it('logs op=complete with a re-read post-condition', async () => {
200
208
  const h = harness()
201
209
  await processMultipartCreate('s', 'big.zip', '', 10, h.env, h.log, h.newId, () => 1)
202
- await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(10), h.env, h.log)
210
+ await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(10), h.env, h.log)
203
211
  await processMultipartComplete(
204
- 's', 'alice/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'etag-1' }],
212
+ 's', 'acct/alice/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'etag-1' }],
205
213
  h.env, h.log, h.newId, () => 1,
206
214
  )
207
215
  expect(
@@ -3,9 +3,11 @@ import { processUpload } from '../../skills/data-portal/template/functions/api/u
3
3
  import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
4
4
 
5
5
  /**
6
- * The manifest is a Map keyed on objectKey, so the schema's `objectKey UNIQUE`
7
- * is real rather than assumed. Task 1704's lesson: a fake that accepts what the
8
- * store rejects passes a test the production path fails.
6
+ * The manifest is a Map keyed on objectKey, so the schema's
7
+ * `UNIQUE (accountId, objectKey)` is real rather than assumed. Every key now
8
+ * leads with the account segment, so on this single-account harness keying on
9
+ * objectKey alone is that same conflict key. Task 1704's lesson: a fake that
10
+ * accepts what the store rejects passes a test the production path fails.
9
11
  */
10
12
  function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
11
13
  const folders = opts.folders ?? ['jobs']
@@ -14,6 +16,9 @@ function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
14
16
  const lines: string[] = []
15
17
 
16
18
  const env = {
19
+ // This portal's own account, baked into wrangler.toml at assemble time. It
20
+ // is the tenant scope for every query and the leading segment of every key.
21
+ PORTAL_ACCOUNT_ID: 'acct',
17
22
  BUCKET: {
18
23
  async put(key: string, body: Uint8Array) {
19
24
  objects.set(key, body)
@@ -45,17 +50,20 @@ function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
45
50
  return null
46
51
  },
47
52
  async run() {
48
- // The upsert: conflict on objectKey replaces in place, which is
49
- // what the real ON CONFLICT (objectKey) does.
50
- const key = String(args[3])
53
+ // The upsert: conflict on (accountId, objectKey) replaces in
54
+ // place, which is what the real
55
+ // ON CONFLICT (accountId, objectKey) does. accountId is bind 1,
56
+ // so every column after it sits one index later than it used to.
57
+ const key = String(args[4])
51
58
  rows.set(key, {
52
59
  fileId: args[0],
53
- ownerId: args[1],
54
- filename: args[2],
60
+ accountId: args[1],
61
+ ownerId: args[2],
62
+ filename: args[3],
55
63
  objectKey: key,
56
- size: args[4],
64
+ size: args[5],
57
65
  ingested: 0,
58
- targetPath: args[7],
66
+ targetPath: args[8],
59
67
  })
60
68
  },
61
69
  }
@@ -83,8 +91,8 @@ describe('targeted upload', () => {
83
91
  's', 'a.pdf', 'jobs', new Uint8Array([1, 2]), h.env, h.log, h.newId, () => 1000,
84
92
  )
85
93
  expect(res.status).toBe(200)
86
- expect([...h.objects.keys()]).toEqual(['alice/jobs/a.pdf'])
87
- expect(h.rows.get('alice/jobs/a.pdf')!.targetPath).toBe('jobs')
94
+ expect([...h.objects.keys()]).toEqual(['acct/alice/jobs/a.pdf'])
95
+ expect(h.rows.get('acct/alice/jobs/a.pdf')!.targetPath).toBe('jobs')
88
96
  })
89
97
 
90
98
  it('stores at the owner root when untargeted', async () => {
@@ -93,7 +101,7 @@ describe('targeted upload', () => {
93
101
  's', 'a.pdf', '', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
94
102
  )
95
103
  expect(res.status).toBe(200)
96
- expect([...h.objects.keys()]).toEqual(['alice/a.pdf'])
104
+ expect([...h.objects.keys()]).toEqual(['acct/alice/a.pdf'])
97
105
  })
98
106
 
99
107
  it('keeps the same filename in two folders as two distinct objects', async () => {
@@ -102,7 +110,7 @@ describe('targeted upload', () => {
102
110
  const h = harness({ folders: ['jobs', 'output'] })
103
111
  await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
104
112
  await processUpload('s', 'a.pdf', 'output', new Uint8Array([2]), h.env, h.log, h.newId, () => 1000)
105
- expect([...h.objects.keys()].sort()).toEqual(['alice/jobs/a.pdf', 'alice/output/a.pdf'])
113
+ expect([...h.objects.keys()].sort()).toEqual(['acct/alice/jobs/a.pdf', 'acct/alice/output/a.pdf'])
106
114
  expect(h.rows.size).toBe(2)
107
115
  })
108
116
 
@@ -111,7 +119,7 @@ describe('targeted upload', () => {
111
119
  await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
112
120
  await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([2, 2]), h.env, h.log, h.newId, () => 2000)
113
121
  expect(h.rows.size).toBe(1)
114
- expect(h.rows.get('alice/jobs/a.pdf')!.size).toBe(2)
122
+ expect(h.rows.get('acct/alice/jobs/a.pdf')!.size).toBe(2)
115
123
  })
116
124
 
117
125
  it('refuses an out-of-grant target and stores nothing', async () => {
@@ -20,6 +20,10 @@ function envWithGenerations(
20
20
  manifest: Record<string, unknown>[] = [],
21
21
  ) {
22
22
  return {
23
+ // The portal's own account, baked in at assemble time. On the shared store
24
+ // (Task 2099) it is the tenant scope every query binds, so the fake below
25
+ // honours it the same way it honours the generation pointer.
26
+ PORTAL_ACCOUNT_ID: 'acct',
23
27
  DB: {
24
28
  prepare(sql: string) {
25
29
  return {
@@ -27,6 +31,10 @@ function envWithGenerations(
27
31
  return {
28
32
  async first() {
29
33
  if (sql.includes('FROM sessions')) {
34
+ // Bound (sessionId, accountId, nowMs). A session belonging to
35
+ // another portal's account resolves to nothing here, and the
36
+ // joined people row carries the account the caller asked for.
37
+ if (bound[1] !== 'acct') return null
30
38
  return { ownerId: 'alice', accountId: 'acct', folders: '' }
31
39
  }
32
40
  return null
@@ -44,13 +44,30 @@ describe('data-portal SKILL.md', () => {
44
44
  expect(skill()).toMatch(/plugin-read/)
45
45
  })
46
46
 
47
- it('records the two-session provisioning sequence', () => {
48
- // The load-bearing sequencing decision: the broker registers a bucket to
49
- // whoever creates it, ON CREATE and never reassigned, so the client
50
- // account must create its own or its ingestion sweep is denied forever.
47
+ it('records the shared store and single-session provisioning (Task 2099)', () => {
48
+ // The load-bearing decision, and the reverse of what this test used to
49
+ // pin. Provisioning was two sessions so the client account would own its
50
+ // own bucket. That is gone: every portal binds ONE shared house-owned
51
+ // database and bucket, created once, and a per-client store is what put
52
+ // the account on Cloudflare's ten-database cap.
51
53
  const text = skill()
52
- expect(text).toMatch(/storage-r2-bucket-create/)
53
- expect(text).toMatch(/client|sub-account/i)
54
+ expect(text).toMatch(/one session|single house session|provisioning is one session/i)
55
+ expect(text).toMatch(/DATA_PORTAL_DB_NAME/)
56
+ expect(text).toMatch(/DATA_PORTAL_BUCKET/)
57
+ // The per-client creates must not come back by copy-paste.
58
+ expect(text).not.toMatch(/storage-r2-bucket-create/)
59
+ })
60
+
61
+ it('pins the tenant discriminator, since one store now holds every client', () => {
62
+ const text = skill()
63
+ expect(text).toMatch(/PORTAL_ACCOUNT_ID/)
64
+ expect(text).toMatch(/storageModel/)
65
+ })
66
+
67
+ it('forbids hand-filling the assembled config', () => {
68
+ // Two sessions resolving the same placeholder independently is what
69
+ // shipped a portal whose bucket name and config disagreed.
70
+ expect(skill()).toMatch(/portal-assemble\.mjs/)
54
71
  })
55
72
 
56
73
  it('names the passcode discipline', () => {
@@ -3,13 +3,18 @@ import { processUpload } from '../../skills/data-portal/template/functions/api/u
3
3
  import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
4
4
 
5
5
  // The fake must model the constraints the schema actually declares, not just
6
- // the happy path. `objectKey TEXT UNIQUE` is what makes a re-upload an upsert
7
- // rather than a second row; a fake that accepts duplicates would let the
8
- // "exactly one manifest row" assertion pass while the real D1 threw.
6
+ // the happy path. The table-level `UNIQUE (accountId, objectKey)` is what makes
7
+ // a re-upload an upsert rather than a second row; a fake that accepts
8
+ // duplicates would let the "exactly one manifest row" assertion pass while the
9
+ // real D1 threw. The store is shared by every portal, so the uniqueness is per
10
+ // account and never on objectKey alone.
9
11
  function makeEnv() {
10
12
  const manifestRows: unknown[][] = []
11
13
  const objects = new Map<string, Uint8Array>()
12
14
  const env = {
15
+ // This portal's own account, baked into wrangler.toml [vars] at assemble
16
+ // time. It is the tenant scope for every query and every object key.
17
+ PORTAL_ACCOUNT_ID: 'acct-a',
13
18
  DB: {
14
19
  prepare(query: string) {
15
20
  let bound: unknown[] = []
@@ -20,11 +25,14 @@ function makeEnv() {
20
25
  },
21
26
  async run() {
22
27
  if (/INSERT INTO manifest/i.test(query)) {
23
- const objectKey = bound[3] as string
24
- const existing = manifestRows.findIndex((r) => r[3] === objectKey)
28
+ const accountId = bound[1] as string
29
+ const objectKey = bound[4] as string
30
+ const existing = manifestRows.findIndex(
31
+ (r) => r[1] === accountId && r[4] === objectKey,
32
+ )
25
33
  if (existing >= 0) {
26
- if (!/ON CONFLICT\s*\(\s*objectKey\s*\)\s*DO UPDATE/i.test(query)) {
27
- throw new Error('UNIQUE constraint failed: manifest.objectKey')
34
+ if (!/ON CONFLICT\s*\(\s*accountId\s*,\s*objectKey\s*\)\s*DO UPDATE/i.test(query)) {
35
+ throw new Error('UNIQUE constraint failed: manifest.accountId, manifest.objectKey')
28
36
  }
29
37
  manifestRows[existing] = bound
30
38
  } else {
@@ -38,11 +46,19 @@ function makeEnv() {
38
46
  },
39
47
  async first<T>() {
40
48
  if (/FROM sessions/i.test(query)) {
41
- return (bound[0] === 'sess-a' ? { ownerId: 'alice' } : null) as T | null
49
+ // Bound (sessionId, accountId, nowMs). The joined people row
50
+ // carries its own account, and resolveSession returns null unless
51
+ // that account equals the one it was asked about.
52
+ return (bound[0] === 'sess-a' && bound[1] === 'acct-a'
53
+ ? { ownerId: 'alice', accountId: 'acct-a' }
54
+ : null) as T | null
42
55
  }
43
56
  if (/FROM manifest/i.test(query)) {
57
+ // Bound (objectKey, accountId).
44
58
  const key = bound[0] as string
45
- return (objects.has(key) ? { fileId: 'x' } : null) as T | null
59
+ return (objects.has(key) && bound[1] === 'acct-a'
60
+ ? { fileId: 'x' }
61
+ : null) as T | null
46
62
  }
47
63
  return null as T | null
48
64
  },
@@ -72,7 +88,7 @@ function makeEnv() {
72
88
  }
73
89
 
74
90
  describe('processUpload', () => {
75
- it('writes the object under the owner prefix and exactly one manifest row', async () => {
91
+ it('writes the object under the account and owner prefix and exactly one manifest row', async () => {
76
92
  const { env, manifestRows, objects } = makeEnv()
77
93
  const lines: string[] = []
78
94
  const res = await processUpload(
@@ -85,7 +101,7 @@ describe('processUpload', () => {
85
101
  () => 1_000_000,
86
102
  )
87
103
  expect(res.status).toBe(200)
88
- expect([...objects.keys()]).toEqual(['alice/invoice.pdf'])
104
+ expect([...objects.keys()]).toEqual(['acct-a/alice/invoice.pdf'])
89
105
  expect(manifestRows.length).toBe(1)
90
106
  })
91
107
 
@@ -100,8 +116,8 @@ describe('processUpload', () => {
100
116
  () => 'up-1',
101
117
  () => 1_000_000,
102
118
  )
103
- // Column order: fileId, ownerId, filename, objectKey, size, uploadedAt, ingested
104
- expect(manifestRows[0][6]).toBe(0)
119
+ // Column order: fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested
120
+ expect(manifestRows[0][7]).toBe(0)
105
121
  })
106
122
 
107
123
  it('emits the full lifeline correlated by uploadId, ending in a verified post-condition', async () => {
@@ -139,7 +155,7 @@ describe('processUpload', () => {
139
155
  expect(manifestRows.length).toBe(0)
140
156
  })
141
157
 
142
- it('rejects a filename that escapes the owner prefix, and writes nothing', async () => {
158
+ it('rejects a filename that escapes the account and owner prefix, and writes nothing', async () => {
143
159
  const { env, objects, manifestRows } = makeEnv()
144
160
  const res = await processUpload(
145
161
  'sess-a',
@@ -230,16 +246,16 @@ describe('processUpload', () => {
230
246
  )
231
247
  expect(second.status).toBe(200)
232
248
  expect(manifestRows.length).toBe(1)
233
- expect(objects.get('alice/invoice.pdf')?.length).toBe(3)
249
+ expect(objects.get('acct-a/alice/invoice.pdf')?.length).toBe(3)
234
250
  })
235
251
 
236
252
  it('updates the row to describe the new object, not the old one', async () => {
237
253
  const { env, manifestRows } = makeEnv()
238
254
  await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
239
255
  await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
240
- // Column order: fileId, ownerId, filename, objectKey, size, uploadedAt, ingested
241
- expect(manifestRows[0][4]).toBe(3)
242
- expect(manifestRows[0][5]).toBe(new Date(2_000).toISOString())
256
+ // Column order: fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested
257
+ expect(manifestRows[0][5]).toBe(3)
258
+ expect(manifestRows[0][6]).toBe(new Date(2_000).toISOString())
243
259
  })
244
260
 
245
261
  it('re-arms ingestion by resetting ingested to 0', async () => {
@@ -248,9 +264,9 @@ describe('processUpload', () => {
248
264
  // ingested, and nothing would ever say so.
249
265
  const { env, manifestRows } = makeEnv()
250
266
  await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
251
- manifestRows[0][6] = 1 // the device-side sweep marks it ingested
267
+ manifestRows[0][7] = 1 // the device-side sweep marks it ingested
252
268
  await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
253
- expect(manifestRows[0][6]).toBe(0)
269
+ expect(manifestRows[0][7]).toBe(0)
254
270
  })
255
271
  })
256
272
  })
@@ -25,34 +25,81 @@ Uploads must keep succeeding while the device is offline, which is the same reas
25
25
  went to Pages. The install's magic-link gate cannot be reused: it gates `/api/chat` on the install
26
26
  and its sessions live in memory, so it does not cross origins.
27
27
 
28
- ## Provisioning is two sessions, and the order matters
28
+ ## One shared store, and provisioning is one session
29
29
 
30
- The storage broker registers a bucket to **whoever creates it**, and that ownership is set on create
31
- and never reassigned. There is no delegation.
30
+ Every portal binds the **same** house-owned D1 database and the **same** house-owned R2 bucket. They
31
+ are created once, for the whole install, and reused by every client thereafter. Do **not** create a
32
+ database or a bucket per client: that is what put the account on Cloudflare's ten-database cap and
33
+ blocked the eleventh portal outright.
32
34
 
33
- 1. **In the client account's own session**, create the bucket with `storage-r2-bucket-create`, named
34
- from `bin/cf-store-name.sh <accountId> <purpose>`. It registers to that account.
35
- 2. **In the admin session**, do everything else: the D1 database, the assembly, recording the Pages
36
- project to the client account with `storage-pages-adopt`, and then the deploy.
35
+ Their identity lives in three keys in `platform/config/cloudflare-house.env`, written once when the
36
+ store is created and never retyped:
37
37
 
38
- If the admin creates the bucket instead, it registers to the house, and the client account's own
39
- ingestion sweep is denied `cross-account` against its own files — permanently, because ownership is
40
- never reassigned. The portal would appear to work while nothing on the client side could read what
41
- it stored.
38
+ ```
39
+ DATA_PORTAL_DB_NAME=<the one shared database>
40
+ DATA_PORTAL_DB_ID=<its id>
41
+ DATA_PORTAL_BUCKET=<the one shared bucket>
42
+ ```
43
+
44
+ That file is house-only and is preserved across upgrades. A new file under `platform/config/` would
45
+ not be: the installer wipes that directory and restores only this one.
46
+
47
+ Because the store already exists, **provisioning is a single house session**. There is no client-side
48
+ step. What is still per client: the Pages project and its custom domain, the account's own
49
+ `PORTAL_FETCH_SECRET`, enrolment of the people, and the assembled tree.
50
+
51
+ Isolation is no longer one store per client. It is an `accountId` column on every row and an
52
+ `<accountId>/` prefix on every object key, with the portal's own account baked into its worker as
53
+ `PORTAL_ACCOUNT_ID` at assemble time. The worker binds that value into every query and every key
54
+ check, so it is the one thing separating this client's rows and objects from another's.
55
+
56
+ `data-portal.json` must carry `"storageModel": "shared"`. An absent value means a pre-2099 portal on
57
+ its own dedicated database.
58
+
59
+ The house-side loops do not take that field at its word. They compare the account's `portalDbName`
60
+ and `bucketName` against the shared names in `cloudflare-house.env` and use what the config actually
61
+ NAMES, logging `op=storage-model result=overridden` when the two disagree. The label is hand-written
62
+ in a hand-maintained file, and trusting it is fail-open in the one direction that leaks: a config
63
+ naming the shared database whose label was dropped or mis-cased would otherwise be read store-wide,
64
+ and the uploads pull would carry another client's files into this account's tree. Write the field
65
+ correctly anyway; it is what the assemble step reads, and it is the fallback before cutover. Those keep their own store **and their own deployed worker**: this
66
+ template binds `accountId` in every query and their database has no such column, so a dedicated
67
+ account is refused at assemble rather than redeployed. Migrating one is deliberate work.
68
+
69
+ The old two-session split is gone, and so is the reason given for it. It existed so the client
70
+ account would own the bucket, on the grounds that its ingestion sweep would otherwise be denied
71
+ `cross-account` against its own files. That was not so: the sweep (`portal-uploads-pull`) runs
72
+ house-level through the house credential and is not owner-gated. The split's real effect was that
73
+ two sessions filled the same store name independently and disagreed, which is the bug this design
74
+ removes.
42
75
 
43
76
  ## Enrol the people first
44
77
 
45
78
  Collect from the operator the portal's custom domain and the list of people, then write
46
- `<accountDir>/data-portal.json` carrying `portalDomain`, `portalDbName`, `bucketName`, and a
47
- `people` array of `{ name, ownerId, prefix }`.
79
+ `<accountDir>/data-portal.json` carrying `portalDomain`, `accountId`, `projectName`,
80
+ `"storageModel": "shared"`, `portalDbName`, `bucketName`, and a `people` array of
81
+ `{ name, ownerId, prefix }`.
82
+
83
+ `portalDbName` and `bucketName` are the SHARED store's names, identical in every account's file and
84
+ copied from `cloudflare-house.env`. They stay in this file because the house-side loops (the uploads
85
+ pull, the index push, the audit) enumerate accounts by it and read the store names from it, which is
86
+ what lets shared and pre-2099 dedicated portals run side by side with no other branching.
48
87
 
49
88
  For each person:
50
89
 
51
90
  ```bash
52
91
  node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
53
- --account <accountId> [--folders <a,b,c>] | npx wrangler d1 execute <portalDbName> --remote --file=-
92
+ --account <accountId> [--storage-model shared|dedicated] [--folders <a,b,c>] \
93
+ | npx wrangler d1 execute <portalDbName> --remote --file=-
54
94
  ```
55
95
 
96
+ `--storage-model` picks the upsert's conflict target and defaults to `shared`. It must match the
97
+ account's `data-portal.json`, because the two `people` tables have different keys: the shared one is
98
+ keyed `(accountId, ownerId)` so two clients can each enrol a person of the same name, and a
99
+ pre-2099 one is keyed on `ownerId` alone. Naming the wrong pair does not degrade, it fails the whole
100
+ statement, so a grandfathered portal must pass `--storage-model dedicated` or its passcode rotation
101
+ stops working. That failure is loud and happens at the database before any row moves.
102
+
56
103
  The script mints the passcode and prints it **once to stderr**, so it reaches the operator's terminal
57
104
  and never the pipe; stdout carries the `INSERT` alone. Do not hand-roll this: the passcode's length
58
105
  and alphabet are pinned in `template/functions/api/_lib/passcode.mjs`, the script is what applies
@@ -93,9 +140,22 @@ files. The gate enforces it too, but do not enrol a name it will reject.
93
140
 
94
141
  ## Assemble the canonical tree
95
142
 
96
- Copy `template/` to `<accountDir>/sites/<project>/` and fill `wrangler.toml`'s `__PROJECT_NAME__`,
97
- `__D1_DATABASE_NAME__`, `__D1_DATABASE_ID__`, and `__R2_BUCKET_NAME__`. **Leave no `__...__`
98
- placeholder in the assembled tree.**
143
+ Copy `template/` to `<accountDir>/sites/<project>/`, then fill `wrangler.toml` with the script. Do
144
+ **not** fill it by hand:
145
+
146
+ ```bash
147
+ node platform/plugins/cloudflare/bin/portal-assemble.mjs \
148
+ --account-dir <accountDir> --platform-root platform \
149
+ --install-origin https://<public-tunnel-hostname> \
150
+ --template platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml
151
+ ```
152
+
153
+ It reads the shared store's identity from `cloudflare-house.env` and the account's own id from
154
+ `data-portal.json`, substitutes every placeholder including `__PORTAL_ACCOUNT_ID__`, and fails closed
155
+ with the offending name when a shared key is missing, when the account is on the dedicated model, or
156
+ when any placeholder survives. Hand-filling these values is exactly what shipped a portal whose
157
+ bucket name and config disagreed, so the one right answer comes from one file and is substituted
158
+ mechanically.
99
159
 
100
160
  `sites/<project>/` is the only location the broker will publish: `resolveSiteDir` resolves
101
161
  `<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat: `index.html`, `portal.css`, `portal.js`,
@@ -107,10 +167,11 @@ config does not declare publishes the site one level too deep, which serves 404
107
167
  The deploy serves everything in that tree except a fixed ignore list — `functions/`, `_worker.js`,
108
168
  `_redirects`, `_headers`, `_routes.json`, `node_modules`, `.git`, `.DS_Store`, `.wrangler` — and
109
169
  `pages deploy` reads no `.assetsignore`, so anything else you leave there is public. Nothing else
110
- belongs in it. `schema.sql` therefore lives beside `template/` in the skill, not inside it. Name the D1 database
111
- with the same `bin/cf-store-name.sh` scheme as the bucket, and apply
112
- `platform/plugins/cloudflare/skills/data-portal/schema.sql` to the remote database from there. It is
113
- `IF NOT EXISTS` throughout, so re-applying is safe.
170
+ belongs in it. `schema.sql` therefore lives beside `template/` in the skill, not inside it. It is
171
+ applied ONCE, to the one shared database named by `DATA_PORTAL_DB_NAME`, when that store is first
172
+ created. A new client portal needs no schema step at all: the tables are already there, and its rows
173
+ are separated from every other client's by `accountId`. The file is `IF NOT EXISTS` throughout, so
174
+ re-applying is safe.
114
175
 
115
176
  Safe, but not a migration: `IF NOT EXISTS` adds no column to a table that already exists. A portal
116
177
  whose `directory` table predates the `isDir` column keeps failing every index push, because the push
@@ -356,4 +417,6 @@ event at request time.
356
417
  ## Tool discipline
357
418
 
358
419
  Never write a token into the tree, never commit one, never echo one. The bucket name and database
359
- name come from `bin/cf-store-name.sh`, never hand-built.
420
+ name come from `platform/config/cloudflare-house.env` by way of `bin/portal-assemble.mjs`, never
421
+ hand-built and never retyped. (`bin/cf-store-name.sh` still names per-account stores for booking and
422
+ other skills; the portal no longer has one.)