@rubytech/create-maxy-code 0.1.478 → 0.1.480

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 (252) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
  4. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
  5. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +830 -0
  6. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
  8. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
  9. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
  10. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +163 -0
  11. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
  12. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  13. package/payload/platform/lib/graph-style/dist/index.js +22 -0
  14. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  15. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
  16. package/payload/platform/lib/graph-style/src/index.ts +20 -0
  17. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +155 -12
  18. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +3 -0
  20. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  21. package/payload/platform/lib/storage-broker/dist/cf-exec.js +68 -28
  22. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  23. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +194 -16
  24. package/payload/platform/lib/storage-broker/src/cf-exec.ts +74 -29
  25. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +13 -1
  26. package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
  27. package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
  28. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
  29. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
  30. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
  31. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
  32. package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
  33. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
  34. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
  35. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
  36. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
  37. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
  38. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
  39. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
  40. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
  41. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
  42. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
  43. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
  44. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
  45. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
  46. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
  47. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
  48. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
  49. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
  50. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
  51. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
  52. package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
  53. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +41 -1
  54. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
  55. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +249 -2
  56. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
  57. package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js +127 -3
  58. package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js.map +1 -1
  59. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +17 -0
  60. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
  61. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +26 -1
  62. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
  63. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts +37 -1
  64. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts.map +1 -1
  65. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js +126 -5
  66. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js.map +1 -1
  67. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +18 -0
  68. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
  69. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +123 -18
  70. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  71. package/payload/platform/plugins/outlook/references/auth.md +45 -2
  72. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
  73. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
  74. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
  75. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
  76. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
  77. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
  80. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +122 -0
  81. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  82. package/payload/server/{chunk-3XLLTG6R.js → chunk-REWXLAPC.js} +30 -10
  83. package/payload/server/package.json +1 -1
  84. package/payload/server/public/activity.html +7 -6
  85. package/payload/server/public/assets/{AdminLoginScreens-DR5eLF-a.js → AdminLoginScreens-DXYJWXaX.js} +1 -1
  86. package/payload/server/public/assets/AdminShell-UDgNN4CA.js +2 -0
  87. package/payload/server/public/assets/Checkbox-C08Ulc_a.js +1 -0
  88. package/payload/server/public/assets/activity-06gsRDVv.js +1 -0
  89. package/payload/server/public/assets/admin-BhiHyORH.js +1 -0
  90. package/payload/server/public/assets/{arc-BrHWN2Kr.js → arc-BHXuzTwg.js} +1 -1
  91. package/payload/server/public/assets/architecture-YZFGNWBL-Ct5q6ks1.js +1 -0
  92. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Clt9NOr0.js → architectureDiagram-Q4EWVU46-DsN_vxau.js} +1 -1
  93. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BOe_jmXi.js → blockDiagram-DXYQGD6D-CvNat1uB.js} +1 -1
  94. package/payload/server/public/assets/browser-Cz4IKS47.js +1 -0
  95. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-B64FaKMV.js → c4Diagram-AHTNJAMY-E9yqTPTW.js} +1 -1
  96. package/payload/server/public/assets/calendar-D0xtNqH8.js +1 -0
  97. package/payload/server/public/assets/channel-PE9vBKEQ.js +1 -0
  98. package/payload/server/public/assets/chat-DwLJ830n.js +1 -0
  99. package/payload/server/public/assets/chevron-left-Cv-TFuEf.js +1 -0
  100. package/payload/server/public/assets/{chunk-2KRD3SAO-BGAUfWvM.js → chunk-2KRD3SAO-DpzI_0EY.js} +1 -1
  101. package/payload/server/public/assets/{chunk-336JU56O-PFi6uQZ0.js → chunk-336JU56O-eR_I9Eu4.js} +2 -2
  102. package/payload/server/public/assets/chunk-426QAEUC-BDNZtJsz.js +1 -0
  103. package/payload/server/public/assets/{chunk-4BX2VUAB-CkxXXCiF.js → chunk-4BX2VUAB-D-6xk1t8.js} +1 -1
  104. package/payload/server/public/assets/{chunk-4TB4RGXK-B-7tN4IA.js → chunk-4TB4RGXK-BkqUcFpL.js} +1 -1
  105. package/payload/server/public/assets/{chunk-55IACEB6-CBkNmqrt.js → chunk-55IACEB6-ChCl2pSP.js} +1 -1
  106. package/payload/server/public/assets/{chunk-5FUZZQ4R-wa9ujn7i.js → chunk-5FUZZQ4R-DasGsg54.js} +1 -1
  107. package/payload/server/public/assets/{chunk-5PVQY5BW-DIk-xBaE.js → chunk-5PVQY5BW-DXlHUX1h.js} +1 -1
  108. package/payload/server/public/assets/{chunk-67CJDMHE-BbMV1lgW.js → chunk-67CJDMHE-BZfVpTsE.js} +1 -1
  109. package/payload/server/public/assets/{chunk-7N4EOEYR-C49Mv-nQ.js → chunk-7N4EOEYR-C6U5TIgT.js} +1 -1
  110. package/payload/server/public/assets/{chunk-AA7GKIK3-Dy_pJOp_.js → chunk-AA7GKIK3-CA7cVDmw.js} +1 -1
  111. package/payload/server/public/assets/{chunk-BSJP7CBP-Ce-OmPqn.js → chunk-BSJP7CBP-DxN2GC3p.js} +1 -1
  112. package/payload/server/public/assets/{chunk-CIAEETIT-tnjq-APC.js → chunk-CIAEETIT-C0-4wA8y.js} +1 -1
  113. package/payload/server/public/assets/{chunk-EDXVE4YY-Bvv_deZ2.js → chunk-EDXVE4YY--kyb1rLc.js} +1 -1
  114. package/payload/server/public/assets/{chunk-ENJZ2VHE-CmxCPKyU.js → chunk-ENJZ2VHE-FBN6hR7V.js} +1 -1
  115. package/payload/server/public/assets/{chunk-FMBD7UC4-Top6y08S.js → chunk-FMBD7UC4-Lpge4M5g.js} +1 -1
  116. package/payload/server/public/assets/{chunk-FOC6F5B3-B9LPNCUS.js → chunk-FOC6F5B3-CM1rwOou.js} +1 -1
  117. package/payload/server/public/assets/{chunk-ICPOFSXX-Bi-_c351.js → chunk-ICPOFSXX-JdYLY3yu.js} +2 -2
  118. package/payload/server/public/assets/{chunk-K5T4RW27-DAnRM04h.js → chunk-K5T4RW27-DCUblGBr.js} +1 -1
  119. package/payload/server/public/assets/{chunk-KGLVRYIC-C0QYcbLe.js → chunk-KGLVRYIC-BP_cide2.js} +1 -1
  120. package/payload/server/public/assets/{chunk-LIHQZDEY-BjvA5ykt.js → chunk-LIHQZDEY-DjMa1lOl.js} +1 -1
  121. package/payload/server/public/assets/{chunk-ORNJ4GCN-Cs46Gj7z.js → chunk-ORNJ4GCN-D51eJAoV.js} +1 -1
  122. package/payload/server/public/assets/{chunk-OYMX7WX6-BBpimab5.js → chunk-OYMX7WX6-4hWIO544.js} +1 -1
  123. package/payload/server/public/assets/chunk-QZHKN3VN-B_Hris8x.js +1 -0
  124. package/payload/server/public/assets/{chunk-U2HBQHQK-CQUSBHoK.js → chunk-U2HBQHQK-BwdQf9cS.js} +1 -1
  125. package/payload/server/public/assets/{chunk-X2U36JSP-Dm8TevG0.js → chunk-X2U36JSP-DmDxwLOg.js} +1 -1
  126. package/payload/server/public/assets/{chunk-XPW4576I-B10QuxYb.js → chunk-XPW4576I-JQyAB7ND.js} +1 -1
  127. package/payload/server/public/assets/{chunk-YZCP3GAM-vaGsu4Wk.js → chunk-YZCP3GAM-BonLdCzI.js} +1 -1
  128. package/payload/server/public/assets/{chunk-ZZ45TVLE-D1Qi22bL.js → chunk-ZZ45TVLE-BXKB2wSk.js} +1 -1
  129. package/payload/server/public/assets/classDiagram-6PBFFD2Q-D8Q05iMI.js +1 -0
  130. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Dm3hAxfm.js +1 -0
  131. package/payload/server/public/assets/clone-C-wOwvpY.js +1 -0
  132. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-CRDvLRXs.js → cose-bilkent-S5V4N54A-BDnd4Blw.js} +1 -1
  133. package/payload/server/public/assets/{dagre-CEmqJTt3.js → dagre-BmexzuER.js} +1 -1
  134. package/payload/server/public/assets/{dagre-KV5264BT-LXMTo7Fp.js → dagre-KV5264BT-D4WVuwPj.js} +1 -1
  135. package/payload/server/public/assets/data-DUDJfnjI.js +1 -0
  136. package/payload/server/public/assets/{diagram-5BDNPKRD-zyis6nPt.js → diagram-5BDNPKRD-Bbf4mT8a.js} +1 -1
  137. package/payload/server/public/assets/{diagram-G4DWMVQ6-BiU0XAAD.js → diagram-G4DWMVQ6-BpqVTUmx.js} +1 -1
  138. package/payload/server/public/assets/{diagram-MMDJMWI5-rGFiz1SX.js → diagram-MMDJMWI5-DY-k5k6r.js} +1 -1
  139. package/payload/server/public/assets/{diagram-TYMM5635-B0wak1ZJ.js → diagram-TYMM5635-CXHxwacS.js} +1 -1
  140. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D8AUxLyX.js → erDiagram-SMLLAGMA-BNKEc3l8.js} +1 -1
  141. package/payload/server/public/assets/{flatten-naJ5PJ4f.js → flatten-jn8bZgcI.js} +1 -1
  142. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BtdUf68b.js → flowDiagram-DWJPFMVM-CbAeuJsi.js} +1 -1
  143. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-C-lkzDmf.js → ganttDiagram-T4ZO3ILL-CeeNnL4l.js} +1 -1
  144. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BBHsY16R.js +1 -0
  145. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DRUKumq2.js → gitGraphDiagram-UUTBAWPF-DG8RCtfg.js} +1 -1
  146. package/payload/server/public/assets/{graph-DRloNic4.js → graph-BAoktBcx.js} +19 -19
  147. package/payload/server/public/assets/graph-labels-LeIhl7ZG.js +1 -0
  148. package/payload/server/public/assets/{graphlib-zRsaoFEC.js → graphlib-BkDExPQi.js} +1 -1
  149. package/payload/server/public/assets/info-OMHHGYJF-C1j5I_zx.js +1 -0
  150. package/payload/server/public/assets/infoDiagram-42DDH7IO-ZWkwHWEu.js +2 -0
  151. package/payload/server/public/assets/{isEmpty-Bnpg7b8f.js → isEmpty-Dkk6_L-B.js} +1 -1
  152. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BguMgoR8.js → ishikawaDiagram-UXIWVN3A-D_iMHV64.js} +1 -1
  153. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CXhyK1n-.js → journeyDiagram-VCZTEJTY-BvPRpulx.js} +1 -1
  154. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-C0UCWVx-.js → kanban-definition-6JOO6SKY-CkWaXWo2.js} +1 -1
  155. package/payload/server/public/assets/{line--uhS9YDE.js → line-BGQT9unx.js} +1 -1
  156. package/payload/server/public/assets/{linear-CAOWPodQ.js → linear-DzrYI8-8.js} +1 -1
  157. package/payload/server/public/assets/{maximize-2-CzDaBugs.js → maximize-2-2MgPSwlx.js} +1 -1
  158. package/payload/server/public/assets/{mermaid-parser.core-D6kPx7vc.js → mermaid-parser.core-DrMY7FMe.js} +2 -2
  159. package/payload/server/public/assets/{mermaid.core-CTd4DS6O.js → mermaid.core-BWdf6fZ7.js} +3 -3
  160. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-_AyvyG8C.js → mindmap-definition-QFDTVHPH-CgqM9h97.js} +1 -1
  161. package/payload/server/public/assets/operator-CZifmNhp.js +1 -0
  162. package/payload/server/public/assets/{ordinal-Wx4m9zHk.js → ordinal-DuJYM7p1.js} +1 -1
  163. package/payload/server/public/assets/packet-4T2RLAQJ-CGiTbe0N.js +1 -0
  164. package/payload/server/public/assets/{page-ef-6rRPL.js → page-B4kllGd_.js} +6 -6
  165. package/payload/server/public/assets/page-DbfE7QwJ.js +1 -0
  166. package/payload/server/public/assets/{pdf-render-CeX6d2v7.js → pdf-render-riHufgrn.js} +1 -1
  167. package/payload/server/public/assets/pie-ZZUOXDRM-BrK023dE.js +1 -0
  168. package/payload/server/public/assets/{pieDiagram-DEJITSTG-6TsUDkkQ.js → pieDiagram-DEJITSTG-BeUGoYS7.js} +1 -1
  169. package/payload/server/public/assets/public-VtmyYz8E.js +1 -0
  170. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CWw9gOI4.js → quadrantDiagram-34T5L4WZ-CX82WgKy.js} +1 -1
  171. package/payload/server/public/assets/radar-PYXPWWZC-p7NRamXX.js +1 -0
  172. package/payload/server/public/assets/{reduce-BUmc8RDR.js → reduce-COyotL2V.js} +1 -1
  173. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DCOlxNvx.js → requirementDiagram-MS252O5E-CzqebQXj.js} +1 -1
  174. package/payload/server/public/assets/{rotate-ccw-DXEKUhXP.js → rotate-ccw-DaZdB2cz.js} +1 -1
  175. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CW4Xbir3.js → sankeyDiagram-XADWPNL6-CaHi1yzU.js} +1 -1
  176. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-vhb_pBLG.js → sequenceDiagram-FGHM5R23-BF2AeiKW.js} +1 -1
  177. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Cb1WExEX.js → stateDiagram-FHFEXIEX-DtUCA8Ih.js} +1 -1
  178. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-a4FvB3L8.js +1 -0
  179. package/payload/server/public/assets/tasks-CvqVDbfO.js +1 -0
  180. package/payload/server/public/assets/{time-entry-format-CsYAjplS.js → time-entry-format-TD-PPEIu.js} +1 -1
  181. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BAHTf_AF.js → timeline-definition-GMOUNBTQ-D3Q1YrKY.js} +1 -1
  182. package/payload/server/public/assets/treeView-SZITEDCU-CoWk8VoG.js +1 -0
  183. package/payload/server/public/assets/treemap-W4RFUUIX-BhWeGj8Y.js +1 -0
  184. package/payload/server/public/assets/{triangle-alert-0lSBIUg-.js → triangle-alert-BaLhVNnn.js} +1 -1
  185. package/payload/server/public/assets/useCopyFeedback-BhjPzRa3.js +1 -0
  186. package/payload/server/public/assets/useSubAccountSwitcher-CWRpVt1-.css +1 -0
  187. package/payload/server/public/assets/{useSubAccountSwitcher-BZnzc5C3.js → useSubAccountSwitcher-HEVLb6Z9.js} +2 -2
  188. package/payload/server/public/assets/useVoiceRecorder-CfGgj3vm.js +2 -0
  189. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DtEttvW4.js → vennDiagram-DHZGUBPP-DnjzQDGf.js} +1 -1
  190. package/payload/server/public/assets/wardley-RL74JXVD-C0q3Hqng.js +1 -0
  191. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-K06y6CVJ.js → wardleyDiagram-NUSXRM2D-T3WHrUvi.js} +1 -1
  192. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D66c40lp.js → xychartDiagram-5P7HB3ND-2Efyv4h2.js} +1 -1
  193. package/payload/server/public/brand-defaults.css +1 -0
  194. package/payload/server/public/browser.html +4 -5
  195. package/payload/server/public/calendar.html +5 -6
  196. package/payload/server/public/chat.html +12 -13
  197. package/payload/server/public/data.html +11 -12
  198. package/payload/server/public/graph.html +10 -11
  199. package/payload/server/public/index.html +14 -15
  200. package/payload/server/public/operator.html +14 -15
  201. package/payload/server/public/privacy.html +1 -0
  202. package/payload/server/public/public.html +12 -13
  203. package/payload/server/public/tasks.html +7 -5
  204. package/payload/server/public/vnc-popout.html +1 -0
  205. package/payload/server/server.js +833 -389
  206. package/payload/server/{src-XQ63FPRE.js → src-JJ6PHO5I.js} +1 -1
  207. package/payload/server/public/assets/AdminShell-D9zaUxPd.js +0 -2
  208. package/payload/server/public/assets/Checkbox-DcbbAIe5.js +0 -1
  209. package/payload/server/public/assets/activity-C9P5NjEZ.js +0 -1
  210. package/payload/server/public/assets/admin-D2KLZnQW.js +0 -1
  211. package/payload/server/public/assets/architecture-YZFGNWBL-CZQzGRSM.js +0 -1
  212. package/payload/server/public/assets/browser-CDWqEiCD.js +0 -1
  213. package/payload/server/public/assets/calendar-BG7fOpbB.js +0 -1
  214. package/payload/server/public/assets/channel-zDUAN9Ks.js +0 -1
  215. package/payload/server/public/assets/chat-RyH-WYQC.js +0 -1
  216. package/payload/server/public/assets/chevron-left-DmXCr6nB.js +0 -1
  217. package/payload/server/public/assets/chunk-426QAEUC-iJnqNRug.js +0 -1
  218. package/payload/server/public/assets/chunk-QZHKN3VN-Qlc2PFAj.js +0 -1
  219. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CFsKL52p.js +0 -1
  220. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BWBUUCiO.js +0 -1
  221. package/payload/server/public/assets/clone-h5XhkDUL.js +0 -1
  222. package/payload/server/public/assets/data-BaY_LkLR.js +0 -1
  223. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CBZPQJLj.js +0 -1
  224. package/payload/server/public/assets/graph-labels-DD-GqS4v.js +0 -1
  225. package/payload/server/public/assets/info-OMHHGYJF-1vIoX-kP.js +0 -1
  226. package/payload/server/public/assets/infoDiagram-42DDH7IO-KHcXRaan.js +0 -2
  227. package/payload/server/public/assets/operator-BZeeyait.js +0 -1
  228. package/payload/server/public/assets/packet-4T2RLAQJ-CTU0gCGb.js +0 -1
  229. package/payload/server/public/assets/page-B0obcg3b.js +0 -1
  230. package/payload/server/public/assets/pie-ZZUOXDRM-D-EEHxhC.js +0 -1
  231. package/payload/server/public/assets/public-9D3MMOkC.js +0 -1
  232. package/payload/server/public/assets/radar-PYXPWWZC-DHXATZGB.js +0 -1
  233. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CxOIKvXh.js +0 -1
  234. package/payload/server/public/assets/tasks-BB0L7e5a.js +0 -1
  235. package/payload/server/public/assets/treeView-SZITEDCU-CC7ouMFd.js +0 -1
  236. package/payload/server/public/assets/treemap-W4RFUUIX-CaCg27im.js +0 -1
  237. package/payload/server/public/assets/useCopyFeedback-jTBQ2h0V.js +0 -1
  238. package/payload/server/public/assets/useSelectionMode-BooArySZ.js +0 -1
  239. package/payload/server/public/assets/useSubAccountSwitcher-D9knHOKq.css +0 -1
  240. package/payload/server/public/assets/useVoiceRecorder-PUT8q_sm.js +0 -2
  241. package/payload/server/public/assets/wardley-RL74JXVD-bMPsqQZ7.js +0 -1
  242. /package/payload/server/public/assets/{_baseFor-k8hYPe-U.js → _baseFor-Bjj9It3Q.js} +0 -0
  243. /package/payload/server/public/assets/{array-BFDiaBgf.js → array-BrrxEo12.js} +0 -0
  244. /package/payload/server/public/assets/{cytoscape.esm-Ch0xptA4.js → cytoscape.esm-CTWJxUxo.js} +0 -0
  245. /package/payload/server/public/assets/{defaultLocale-ZeknFqNe.js → defaultLocale-CFDbRV4k.js} +0 -0
  246. /package/payload/server/public/assets/{dist-B2N0v8hZ.js → dist-BATfy7Sn.js} +0 -0
  247. /package/payload/server/public/assets/{init-0VJVrkRJ.js → init-dXCzGgLP.js} +0 -0
  248. /package/payload/server/public/assets/{katex-Br7bH10N.js → katex-CHA1JFYJ.js} +0 -0
  249. /package/payload/server/public/assets/{path-INs8XTPH.js → path-CCRpdXhK.js} +0 -0
  250. /package/payload/server/public/assets/{preload-helper-D_sPnjeL.js → preload-helper-CH6UZRzu.js} +0 -0
  251. /package/payload/server/public/assets/{rough.esm-DU9bZLvZ.js → rough.esm-Bud4g4fh.js} +0 -0
  252. /package/payload/server/public/assets/{src-DHSQDYx-.js → src-DNdBRXXx.js} +0 -0
@@ -1,8 +1,17 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs";
3
+ import {
4
+ mkdtempSync,
5
+ mkdirSync,
6
+ writeFileSync,
7
+ appendFileSync,
8
+ existsSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ symlinkSync,
12
+ } from "node:fs";
4
13
  import { tmpdir } from "node:os";
5
- import { join } from "node:path";
14
+ import { join, dirname } from "node:path";
6
15
  import { makeCfExec, makeHouseCfExec, type RunFn, type FetchFn } from "../cf-exec.js";
7
16
 
8
17
  const cred = { apiToken: "cfat_x", accountId: "acc-h" };
@@ -685,40 +694,208 @@ function projectPage(names: string[], cursor?: string) {
685
694
  });
686
695
  }
687
696
 
697
+ // A real site tree on disk. pagesDeploy copies the tree before uploading it, so
698
+ // these tests cannot hand it a path that does not exist.
699
+ function siteTree(files: Record<string, string>): string {
700
+ const dir = mkdtempSync(join(tmpdir(), "pages-site-"));
701
+ for (const [rel, body] of Object.entries(files)) {
702
+ const abs = join(dir, rel);
703
+ mkdirSync(dirname(abs), { recursive: true });
704
+ writeFileSync(abs, body);
705
+ }
706
+ return dir;
707
+ }
708
+
709
+ const PORTAL = {
710
+ "index.html": "<!doctype html>",
711
+ "portal.js": "export {}",
712
+ "functions/api/files.js": "export {}",
713
+ "wrangler.toml": 'name = "kit-data"\ndatabase_id = "secret-looking"\n',
714
+ };
715
+
716
+ /** Snapshots the uploaded directory's contents at call time, because
717
+ * pagesDeploy deletes the staging tree before it returns. */
718
+ function snapshottingRun(stdout: string) {
719
+ const calls: Array<{
720
+ args: string[];
721
+ cwd?: string;
722
+ uploadDir: string;
723
+ uploaded: string[];
724
+ }> = [];
725
+ const run: RunFn = async (_cmd, args, _env, opts) => {
726
+ const uploadDir = args[3];
727
+ const uploaded: string[] = [];
728
+ const walk = (d: string, prefix: string) => {
729
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
730
+ if (entry.isDirectory()) walk(join(d, entry.name), `${prefix}${entry.name}/`);
731
+ else uploaded.push(`${prefix}${entry.name}`);
732
+ }
733
+ };
734
+ walk(uploadDir, "");
735
+ calls.push({ args, cwd: opts?.cwd, uploadDir, uploaded: uploaded.sort() });
736
+ return { stdout };
737
+ };
738
+ return { run, calls };
739
+ }
740
+
741
+ const DEPLOYED = "✨ Deployment complete! Take a peek over at https://abc123.glsmithandsons.pages.dev\n";
742
+
688
743
  test("pagesDeploy shells npx wrangler (never bare) and returns the deployment url", async () => {
689
- const { run, calls } = recordingRun(
690
- "Uploading... (2/2)\n✨ Deployment complete! Take a peek over at https://abc123.glsmithandsons.pages.dev\n",
691
- );
744
+ const dir = siteTree(PORTAL);
745
+ const { run, calls } = recordingRun(`Uploading... (2/2)\n${DEPLOYED}`);
692
746
  const cf = makeCfExec(cred, run);
693
- const { url } = await cf.pagesDeploy("/acct/sites/gls", "glsmithandsons", "main");
747
+ const { url } = await cf.pagesDeploy(dir, "glsmithandsons", "main");
694
748
  assert.equal(url, "https://abc123.glsmithandsons.pages.dev");
695
749
  assert.equal(calls[0].cmd, "npx");
696
- assert.deepEqual(calls[0].args, [
697
- "wrangler", "pages", "deploy", "/acct/sites/gls",
750
+ assert.deepEqual(calls[0].args.slice(0, 3), ["wrangler", "pages", "deploy"]);
751
+ assert.deepEqual(calls[0].args.slice(4), [
698
752
  "--project-name", "glsmithandsons", "--branch", "main",
699
753
  ]);
700
754
  assert.equal(calls[0].env.CLOUDFLARE_API_TOKEN, "cfat_x");
701
755
  });
702
756
 
703
757
  test("pagesDeploy omits --branch when no branch is given", async () => {
704
- const { run, calls } = recordingRun("✨ Deployment complete! Take a peek over at https://x.y.pages.dev\n");
758
+ const dir = siteTree(PORTAL);
759
+ const { run, calls } = recordingRun(DEPLOYED);
705
760
  const cf = makeCfExec(cred, run);
706
- await cf.pagesDeploy("/acct/sites/gls", "glsmithandsons");
707
- assert.deepEqual(calls[0].args, [
708
- "wrangler", "pages", "deploy", "/acct/sites/gls", "--project-name", "glsmithandsons",
709
- ]);
761
+ await cf.pagesDeploy(dir, "glsmithandsons");
762
+ assert.deepEqual(calls[0].args.slice(4), ["--project-name", "glsmithandsons"]);
710
763
  });
711
764
 
712
765
  test("pagesDeploy throws with the body when wrangler prints no deployment url", async () => {
766
+ const dir = siteTree(PORTAL);
713
767
  const { run } = recordingRun("Uploading... (0/0)\nsomething went sideways\n");
714
768
  const cf = makeCfExec(cred, run);
715
769
  await assert.rejects(
716
- () => cf.pagesDeploy("/acct/sites/gls", "glsmithandsons"),
770
+ () => cf.pagesDeploy(dir, "glsmithandsons"),
717
771
  /something went sideways/,
718
772
  "a url-less wrangler run must surface its output, never report a silent success",
719
773
  );
720
774
  });
721
775
 
776
+ // --- Task 1782: the uploaded set is the broker's, not whatever the tree holds ---
777
+
778
+ test("pagesDeploy uploads a staged copy with the wrangler config left out", async () => {
779
+ const dir = siteTree(PORTAL);
780
+ const { run, calls } = snapshottingRun(DEPLOYED);
781
+ const cf = makeCfExec(cred, run);
782
+ const { excluded } = await cf.pagesDeploy(dir, "kit-data", "main");
783
+ assert.notEqual(
784
+ calls[0].uploadDir,
785
+ dir,
786
+ "the directory handed to wrangler must not be the tree that holds the config",
787
+ );
788
+ assert.deepEqual(
789
+ calls[0].uploaded,
790
+ ["functions/api/files.js", "index.html", "portal.js"],
791
+ "every site file is published and the config is not",
792
+ );
793
+ assert.deepEqual(excluded, ["wrangler.toml"]);
794
+ });
795
+
796
+ test("pagesDeploy still runs wrangler in the tree that holds the config", async () => {
797
+ const dir = siteTree(PORTAL);
798
+ const { run, calls } = snapshottingRun(DEPLOYED);
799
+ const cf = makeCfExec(cred, run);
800
+ await cf.pagesDeploy(dir, "kit-data", "main");
801
+ // wrangler finds its config by walking up from the working directory, so the
802
+ // cwd is what keeps the D1 and R2 bindings applying once the file itself has
803
+ // left the upload. It is also still the writable scratch anchor Task 1770
804
+ // pinned it for.
805
+ assert.equal(calls[0].cwd, dir);
806
+ });
807
+
808
+ test("pagesDeploy excludes every config filename wrangler itself discovers", async () => {
809
+ const dir = siteTree({
810
+ "index.html": "<!doctype html>",
811
+ "wrangler.toml": "x",
812
+ "wrangler.json": "{}",
813
+ "wrangler.jsonc": "{}",
814
+ });
815
+ const { run, calls } = snapshottingRun(DEPLOYED);
816
+ const cf = makeCfExec(cred, run);
817
+ const { excluded } = await cf.pagesDeploy(dir, "kit-data");
818
+ assert.deepEqual(calls[0].uploaded, ["index.html"]);
819
+ assert.deepEqual(excluded, ["wrangler.json", "wrangler.jsonc", "wrangler.toml"]);
820
+ });
821
+
822
+ test("pagesDeploy keeps a nested file that merely shares the config's name", async () => {
823
+ const dir = siteTree({
824
+ "index.html": "<!doctype html>",
825
+ "wrangler.toml": "x",
826
+ "docs/wrangler.toml": "an example the site publishes on purpose",
827
+ });
828
+ const { run, calls } = snapshottingRun(DEPLOYED);
829
+ const cf = makeCfExec(cred, run);
830
+ const { excluded } = await cf.pagesDeploy(dir, "kit-data");
831
+ assert.deepEqual(calls[0].uploaded, ["docs/wrangler.toml", "index.html"]);
832
+ assert.deepEqual(excluded, ["wrangler.toml"], "only the config wrangler reads is dropped");
833
+ });
834
+
835
+ test("pagesDeploy reports nothing excluded for a tree that carries no config", async () => {
836
+ const dir = siteTree({ "index.html": "<!doctype html>" });
837
+ const { run, calls } = snapshottingRun(DEPLOYED);
838
+ const cf = makeCfExec(cred, run);
839
+ const { excluded } = await cf.pagesDeploy(dir, "glsmithandsons");
840
+ assert.deepEqual(calls[0].uploaded, ["index.html"]);
841
+ assert.deepEqual(excluded, []);
842
+ });
843
+
844
+ test("pagesDeploy publishes what a symlink pointed at, including one leaving the tree", async () => {
845
+ // wrangler's own upload walk stats each entry, so deploying in place published
846
+ // the link's target. Copying links verbatim would strand an escaping one.
847
+ // A relative link escaping the tree is the shape that would dangle if the copy
848
+ // reproduced it verbatim somewhere else on disk. It does not: node's fs.cp
849
+ // rewrites every symlink to its resolved absolute target, so the staged link
850
+ // still points at the real file and wrangler's stat-following walk publishes
851
+ // it exactly as an in-place deploy did. This pins that behaviour rather than
852
+ // trusting it — the whole change rests on the staged tree being what wrangler
853
+ // would have uploaded.
854
+ const base = mkdtempSync(join(tmpdir(), "pages-base-"));
855
+ mkdirSync(join(base, "shared"));
856
+ writeFileSync(join(base, "shared", "shared.css"), "body{}");
857
+ const dir = join(base, "site");
858
+ mkdirSync(dir);
859
+ writeFileSync(join(dir, "index.html"), "<!doctype html>");
860
+ writeFileSync(join(dir, "wrangler.toml"), "x");
861
+ symlinkSync(join("..", "shared", "shared.css"), join(dir, "theme.css"));
862
+ // Read during the run: the staging tree is gone by the time pagesDeploy returns.
863
+ let staged = "";
864
+ const run: RunFn = async (_cmd, args) => {
865
+ staged = readFileSync(join(args[3], "theme.css"), "utf8");
866
+ return { stdout: DEPLOYED };
867
+ };
868
+ const cf = makeCfExec(cred, run);
869
+ await cf.pagesDeploy(dir, "kit-data");
870
+ assert.equal(
871
+ staged,
872
+ "body{}",
873
+ "the staged copy carries the target's bytes, not a link that resolves to nothing",
874
+ );
875
+ });
876
+
877
+ test("pagesDeploy removes the staging tree on the success path", async () => {
878
+ const dir = siteTree(PORTAL);
879
+ const { run, calls } = snapshottingRun(DEPLOYED);
880
+ const cf = makeCfExec(cred, run);
881
+ await cf.pagesDeploy(dir, "kit-data");
882
+ assert.equal(existsSync(calls[0].uploadDir), false);
883
+ assert.equal(existsSync(join(dir, "wrangler.toml")), true, "the real tree is untouched");
884
+ });
885
+
886
+ test("pagesDeploy removes the staging tree when wrangler fails", async () => {
887
+ const dir = siteTree(PORTAL);
888
+ let uploadDir = "";
889
+ const run: RunFn = async (_cmd, args) => {
890
+ uploadDir = args[3];
891
+ throw new Error("wrangler exploded");
892
+ };
893
+ const cf = makeCfExec(cred, run);
894
+ await assert.rejects(() => cf.pagesDeploy(dir, "kit-data"), /wrangler exploded/);
895
+ assert.notEqual(uploadDir, "");
896
+ assert.equal(existsSync(uploadDir), false, "a failed deploy must not strand a copy of the site");
897
+ });
898
+
722
899
  test("pagesProjectList follows the pagination cursor to exhaustion via the API, never wrangler", async () => {
723
900
  const { fetchFn, calls } = sequenceFetch([
724
901
  { ok: true, status: 200, body: projectPage(["glsmithandsons"], "c1") },
@@ -804,14 +981,15 @@ test("pagesDeploy pins wrangler's working directory to the deploy dir", async ()
804
981
  const { run, calls } = recordingRun(
805
982
  "\u2728 Deployment complete! Take a peek over at https://abc.kit-data.pages.dev\n",
806
983
  );
984
+ const dir = siteTree(PORTAL);
807
985
  const cf = makeCfExec(cred, run);
808
- await cf.pagesDeploy("/acct/sites/kit-data", "kit-data", "main");
986
+ await cf.pagesDeploy(dir, "kit-data", "main");
809
987
  // Unpinned, wrangler resolves .wrangler/tmp against a directory it infers --
810
988
  // on a real install that is the immutable server tree, where the mkdir is
811
989
  // EPERM and every deploy dies before uploading.
812
990
  assert.equal(
813
991
  calls[0].cwd,
814
- "/acct/sites/kit-data",
992
+ dir,
815
993
  "wrangler must run in the deploy dir so its scratch dir lands beside the tree it uploads",
816
994
  );
817
995
  });
@@ -3,7 +3,9 @@
3
3
  // wrangler. `run` is injectable so the wiring is unit-testable without wrangler.
4
4
 
5
5
  import { execFile } from "node:child_process";
6
- import { join } from "node:path";
6
+ import { cp, mkdtemp, rm } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join, relative } from "node:path";
7
9
  import type { HouseCredential } from "./house-credential.js";
8
10
  import { readHouseCredential } from "./house-credential.js";
9
11
  import { resolveHouseScopedToken } from "./house-scoped-token.js";
@@ -60,9 +62,24 @@ export interface CfExec {
60
62
  r2ObjectDelete(bucket: string, key: string): Promise<void>;
61
63
  pagesProjectList(): Promise<{ name: string }[]>;
62
64
  pagesProjectCreate(name: string, productionBranch: string): Promise<void>;
63
- pagesDeploy(dir: string, project: string, branch?: string): Promise<{ url: string }>;
65
+ /** `excluded` names the files dropped from the upload set, so the caller can
66
+ * log what was published rather than assert it (Task 1782). */
67
+ pagesDeploy(
68
+ dir: string,
69
+ project: string,
70
+ branch?: string,
71
+ ): Promise<{ url: string; excluded: string[] }>;
64
72
  }
65
73
 
74
+ // The filenames `wrangler pages deploy` discovers as its own configuration
75
+ // (wrangler 4.112.0). Task 1782 — the upload walk has a fixed ignore list with
76
+ // no config hook, so a config sitting in the deployed tree is served publicly at
77
+ // /wrangler.toml, disclosing the D1 database id and R2 bucket name, both of
78
+ // which embed the account id. All three names are dropped rather than only the
79
+ // .toml the data-portal template ships, so a tree that later gains a .json
80
+ // config cannot quietly start republishing it.
81
+ const WRANGLER_CONFIG_NAMES = ["wrangler.json", "wrangler.jsonc", "wrangler.toml"];
82
+
66
83
  // An object key carries `/` as a real path separator (the data portal's
67
84
  // per-person prefixes), so it is encoded segment-wise rather than whole:
68
85
  // separators stay separators, and spaces and specials are escaped. This
@@ -381,34 +398,62 @@ export function makeCfExec(
381
398
  }
382
399
  },
383
400
  async pagesDeploy(dir, project, branch) {
384
- // wrangler is the only supported Pages upload path (it hashes the tree and
385
- // uploads only what is missing); the API has no single-call equivalent.
386
- const args = ["wrangler", "pages", "deploy", dir, "--project-name", project];
387
- if (branch !== undefined) args.push("--branch", branch);
388
- // Pin the working directory to the tree being published. Left to
389
- // inference, wrangler anchors `.wrangler/tmp` at the install's `server/`
390
- // directory (its nearest package.json/node_modules root), which the
391
- // platform marks immutable so the scratch mkdir returns EPERM and the
392
- // deploy never reaches the upload. `dir` is the realpath-confined deploy
393
- // directory the route resolved, it is writable, and it already holds this
394
- // site's own wrangler.toml.
395
- const { stdout } = await run("npx", args, env, { cwd: dir });
396
- // The deployment URL is what the caller relays, so parse it strictly.
397
- // Strip ANSI first: wrangler colours its output when it thinks it has a
398
- // terminal, and a colour reset adjacent to the URL would otherwise be
399
- // swallowed by the host character class and returned as part of it.
400
- const plain = stdout.replace(/\[[0-9;]*m/g, "");
401
- // Take the LAST match, not the first: wrangler prints the deployment URL
402
- // on its closing line, and any earlier .pages.dev mention (an alias or a
403
- // progress line) would otherwise win and hand back the wrong address.
404
- const urls = plain.match(/https:\/\/[A-Za-z0-9._-]+\.pages\.dev/g);
405
- const url = urls?.[urls.length - 1];
406
- // wrangler exits 0 on some partial paths, so a url-less success is an
407
- // anomaly to surface with its output, never a silent "deployed".
408
- if (!url) {
409
- throw new Error(`storage-broker: pages deploy returned no deployment url: ${stdout}`);
401
+ // Task 1782 publish a directory the broker composes, not whatever the
402
+ // account's tree happens to hold. wrangler takes the upload directory from
403
+ // the positional argument and finds its configuration by walking up from
404
+ // the working directory, which are two independent inputs; pointing them
405
+ // at different places is what removes the config from the published assets
406
+ // while leaving the D1 and R2 bindings it declares fully in force.
407
+ // Measured on the house account 2026-07-20: a brand-new project deployed
408
+ // with the config outside the upload received the same bindings and the
409
+ // same config hash as the control that uploaded it.
410
+ const staged = await mkdtemp(join(tmpdir(), "maxy-pages-upload-"));
411
+ const excluded: string[] = [];
412
+ try {
413
+ await cp(dir, staged, {
414
+ recursive: true,
415
+ // Only a config at the root of the tree is wrangler's own. A nested
416
+ // file of the same name is site content and stays published.
417
+ filter: (src) => {
418
+ const rel = relative(dir, src);
419
+ if (!WRANGLER_CONFIG_NAMES.includes(rel)) return true;
420
+ excluded.push(rel);
421
+ return false;
422
+ },
423
+ });
424
+ // wrangler is the only supported Pages upload path (it hashes the tree and
425
+ // uploads only what is missing); the API has no single-call equivalent.
426
+ const args = ["wrangler", "pages", "deploy", staged, "--project-name", project];
427
+ if (branch !== undefined) args.push("--branch", branch);
428
+ // Pin the working directory to the tree being published. Left to
429
+ // inference, wrangler anchors `.wrangler/tmp` at the install's `server/`
430
+ // directory (its nearest package.json/node_modules root), which the
431
+ // platform marks immutable — so the scratch mkdir returns EPERM and the
432
+ // deploy never reaches the upload. `dir` is the realpath-confined deploy
433
+ // directory the route resolved, and it is writable. Since 1782 it is also
434
+ // the only place wrangler can still find this site's config, which no
435
+ // longer travels with the assets.
436
+ const { stdout } = await run("npx", args, env, { cwd: dir });
437
+ // The deployment URL is what the caller relays, so parse it strictly.
438
+ // Strip ANSI first: wrangler colours its output when it thinks it has a
439
+ // terminal, and a colour reset adjacent to the URL would otherwise be
440
+ // swallowed by the host character class and returned as part of it.
441
+ const plain = stdout.replace(/\[[0-9;]*m/g, "");
442
+ // Take the LAST match, not the first: wrangler prints the deployment URL
443
+ // on its closing line, and any earlier .pages.dev mention (an alias or a
444
+ // progress line) would otherwise win and hand back the wrong address.
445
+ const urls = plain.match(/https:\/\/[A-Za-z0-9._-]+\.pages\.dev/g);
446
+ const url = urls?.[urls.length - 1];
447
+ // wrangler exits 0 on some partial paths, so a url-less success is an
448
+ // anomaly to surface with its output, never a silent "deployed".
449
+ if (!url) {
450
+ throw new Error(`storage-broker: pages deploy returned no deployment url: ${stdout}`);
451
+ }
452
+ return { url, excluded: excluded.sort() };
453
+ } finally {
454
+ // A failed deploy must not strand a second copy of the site on disk.
455
+ await rm(staged, { recursive: true, force: true });
410
456
  }
411
- return { url };
412
457
  },
413
458
 
414
459
  // --- R2 objects (Task 1691) -------------------------------------------
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:6009a50cf8e577364b02bee6ca6a47c112f463807f891975c445989d027176f0
4
+ content-hash: sha256:c7a28352289a7121b08a8d8f2d5bcb1e79497a6acda2e355756cf7b02051c3a5
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -1285,6 +1285,18 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
1285
1285
  - The `/data` page handles this itself: files over 95 MB are split into ~48 MB parts and sent one at a time under the cap, and the server reassembles them. No plan change or dashboard action is needed for uploads up to the 2 GB app ceiling.
1286
1286
  - Raising the proxied body limit is a paid-plan change made on the Cloudflare dashboard, not something the agent does.
1287
1287
 
1288
+ ## The client file portal
1289
+
1290
+ A portal gives named people their own passcode-protected page on a custom domain. Two things sit on it.
1291
+
1292
+ **What they send you.** Anything they upload lands in the account's own storage, one area per person, and nobody sees anyone else's files.
1293
+
1294
+ **What you have prepared for them.** The same page shows the account's deliverable folders — the work produced for that client. Which folders those are is not a setting to maintain: it follows the account's own filing structure, so a business whose work is organised into quotes sees quotes, and a business organised some other way sees that instead. Operator working data, the client's own uploads, and anything internal are never shown.
1295
+
1296
+ Folder names and sizes are published to the portal on a schedule, so the client can browse even while the device is off. The files themselves stay on the device. Opening one checks the device is reachable and then hands the browser a link that expires after a few minutes; if the device is down the client is told exactly that, rather than getting a broken download.
1297
+
1298
+ Each person is bound to one account when they are enrolled. Someone enrolled before this existed has no account bound and sees no folders until they are enrolled again.
1299
+
1288
1300
  ## What the agent does and does not do
1289
1301
 
1290
1302
  **Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
@@ -34,7 +34,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
34
34
  | [cloudflare/SKILL.md](skills/cloudflare/SKILL.md) | The entry point for every Cloudflare operation. Routes each operation class to its reference, names the outcome contracts (tunnel external HTTP 200; hosting deployed URL; D1 round-trip), and states the tool-discipline + secret-redaction rules that bind the agent. |
35
35
  | [site-deploy/SKILL.md](skills/site-deploy/SKILL.md) | Assemble one canonical content tree into a live Pages deployment on its custom domain that survives the install device being offline. Names the single-source-tree convention, the reused per-scope token, the form-to-D1 trigger, the R2 binding trigger, Open Graph correctness, and the cache-busted live-`200` done-gate; defers the command blocks to `hosting-sites.md`, `d1-data-capture.md` and `r2-object-storage.md`. Admin-run; a content specialist hands the deploy off to it. |
36
36
  | [calendar-site/SKILL.md](skills/calendar-site/SKILL.md) | Assemble and deploy one account's public booking page — a static page plus edge Functions over a D1 `bookings` table — on its own custom domain, so a submission survives the install device being offline. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
37
- | [data-portal/SKILL.md](skills/data-portal/SKILL.md) | Stand up one account's gated file-drop portal on its own custom domain: each enrolled person signs in with their own passcode and can upload, list, download and delete only their own files, into one R2 bucket under a per-person prefix with a D1 manifest row per upload. The bucket is created by the owning account's own session — the broker registers ownership to the creator and never reassigns it — while the admin assembles and deploys. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
37
+ | [data-portal/SKILL.md](skills/data-portal/SKILL.md) | Stand up one account's gated file-drop portal on its own custom domain: each enrolled person signs in with their own passcode and can upload, list, download and delete only their own files, into one R2 bucket under a per-person prefix with a D1 manifest row per upload. The same portal also shows that sub-account's deliverable folders, published from the device as metadata only and opened through a short-lived signed link. The bucket is created by the owning account's own session — the broker registers ownership to the creator and never reassigns it — while the admin assembles and deploys. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
38
38
 
39
39
  ### References
40
40
 
@@ -69,6 +69,31 @@ The setup-done claim only fires when `curl -I https://<hostname>` issued from ou
69
69
  - **Cloudflare account identity** — `cert.pem` from OAuth. One account per brand per device.
70
70
  - **Account binding drift** — `~/{configDir}/cloudflared/account-binding.json` is a historical drift marker. Reset by `rm -rf ~/.${BRAND}/cloudflared/` per `references/reset-guide.md` when switching accounts.
71
71
 
72
+ ## Data-portal folder index
73
+
74
+ The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made.
75
+
76
+ `bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
77
+
78
+ Opening a file reaches the install: the portal probes it, then mints an `/api/portal/fetch` link signed with the account's `PORTAL_FETCH_SECRET` and valid for five minutes. The install verifies the signature **and independently re-derives the exposed folder set**, so a correctly signed link for a non-exposed path is still refused. That second check is the security boundary, not the signature.
79
+
80
+ What to watch, on the device:
81
+
82
+ ```
83
+ [portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> exposed=<dirs|none>
84
+ [portal-index] op=replace account=<id> deleted=all inserted=<n>
85
+ [portal-index] op=verify account=<id> rowsAfter=<n> expected=<n>
86
+ [portal-index] op=collision account=<id> dir=<dir> claimedBy=plugin-owned
87
+ [portal-index] op=exposed-changed account=<id> added=<dirs|none> removed=<dirs|none>
88
+ [portal-index] op=audit accounts=<n> stale=<n> never-ran=<n>
89
+ ```
90
+
91
+ `exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression.
92
+
93
+ `op=audit` is the standing check, and it is the only one that speaks when the push does **not**. It runs in-process inside the heartbeat rather than as a spawn, and reads only local state, so it cannot be blinded by the same failure it is watching for. It fires hourly even when everything is healthy — an audit that goes quiet on success is indistinguishable from an audit that stopped running. `result=never-ran` on an account with a `data-portal.json` means no cycle has ever succeeded for it; `result=stale` with `ageMin` means cycles have stopped. Its input is written only after `op=verify`, so a stale entry means the push stopped succeeding rather than merely stopped logging.
94
+
95
+ On the portal side `[data-portal] op=list` carries `account=` and `op=fetch-through` carries the outcome; on the install `[portal-fetch]` logs `op=request` then exactly one of `op=served`, `op=denied`, `op=miss`. A portal `fetch-through` with no matching install `op=request` means the request never arrived, rather than having been refused. `[portal-fetch] op=no-secret account=<id>` is an unconfigured portal, which the `op=denied reason=secret` line alone would not distinguish from a bad signature.
96
+
72
97
  ## Discipline
73
98
 
74
99
  The agent's permitted surfaces are: direct `cloudflared` and `wrangler` invocations via Bash following the references; Cloudflare API calls authenticated by a reused per-scope narrow token, minted once if absent and persisted to the secrets file (the master token stays in that same file, never on a command line that echoes, never in chat); the reference files; and live verification (`curl -I` for tunnels, the deployed URL for hosting, a `SELECT` for D1). Out of bounds: Playwright or Chrome DevTools driving the dashboard, browser-automating master-token creation, WebSearch-for-CF-recipes, ad-hoc `cloudflared` flag invention not in the runbook, and writing or echoing any token. When a step fails, the agent reports the exact output (secrets redacted), cites the recovery step from `references/reset-guide.md`, and stops.
@@ -0,0 +1,80 @@
1
+ // D1 over the Cloudflare HTTP API, with bound parameters.
2
+ //
3
+ // Not `wrangler d1 execute`: that spawns a process per statement, which is the
4
+ // CPU-burn defect Task 1805 fixed, and it takes a raw SQL string, which would
5
+ // mean interpolating filenames into SQL. The index push writes one row per file,
6
+ // so both matter here.
7
+ //
8
+ // storage-broker's cf-exec.d1Query still spawns wrangler, and the scheduling
9
+ // plugin has its own HTTP client. Converging the three is a follow-up; this
10
+ // client depends on neither, because the cloudflare plugin is where Cloudflare
11
+ // access belongs and importing the scheduling plugin from here would invert that.
12
+
13
+ const API = 'https://api.cloudflare.com/client/v4'
14
+
15
+ /**
16
+ * Always carries the response body into the message. A bare status renders
17
+ * every failure identical and unactionable.
18
+ * @param {unknown} parsed @param {number} status @param {string} label
19
+ * @returns {never}
20
+ */
21
+ function fail(parsed, status, label) {
22
+ const errs = /** @type {{errors?: {message?: string}[]}} */ (parsed)?.errors
23
+ const detail = Array.isArray(errs) && errs.length ? errs.map((e) => e?.message).join('; ') : ''
24
+ throw new Error(`${label} failed: HTTP ${status}${detail ? ` — ${detail}` : ''}`)
25
+ }
26
+
27
+ /**
28
+ * @param {{ accountId: string, token: string, dbName: string,
29
+ * fetchFn?: (url: string, init: object) => Promise<{ ok: boolean, status: number, json: () => Promise<unknown> }> }} opts
30
+ */
31
+ export function createD1Client(opts) {
32
+ const fetchFn = opts.fetchFn ?? /** @type {never} */ (globalThis.fetch)
33
+ const headers = { Authorization: `Bearer ${opts.token}`, 'Content-Type': 'application/json' }
34
+ /** @type {string | undefined} */ let databaseId
35
+ let statements = 0
36
+
37
+ async function resolveId() {
38
+ if (databaseId !== undefined) return databaseId
39
+ const url = `${API}/accounts/${opts.accountId}/d1/database?name=${encodeURIComponent(opts.dbName)}`
40
+ const res = await fetchFn(url, { method: 'GET', headers })
41
+ const parsed = await res.json()
42
+ if (!res.ok) fail(parsed, res.status, 'd1 database lookup')
43
+ const list = /** @type {{result?: {name?: string, uuid?: string}[]}} */ (parsed).result ?? []
44
+ // Exact name match, never "the first result": `?name=` is a filter whose
45
+ // matching rule is not documented, so taking position on trust would let a
46
+ // near-name run this account's statements against the wrong database.
47
+ const hit = list.find((d) => d.name === opts.dbName)
48
+ if (!hit || typeof hit.uuid !== 'string') {
49
+ throw new Error(`no D1 database named "${opts.dbName}" in account ${opts.accountId}`)
50
+ }
51
+ databaseId = hit.uuid
52
+ return databaseId
53
+ }
54
+
55
+ /** @param {string} sql @param {unknown[]} [params] */
56
+ async function query(sql, params = []) {
57
+ const id = await resolveId()
58
+ // Counted before the await: `statements` is what reached D1, and a
59
+ // statement that was issued and then errored still went. Counting successes
60
+ // only would report 0 both for "the lookup failed so nothing was sent" and
61
+ // for "one statement was sent and errored".
62
+ statements++
63
+ const res = await fetchFn(`${API}/accounts/${opts.accountId}/d1/database/${id}/query`, {
64
+ method: 'POST',
65
+ headers,
66
+ body: JSON.stringify({ sql, params }),
67
+ })
68
+ const parsed = await res.json()
69
+ if (!res.ok) fail(parsed, res.status, 'd1 query')
70
+ const first = (/** @type {{result?: {results?: unknown}[]}} */ (parsed).result ?? [])[0]
71
+ return /** @type {Record<string, unknown>[]} */ (first?.results ?? [])
72
+ }
73
+
74
+ return {
75
+ query,
76
+ get statements() {
77
+ return statements
78
+ },
79
+ }
80
+ }
@@ -20,7 +20,7 @@
20
20
  //
21
21
  // Usage:
22
22
  // node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
23
- // | npx wrangler d1 execute <db> --remote --file=-
23
+ // --account <accountId> | npx wrangler d1 execute <db> --remote --file=-
24
24
 
25
25
  import {
26
26
  PASSCODE_LENGTH,
@@ -60,6 +60,17 @@ if (!OWNER_ID.test(/** @type {string} */ (ownerId))) {
60
60
  // the literal harmlessly — but it breaks the pipeable contract, so refuse it.
61
61
  if (/[\r\n]/.test(/** @type {string} */ (name))) die('--name cannot contain a line break')
62
62
 
63
+ // The sub-account whose deliverable folders this person may read. Required, and
64
+ // validated to the same charset the account partition uses: a person enrolled
65
+ // with no account resolves to an EMPTY folder list, which is indistinguishable
66
+ // from a sub-account that simply has no deliverables. That is a failure nobody
67
+ // diagnoses, so it fails here instead — loudly, before the row exists.
68
+ const accountId = arg('--account')
69
+ if (!accountId) die('--account <accountId> is required')
70
+ if (!/^[A-Za-z0-9-]+$/.test(/** @type {string} */ (accountId))) {
71
+ die('--account must match ^[A-Za-z0-9-]+$ — an account id, not a path')
72
+ }
73
+
63
74
  const saltBytes = new Uint8Array(16)
64
75
  crypto.getRandomValues(saltBytes)
65
76
  const saltHex = randomSaltHex(saltBytes)
@@ -84,8 +95,12 @@ const createdAt = new Date().toISOString()
84
95
  //
85
96
  // `createdAt` is deliberately absent from the DO UPDATE: it records when the
86
97
  // person was enrolled, and rotating a passcode is not creating a person.
98
+ //
99
+ // `accountId` IS in the DO UPDATE: re-enrolling someone against a different
100
+ // sub-account has to move them, or they keep reading folders they no longer
101
+ // belong to.
87
102
  console.log(
88
- `INSERT INTO people (ownerId, name, salt, hash, createdAt) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}') ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash;`,
103
+ `INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}') ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId;`,
89
104
  )
90
105
 
91
106
  // stderr: everything for the human. Never stdout — see the note at the top.