@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
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env node
2
+ // Publish a sub-account's deliverable file INDEX to its portal's D1.
3
+ //
4
+ // Metadata only. The bytes stay on the device and are fetched on demand through
5
+ // a short-lived signed link, which is what lets the portal render the folder
6
+ // tree while the device is offline.
7
+ //
8
+ // Full replace per account rather than a diff: a delete or a rename on the
9
+ // device converges on the next cycle with no tombstone protocol to get wrong.
10
+ //
11
+ // Runnable standalone as well as from the heartbeat, because the heartbeat spawn
12
+ // this copies has never been observed running on a device (Task 1828). If the
13
+ // hourly run is silently dead, this script still works by hand.
14
+ //
15
+ // node portal-index-push.mjs --account-dir <dir> --account <id> --db <name>
16
+ // node portal-index-push.mjs --all-accounts
17
+ //
18
+ // Credentials come from the house file, never a flag; PLATFORM_ROOT locates it.
19
+
20
+ import { readFile, readdir, stat } from 'node:fs/promises'
21
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
22
+ import { execFileSync } from 'node:child_process'
23
+ import { join, resolve } from 'node:path'
24
+ import { fileURLToPath } from 'node:url'
25
+ import { resolveExposedDirs } from './schema-exposed-dirs.mjs'
26
+ import { createD1Client } from './d1-http.mjs'
27
+
28
+ const TAG = '[portal-index]'
29
+
30
+ /** Read `KEY=value` pairs from a secrets file without polluting process.env.
31
+ * Values are trimmed, matching the readers in reconcile-bookings.ts and
32
+ * house-scoped-token.ts — a CRLF file would otherwise leave a stray "\r" that
33
+ * reaches Cloudflare as an opaque invalid-header error.
34
+ * @param {string} path @returns {Record<string, string>} */
35
+ function readEnvFile(path) {
36
+ /** @type {Record<string, string>} */ const out = {}
37
+ if (!existsSync(path)) return out
38
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
39
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/)
40
+ if (m) out[m[1]] = m[2].trim()
41
+ }
42
+ return out
43
+ }
44
+
45
+ /**
46
+ * Files under `exposed` dirs, at any depth. Dot-prefixed entries are skipped at
47
+ * every level: `.uploads-tmp/` holds in-flight upload partials (files.ts:25) and
48
+ * must never be published as a deliverable.
49
+ * @param {string} accountDir @param {string[]} exposed
50
+ * @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string}[]>}
51
+ */
52
+ export async function walkExposed(accountDir, exposed) {
53
+ /** @type {{relPath: string, sizeBytes: number, modifiedAt: string}[]} */
54
+ const out = []
55
+
56
+ /** @param {string} rel */
57
+ async function walk(rel) {
58
+ /** @type {import('node:fs').Dirent[]} */ let entries
59
+ try {
60
+ entries = await readdir(join(accountDir, rel), { withFileTypes: true })
61
+ } catch {
62
+ return // absent or unreadable dir contributes nothing
63
+ }
64
+ for (const e of entries) {
65
+ if (e.name.startsWith('.')) continue
66
+ const childRel = `${rel}/${e.name}`
67
+ if (e.isDirectory()) {
68
+ await walk(childRel)
69
+ } else if (e.isFile()) {
70
+ try {
71
+ const st = await stat(join(accountDir, childRel))
72
+ out.push({ relPath: childRel, sizeBytes: st.size, modifiedAt: st.mtime.toISOString() })
73
+ } catch {
74
+ // vanished mid-walk; the next cycle settles it
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ for (const dir of exposed) await walk(dir)
81
+ return out
82
+ }
83
+
84
+ /**
85
+ * @param {{ accountDir: string, accountId: string, dbName: string,
86
+ * client: { query: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]> },
87
+ * log: (line: string) => void, nowIso: string,
88
+ * previousExposed?: string[] }} opts
89
+ * @returns {Promise<{ exposed: string[], rows: number, schemaPresent: boolean }>}
90
+ */
91
+ export async function pushAccount(opts) {
92
+ const { accountDir, accountId, client, log, nowIso } = opts
93
+
94
+ /** @type {string | null} */ let schemaText = null
95
+ try {
96
+ schemaText = await readFile(join(accountDir, 'SCHEMA.md'), 'utf8')
97
+ } catch {
98
+ schemaText = null
99
+ }
100
+
101
+ const r = resolveExposedDirs(schemaText)
102
+ log(
103
+ `${TAG} op=resolve account=${accountId} schemaPresent=${r.schemaPresent} allowed=${r.allowed.length} ` +
104
+ `domainBuckets=${r.domainBuckets.join(',') || 'none'} exposed=${r.exposed.join(',') || 'none'}`,
105
+ )
106
+ // The generator gives a plugin-claimed dir to the plugin-owned region only
107
+ // (account-schema-owned-dirs.py:287), so this is normally silent. When it is
108
+ // not, a bucket has vanished from the client's view and nothing else says so.
109
+ for (const c of r.collisions) {
110
+ log(`${TAG} op=collision account=${accountId} dir=${c} claimedBy=plugin-owned`)
111
+ }
112
+
113
+ // Emitted only on a delta, so the line stays rare and is always worth
114
+ // reading. A schema regeneration — a vertical change, a plugin claiming a
115
+ // dir — silently changes what the client sees, with no deploy and nothing
116
+ // else to say so. `removed=` on a populated bucket is the client-visible
117
+ // regression. Reported from the push because the push is what applies the
118
+ // change; if it is not running, nothing changes and there is nothing to say.
119
+ if (opts.previousExposed) {
120
+ const added = r.exposed.filter((d) => !opts.previousExposed.includes(d))
121
+ const removed = opts.previousExposed.filter((d) => !r.exposed.includes(d))
122
+ if (added.length || removed.length) {
123
+ log(
124
+ `${TAG} op=exposed-changed account=${accountId} ` +
125
+ `added=${added.join(',') || 'none'} removed=${removed.join(',') || 'none'}`,
126
+ )
127
+ }
128
+ }
129
+
130
+ // Fail closed: no schema, or nothing exposed, means write NOTHING — not a
131
+ // delete. A delete would leave the table identical to a successful publish of
132
+ // an account that genuinely has no deliverables, which is the one state an
133
+ // operator must be able to tell apart.
134
+ if (!r.schemaPresent || r.exposed.length === 0) {
135
+ return { exposed: r.exposed, rows: 0, schemaPresent: r.schemaPresent }
136
+ }
137
+
138
+ const rows = await walkExposed(accountDir, r.exposed)
139
+
140
+ await client.query('DELETE FROM directory WHERE accountId = ?', [accountId])
141
+ for (const row of rows) {
142
+ await client.query(
143
+ 'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt) VALUES (?, ?, ?, ?, ?)',
144
+ [accountId, row.relPath, row.sizeBytes, row.modifiedAt, nowIso],
145
+ )
146
+ }
147
+ log(`${TAG} op=replace account=${accountId} deleted=all inserted=${rows.length}`)
148
+
149
+ // Read back rather than trusting the write: "issued the inserts" is not "the
150
+ // rows are there", and a half-applied batch is otherwise invisible.
151
+ const check = await client.query('SELECT COUNT(*) AS n FROM directory WHERE accountId = ?', [accountId])
152
+ const after = Number(check[0]?.n ?? -1)
153
+ log(`${TAG} op=verify account=${accountId} rowsAfter=${after} expected=${rows.length}`)
154
+
155
+ return { exposed: r.exposed, rows: rows.length, schemaPresent: true }
156
+ }
157
+
158
+ /**
159
+ * Where the push records what it last successfully wrote, per account.
160
+ *
161
+ * The heartbeat's audit reads this IN-PROCESS to answer "is this index still
162
+ * being refreshed?" — a question no event-driven line can answer, because the
163
+ * failure being detected is the absence of events. It lives under data/ rather
164
+ * than in an account dir so the audit needs nothing but PLATFORM_ROOT, and so
165
+ * it never trips the account write-path guard.
166
+ * @param {string} platformRoot
167
+ */
168
+ export function stateFilePath(platformRoot) {
169
+ return resolve(platformRoot, '..', 'data', 'portal-index-state.json')
170
+ }
171
+
172
+ /** @param {string} platformRoot @returns {Record<string, {lastPushMs: number, rows: number, exposed: string[]}>} */
173
+ export function readState(platformRoot) {
174
+ try {
175
+ return JSON.parse(readFileSync(stateFilePath(platformRoot), 'utf8'))
176
+ } catch {
177
+ return {}
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Written ONLY after op=verify, so a stale entry means the push stopped
183
+ * succeeding rather than merely stopped logging. Read-modify-write per account
184
+ * so one account's failure does not erase another's record.
185
+ * @param {string} platformRoot @param {string} accountId
186
+ * @param {{lastPushMs: number, rows: number, exposed: string[]}} entry
187
+ */
188
+ export function recordSuccess(platformRoot, accountId, entry) {
189
+ const path = stateFilePath(platformRoot)
190
+ const state = readState(platformRoot)
191
+ state[accountId] = entry
192
+ try {
193
+ writeFileSync(path, JSON.stringify(state, null, 1))
194
+ } catch (err) {
195
+ // Never fatal: the index IS published at this point. But never silent
196
+ // either — an unwritable state file makes every later audit report a
197
+ // healthy account as never-ran, which is a false alarm that would send an
198
+ // operator looking in the wrong place.
199
+ console.error(
200
+ `${TAG} op=state-write-failed account=${accountId} path="${path}" err="${err instanceof Error ? err.message : String(err)}"`,
201
+ )
202
+ }
203
+ }
204
+
205
+ /** @param {string} flag */
206
+ function arg(flag) {
207
+ const i = process.argv.indexOf(flag)
208
+ return i === -1 ? undefined : process.argv[i + 1]
209
+ }
210
+
211
+ /**
212
+ * Every account carrying a data-portal.json has a portal. The file's presence is
213
+ * the enrolment signal — there is no separate flag to keep in sync with it.
214
+ * @param {string} accountsRoot
215
+ * @returns {Promise<{accountDir: string, accountId: string, dbName: string}[]>}
216
+ */
217
+ async function portalAccounts(accountsRoot) {
218
+ /** @type {{accountDir: string, accountId: string, dbName: string}[]} */ const out = []
219
+ /** @type {string[]} */ let names
220
+ try {
221
+ names = await readdir(accountsRoot)
222
+ } catch {
223
+ return out
224
+ }
225
+ for (const accountId of names) {
226
+ const accountDir = join(accountsRoot, accountId)
227
+ try {
228
+ const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
229
+ if (typeof cfg?.portalDbName === 'string' && cfg.portalDbName) {
230
+ out.push({ accountDir, accountId, dbName: cfg.portalDbName })
231
+ } else {
232
+ console.error(`${TAG} op=skip account=${accountId} reason=no-portalDbName`)
233
+ }
234
+ } catch {
235
+ // No data-portal.json: this account has no portal. Not an error.
236
+ }
237
+ }
238
+ return out
239
+ }
240
+
241
+ /**
242
+ * Resolve the Cloudflare credentials the D1 API needs.
243
+ *
244
+ * Two things this must not conflate. `cfAccountId` is the CLOUDFLARE account
245
+ * (the `/accounts/<id>/d1/...` path segment) and comes from the house
246
+ * credential; the Maxy sub-account id is a different value entirely and is what
247
+ * scopes the `directory` rows. Passing one where the other belongs makes every
248
+ * database lookup fail.
249
+ *
250
+ * And the house token is a MINTER — using it directly against D1 returns 10000.
251
+ * `cf-token.sh` mints (or reuses) a D1-scoped token from it and prints the
252
+ * secrets KEY holding the value, never the value itself.
253
+ *
254
+ * @param {string} platformRoot
255
+ * @returns {{ cfAccountId: string, token: string } | { error: string }}
256
+ */
257
+ function resolveCloudflareCredentials(platformRoot) {
258
+ const houseEnvPath = join(platformRoot, 'config', 'cloudflare-house.env')
259
+ const house = readEnvFile(houseEnvPath)
260
+ if (!house.CLOUDFLARE_API_TOKEN) return { error: 'no-house-cloudflare-credential' }
261
+ // Required rather than inferred: the D1 endpoint is addressed per Cloudflare
262
+ // account, and guessing it would run these statements against the wrong one.
263
+ if (!house.CLOUDFLARE_ACCOUNT_ID) return { error: 'no-house-cloudflare-account-id' }
264
+
265
+ const cfTokenSh = join(platformRoot, 'plugins', 'cloudflare', 'bin', 'cf-token.sh')
266
+ if (!existsSync(cfTokenSh)) return { error: 'cf-token-sh-missing' }
267
+
268
+ /** @type {string} */ let keyName
269
+ try {
270
+ keyName = execFileSync(cfTokenSh, ['d1', houseEnvPath], { encoding: 'utf8' }).trim()
271
+ } catch (err) {
272
+ const detail = err instanceof Error ? err.message : String(err)
273
+ return { error: `scoped-token-mint-failed: ${detail}` }
274
+ }
275
+ // Re-read: cf-token.sh persists the minted token into the same file.
276
+ const token = readEnvFile(houseEnvPath)[keyName]
277
+ if (!token) return { error: `scoped-token-absent-after-mint key=${keyName}` }
278
+
279
+ return { cfAccountId: house.CLOUDFLARE_ACCOUNT_ID, token }
280
+ }
281
+
282
+ async function main() {
283
+ const platformRoot = process.env.PLATFORM_ROOT
284
+ if (!platformRoot) {
285
+ console.error(`${TAG} op=usage reason=no-platform-root PLATFORM_ROOT is required`)
286
+ process.exit(1)
287
+ }
288
+
289
+ const cred = resolveCloudflareCredentials(platformRoot)
290
+ if ('error' in cred) {
291
+ console.error(`${TAG} op=skip reason=${cred.error}`)
292
+ process.exit(1)
293
+ }
294
+
295
+ /** @type {{accountDir: string, accountId: string, dbName: string}[]} */ let targets
296
+ if (process.argv.includes('--all-accounts')) {
297
+ // Same derivation as the platform's own DATA_ROOT (data-path.ts:19), from
298
+ // the PLATFORM_ROOT the heartbeat already passes.
299
+ const accountsRoot = resolve(platformRoot, '..', 'data', 'accounts')
300
+ targets = await portalAccounts(accountsRoot)
301
+ console.error(`${TAG} op=enumerate accountsRoot="${accountsRoot}" accounts=${targets.length}`)
302
+ } else {
303
+ const accountDir = arg('--account-dir')
304
+ const accountId = arg('--account')
305
+ const dbName = arg('--db')
306
+ if (!accountDir || !accountId || !dbName) {
307
+ console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
308
+ process.exit(1)
309
+ }
310
+ targets = [{ accountDir, accountId, dbName }]
311
+ }
312
+
313
+ const state = readState(platformRoot)
314
+ let failed = 0
315
+ for (const t of targets) {
316
+ try {
317
+ // cfAccountId addresses the D1 endpoint; t.accountId scopes the rows.
318
+ // Two different identifiers — see resolveCloudflareCredentials.
319
+ const client = createD1Client({
320
+ accountId: cred.cfAccountId,
321
+ token: cred.token,
322
+ dbName: t.dbName,
323
+ })
324
+ const r = await pushAccount({
325
+ ...t,
326
+ client,
327
+ log: (l) => console.error(l),
328
+ nowIso: new Date().toISOString(),
329
+ previousExposed: state[t.accountId]?.exposed,
330
+ })
331
+ // Only a schema-present run records success. A fail-closed run published
332
+ // nothing, so recording it would let a broken schema masquerade as a
333
+ // healthy hourly cycle for as long as it stayed broken.
334
+ if (r.schemaPresent) {
335
+ recordSuccess(platformRoot, t.accountId, {
336
+ lastPushMs: Date.now(),
337
+ rows: r.rows,
338
+ exposed: r.exposed,
339
+ })
340
+ }
341
+ console.error(`${TAG} op=done account=${t.accountId} rows=${r.rows}`)
342
+ } catch (err) {
343
+ // One account's failure must not stop the rest, and it must never be
344
+ // swallowed — the message is the only thing that says which account.
345
+ failed++
346
+ console.error(
347
+ `${TAG} op=failed account=${t.accountId} err="${err instanceof Error ? err.message : String(err)}"`,
348
+ )
349
+ }
350
+ }
351
+ if (failed > 0) process.exit(1)
352
+ }
353
+
354
+ // Only when invoked directly, so the module stays importable by tests.
355
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
356
+ void main()
357
+ }
@@ -0,0 +1,103 @@
1
+ // Which folders of an account may a portal client see?
2
+ //
3
+ // Not a list in this file. The answer is derived per account from that
4
+ // account's own SCHEMA.md, which is generated by
5
+ // platform/scripts/lib/account-schema-owned-dirs.py: one bucket per top-level
6
+ // entity of the brand's vertical ontology, plus whatever plugins claim, inside
7
+ // a closed allowed-top-level set.
8
+ //
9
+ // Exposed = ontology buckets + `output`, intersected with the allowed set.
10
+ // Ontology buckets hold per-entity artifacts ("File a job's artifacts under its
11
+ // entity folder", the region's own prose); `output` is the delivery convention
12
+ // (doc-source-resolve.ts:14 calls it agent-authored). Operator-data buckets,
13
+ // uploads, plugin workspaces and platform-internal dirs are in neither source
14
+ // and so are never exposed.
15
+
16
+ const ONT_START = '<!-- ontology-buckets:start -->'
17
+ const ONT_END = '<!-- ontology-buckets:end -->'
18
+ const OWNED_START = '<!-- plugin-owned-dirs:start -->'
19
+ const OWNED_END = '<!-- plugin-owned-dirs:end -->'
20
+
21
+ /** The delivery convention, always exposed when the account allows it. */
22
+ export const ALWAYS_EXPOSED = 'output'
23
+
24
+ /**
25
+ * The generator writes the ontology region with an ASCII hyphen and the
26
+ * plugin-owned region with an em dash (account-schema-owned-dirs.py:294 vs
27
+ * :281). Matching the wrong one yields zero buckets and reads as "this account
28
+ * has no deliverables", which is why each region has its own pattern.
29
+ */
30
+ const DOMAIN_LINE = /^- `([^`/]+)\/` - one folder per /
31
+ const OWNED_LINE = /^- `([^`/]+)\/` — /
32
+
33
+ /** @param {string} text @param {string} start @param {string} end */
34
+ function region(text, start, end) {
35
+ const i = text.indexOf(start)
36
+ if (i === -1) return null
37
+ const j = text.indexOf(end, i)
38
+ return text.slice(i, j === -1 ? text.length : j)
39
+ }
40
+
41
+ /** @param {string} text @returns {string[] | null} */
42
+ function allowedBlockOrNull(text) {
43
+ const lines = text.split('\n')
44
+ const start = lines.findIndex((l) => l.trim() === '```allowed-top-level')
45
+ if (start === -1) return null
46
+ for (let j = start + 1; j < lines.length; j++) {
47
+ if (lines[j].trim() === '```') {
48
+ return lines.slice(start + 1, j).map((l) => l.trim()).filter(Boolean)
49
+ }
50
+ }
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * @param {string | null} schemaText Contents of accounts/<id>/SCHEMA.md, or null when absent.
56
+ * @returns {{ schemaPresent: boolean, allowed: string[], domainBuckets: string[],
57
+ * pluginOwned: string[], collisions: string[], exposed: string[] }}
58
+ */
59
+ export function resolveExposedDirs(schemaText) {
60
+ const empty = {
61
+ schemaPresent: false,
62
+ allowed: /** @type {string[]} */ ([]),
63
+ domainBuckets: /** @type {string[]} */ ([]),
64
+ pluginOwned: /** @type {string[]} */ ([]),
65
+ collisions: /** @type {string[]} */ ([]),
66
+ exposed: /** @type {string[]} */ ([]),
67
+ }
68
+ // Fail CLOSED. fs-schema-guard.sh:76-78 fails open on a missing schema so an
69
+ // unseeded legacy account is not write-blocked; a read surface facing a
70
+ // client must do the opposite. Do not "fix" this to match the guard.
71
+ if (typeof schemaText !== 'string' || schemaText.length === 0) return empty
72
+
73
+ const allowed = allowedBlockOrNull(schemaText)
74
+ if (allowed === null) return { ...empty, schemaPresent: true }
75
+
76
+ const ont = region(schemaText, ONT_START, ONT_END)
77
+ const owned = region(schemaText, OWNED_START, OWNED_END)
78
+
79
+ /** @param {string | null} text @param {RegExp} re */
80
+ const dirsIn = (text, re) => {
81
+ if (!text) return []
82
+ /** @type {string[]} */ const out = []
83
+ for (const line of text.split('\n')) {
84
+ const m = re.exec(line.trim())
85
+ if (m && !out.includes(m[1])) out.push(m[1])
86
+ }
87
+ return out
88
+ }
89
+
90
+ const domainBuckets = dirsIn(ont, DOMAIN_LINE)
91
+ const pluginOwned = dirsIn(owned, OWNED_LINE)
92
+
93
+ // The generator omits a plugin-claimed dir from the ontology region
94
+ // (account-schema-owned-dirs.py:287), so this should be empty in practice.
95
+ // When it is not, the bucket silently vanishes from the client's view — the
96
+ // caller logs it rather than guessing which side should win.
97
+ const collisions = domainBuckets.filter((d) => pluginOwned.includes(d))
98
+
99
+ const candidates = [ALWAYS_EXPOSED, ...domainBuckets.filter((d) => !collisions.includes(d))]
100
+ const exposed = [...new Set(candidates)].filter((d) => allowed.includes(d)).sort()
101
+
102
+ return { schemaPresent: true, allowed, domainBuckets, pluginOwned, collisions, exposed }
103
+ }
@@ -67,7 +67,7 @@ describe('sessions', () => {
67
67
  it('mints a session resolvable to its owner', async () => {
68
68
  const { db } = fakeDb()
69
69
  await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
70
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice' })
70
+ expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '' })
71
71
  })
72
72
 
73
73
  it('does not resolve an expired session', async () => {
@@ -97,7 +97,7 @@ describe('session revocation — the property the design claims', () => {
97
97
  // by construction, not by remembering to delete anything.
98
98
  const { db, peopleRows } = fakeDb()
99
99
  await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
100
- expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice' })
100
+ expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '' })
101
101
 
102
102
  peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
103
103
 
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { createD1Client } from '../../bin/d1-http.mjs'
3
+
4
+ type Init = { method?: string; body?: string; headers?: Record<string, string> }
5
+ type Call = { url: string; init: Init }
6
+
7
+ function fakeFetch(calls: Call[], rows: unknown[] = []) {
8
+ return async (url: string, init: Init) => {
9
+ calls.push({ url, init })
10
+ if (url.includes('?name=')) {
11
+ return { ok: true, status: 200, json: async () => ({ result: [{ name: 'portal-db', uuid: 'db-uuid' }] }) }
12
+ }
13
+ return { ok: true, status: 200, json: async () => ({ result: [{ results: rows }] }) }
14
+ }
15
+ }
16
+
17
+ const opts = (fetchFn: unknown) => ({
18
+ accountId: 'acc',
19
+ token: 't',
20
+ dbName: 'portal-db',
21
+ fetchFn: fetchFn as never,
22
+ })
23
+
24
+ describe('createD1Client', () => {
25
+ it('resolves the database by exact name then queries by uuid', async () => {
26
+ const calls: Call[] = []
27
+ const c = createD1Client(opts(fakeFetch(calls)))
28
+ await c.query('SELECT 1')
29
+ expect(calls[0].url).toContain('/accounts/acc/d1/database?name=portal-db')
30
+ expect(calls[1].url).toContain('/d1/database/db-uuid/query')
31
+ expect(calls[1].init.headers?.Authorization).toBe('Bearer t')
32
+ })
33
+
34
+ it('sends values as params, never interpolated into sql', async () => {
35
+ const calls: Call[] = []
36
+ const c = createD1Client(opts(fakeFetch(calls)))
37
+ await c.query('INSERT INTO directory (relPath) VALUES (?)', ["o'brien.pdf"])
38
+ const body = JSON.parse(calls[1].init.body ?? '{}')
39
+ expect(body.sql).toBe('INSERT INTO directory (relPath) VALUES (?)')
40
+ expect(body.params).toEqual(["o'brien.pdf"])
41
+ })
42
+
43
+ it('returns the rows of the first result', async () => {
44
+ const c = createD1Client(opts(fakeFetch([], [{ n: 3 }])))
45
+ expect(await c.query('SELECT COUNT(*) AS n FROM directory')).toEqual([{ n: 3 }])
46
+ })
47
+
48
+ it('caches the name lookup across statements', async () => {
49
+ const calls: Call[] = []
50
+ const c = createD1Client(opts(fakeFetch(calls)))
51
+ await c.query('SELECT 1')
52
+ await c.query('SELECT 2')
53
+ expect(calls.filter((x) => x.url.includes('?name=')).length).toBe(1)
54
+ expect(c.statements).toBe(2)
55
+ })
56
+
57
+ it('throws carrying the response body when the api fails', async () => {
58
+ // Never discard an error body: a bare status turns every failure into the
59
+ // same unactionable line.
60
+ const fetchFn = async () => ({
61
+ ok: false,
62
+ status: 403,
63
+ json: async () => ({ errors: [{ message: 'insufficient permissions' }] }),
64
+ })
65
+ const c = createD1Client(opts(fetchFn))
66
+ await expect(c.query('SELECT 1')).rejects.toThrow(/403.*insufficient permissions/)
67
+ })
68
+
69
+ it('refuses a database whose name only nearly matches', async () => {
70
+ // `?name=` is a filter whose matching rule is not documented, so taking the
71
+ // first result on trust could run this account's statements against a
72
+ // near-named database.
73
+ const fetchFn = async (url: string) => ({
74
+ ok: true,
75
+ status: 200,
76
+ json: async () =>
77
+ url.includes('?name=') ? { result: [{ name: 'portal-db-old', uuid: 'x' }] } : { result: [] },
78
+ })
79
+ const c = createD1Client(opts(fetchFn))
80
+ await expect(c.query('SELECT 1')).rejects.toThrow(/no D1 database named/)
81
+ })
82
+
83
+ it('counts a statement that was issued and then errored', async () => {
84
+ let n = 0
85
+ const fetchFn = async (url: string) => {
86
+ n++
87
+ if (url.includes('?name=')) {
88
+ return { ok: true, status: 200, json: async () => ({ result: [{ name: 'portal-db', uuid: 'u' }] }) }
89
+ }
90
+ return { ok: false, status: 500, json: async () => ({ errors: [{ message: 'boom' }] }) }
91
+ }
92
+ const c = createD1Client(opts(fetchFn))
93
+ await expect(c.query('SELECT 1')).rejects.toThrow()
94
+ expect(c.statements).toBe(1)
95
+ })
96
+ })