@rubytech/create-maxy-code 0.1.488 → 0.1.490

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 (282) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +748 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +1716 -0
  4. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +121 -0
  5. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +103 -0
  6. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +51 -0
  7. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -1
  8. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +2 -2
  9. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +20 -4
  11. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -1
  12. package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +55 -0
  13. package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +19 -4
  14. package/payload/platform/lib/graph-style/dist/index.d.ts +24 -0
  15. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  16. package/payload/platform/lib/graph-style/dist/index.js +25 -1
  17. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  18. package/payload/platform/lib/graph-style/src/__tests__/additional-base-labels.test.ts +22 -0
  19. package/payload/platform/lib/graph-style/src/index.ts +27 -0
  20. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +15 -0
  21. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/audit.d.ts +34 -1
  23. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  24. package/payload/platform/lib/storage-broker/dist/audit.js +28 -4
  25. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -0
  27. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/cf-exec.js +22 -0
  29. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  30. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +21 -0
  31. package/payload/platform/lib/storage-broker/src/audit.ts +67 -3
  32. package/payload/platform/lib/storage-broker/src/cf-exec.ts +48 -0
  33. package/payload/platform/neo4j/schema.cypher +105 -0
  34. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +34 -5
  35. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -2
  36. package/payload/platform/plugins/cloudflare/PLUGIN.md +22 -2
  37. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +161 -41
  38. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +95 -0
  39. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +75 -13
  40. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +216 -16
  41. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +38 -2
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +83 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +48 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +62 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +103 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +211 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +168 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +138 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +179 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +14 -13
  51. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +87 -13
  52. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +87 -9
  53. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +74 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +91 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +23 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +45 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +72 -5
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +80 -13
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +358 -16
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +12 -1
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +45 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +258 -21
  63. package/payload/platform/plugins/docs/references/calendar-booking.md +5 -1
  64. package/payload/platform/plugins/docs/references/internals.md +2 -0
  65. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +61 -31
  66. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
  67. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
  68. package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
  69. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  70. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +70 -5
  71. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -1
  72. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js +31 -6
  73. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js.map +1 -1
  74. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +31 -4
  75. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  76. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +18 -7
  77. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts +15 -6
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts.map +1 -1
  80. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +24 -11
  81. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
  82. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +5 -122
  83. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  84. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +84 -5
  85. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  86. package/payload/platform/scripts/__tests__/quarantine-restore.test.sh +143 -0
  87. package/payload/platform/scripts/quarantine-restore.mjs +175 -0
  88. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +24 -7
  89. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +52 -74
  91. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/index.js +11 -8
  93. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  94. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +32 -0
  95. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  96. package/payload/platform/services/whatsapp-channel/dist/notification.js +32 -2
  97. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  98. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +6 -1
  99. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  100. package/payload/platform/services/whatsapp-channel/dist/targets.js +6 -0
  101. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  102. package/payload/platform/templates/account-schema/SCHEMA.md +7 -4
  103. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  104. package/payload/platform/templates/agents/passive/IDENTITY.md +6 -0
  105. package/payload/platform/templates/agents/public/IDENTITY.md +6 -0
  106. package/payload/platform/templates/specialists/agents/citation-auditor.md +6 -0
  107. package/payload/platform/templates/specialists/agents/coding-assistant.md +6 -0
  108. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +6 -0
  109. package/payload/platform/templates/specialists/agents/content-producer.md +6 -0
  110. package/payload/platform/templates/specialists/agents/data-manager.md +6 -0
  111. package/payload/platform/templates/specialists/agents/database-operator.md +6 -0
  112. package/payload/platform/templates/specialists/agents/librarian.md +6 -0
  113. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -0
  114. package/payload/platform/templates/specialists/agents/project-manager.md +6 -0
  115. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +6 -0
  116. package/payload/platform/templates/specialists/agents/research-assistant.md +7 -1
  117. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +6 -0
  118. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +6 -0
  119. package/payload/server/{chunk-REWXLAPC.js → chunk-KZFE3MOY.js} +52 -4
  120. package/payload/server/public/activity.html +5 -6
  121. package/payload/server/public/assets/AdminLoginScreens-DgVFsHtd.js +1 -0
  122. package/payload/server/public/assets/AdminShell-BwNqwb4k.js +2 -0
  123. package/payload/server/public/assets/activity-2DL2DwLr.js +1 -0
  124. package/payload/server/public/assets/admin-COe0dT2U.js +1 -0
  125. package/payload/server/public/assets/{arc-D-QzyndF.js → arc-CFMJyMm-.js} +1 -1
  126. package/payload/server/public/assets/architecture-YZFGNWBL-Cypt5qHL.js +1 -0
  127. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-W7f0Us4S.js → architectureDiagram-Q4EWVU46-CiSSgdL8.js} +1 -1
  128. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CfPS4FXg.js → blockDiagram-DXYQGD6D-_sXtIW5c.js} +1 -1
  129. package/payload/server/public/assets/{bot-DnuyRsLS.js → bot-CpBuDQlF.js} +1 -1
  130. package/payload/server/public/assets/{browser-q4EFqKvR.js → browser-CSApuZrv.js} +1 -1
  131. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Dq60cLVI.js → c4Diagram-AHTNJAMY-Bh7OAMXs.js} +1 -1
  132. package/payload/server/public/assets/{calendar-C-q9kWmA.js → calendar-DlvwZnS7.js} +1 -1
  133. package/payload/server/public/assets/channel-BMyXE4zc.js +1 -0
  134. package/payload/server/public/assets/chat-DWvYv9jq.js +1 -0
  135. package/payload/server/public/assets/chevron-down-Dnuuw6Q2.js +1 -0
  136. package/payload/server/public/assets/chevron-left-CC4zmWOr.js +1 -0
  137. package/payload/server/public/assets/chevron-right-CG_lHTx_.js +1 -0
  138. package/payload/server/public/assets/{chunk-2KRD3SAO-CXDjuDN0.js → chunk-2KRD3SAO-D4fktAwW.js} +1 -1
  139. package/payload/server/public/assets/{chunk-336JU56O-926ox0tj.js → chunk-336JU56O-CcQoKH2G.js} +2 -2
  140. package/payload/server/public/assets/chunk-426QAEUC-Xu6JKP4d.js +1 -0
  141. package/payload/server/public/assets/{chunk-4BX2VUAB-C2nrPCfN.js → chunk-4BX2VUAB-C13Wz3zS.js} +1 -1
  142. package/payload/server/public/assets/{chunk-4TB4RGXK-CuW3FOR6.js → chunk-4TB4RGXK-BL48D7gN.js} +1 -1
  143. package/payload/server/public/assets/{chunk-55IACEB6-B4_-In90.js → chunk-55IACEB6-TmOKdy8a.js} +1 -1
  144. package/payload/server/public/assets/{chunk-5FUZZQ4R-DnYGr9CP.js → chunk-5FUZZQ4R-DZ_jSF6N.js} +1 -1
  145. package/payload/server/public/assets/{chunk-5PVQY5BW-BYCOi9bf.js → chunk-5PVQY5BW-De4G1vfV.js} +1 -1
  146. package/payload/server/public/assets/{chunk-67CJDMHE-CBxsC4JP.js → chunk-67CJDMHE-Cif9Uu5C.js} +1 -1
  147. package/payload/server/public/assets/{chunk-7N4EOEYR-CAACaqFr.js → chunk-7N4EOEYR-WjTHCXnd.js} +1 -1
  148. package/payload/server/public/assets/{chunk-AA7GKIK3-BoamZdS6.js → chunk-AA7GKIK3-CwCfdQmb.js} +1 -1
  149. package/payload/server/public/assets/{chunk-BSJP7CBP-CHk82eFy.js → chunk-BSJP7CBP-9A67yXwT.js} +1 -1
  150. package/payload/server/public/assets/{chunk-CIAEETIT-DdeW9zxl.js → chunk-CIAEETIT-DPMiWSBi.js} +1 -1
  151. package/payload/server/public/assets/{chunk-EDXVE4YY-DJqSRv2w.js → chunk-EDXVE4YY-C2f_Qvx-.js} +1 -1
  152. package/payload/server/public/assets/{chunk-ENJZ2VHE-BRi4BwM5.js → chunk-ENJZ2VHE-B-Q9EbSQ.js} +1 -1
  153. package/payload/server/public/assets/{chunk-FMBD7UC4-OJp2CtUE.js → chunk-FMBD7UC4-Bmt_ZRlV.js} +1 -1
  154. package/payload/server/public/assets/{chunk-FOC6F5B3-BUAQ6spG.js → chunk-FOC6F5B3-DfGJGxpX.js} +1 -1
  155. package/payload/server/public/assets/{chunk-ICPOFSXX-DeRggD-Q.js → chunk-ICPOFSXX-Wyym1Q8K.js} +2 -2
  156. package/payload/server/public/assets/{chunk-K5T4RW27-EHiXZ8Py.js → chunk-K5T4RW27-DMpoaqw2.js} +1 -1
  157. package/payload/server/public/assets/{chunk-KGLVRYIC-tOajPcpn.js → chunk-KGLVRYIC-DYUFgri2.js} +1 -1
  158. package/payload/server/public/assets/{chunk-LIHQZDEY-uwO6oHIU.js → chunk-LIHQZDEY-x3LmeKmx.js} +1 -1
  159. package/payload/server/public/assets/{chunk-ORNJ4GCN-Dpa1_BCA.js → chunk-ORNJ4GCN-BeuNAWkW.js} +1 -1
  160. package/payload/server/public/assets/{chunk-OYMX7WX6-ChdDqaQL.js → chunk-OYMX7WX6-BUWVAfQA.js} +1 -1
  161. package/payload/server/public/assets/chunk-QZHKN3VN-7_kS3RBy.js +1 -0
  162. package/payload/server/public/assets/{chunk-U2HBQHQK-5JgWosPv.js → chunk-U2HBQHQK-DKS5adQo.js} +1 -1
  163. package/payload/server/public/assets/{chunk-X2U36JSP-BGJqysB2.js → chunk-X2U36JSP-CT_UM4IN.js} +1 -1
  164. package/payload/server/public/assets/{chunk-XPW4576I-GERE2ekx.js → chunk-XPW4576I-ByFnN4JO.js} +1 -1
  165. package/payload/server/public/assets/{chunk-YZCP3GAM-BskqX1V5.js → chunk-YZCP3GAM-DCetC20J.js} +1 -1
  166. package/payload/server/public/assets/{chunk-ZZ45TVLE-DG1iY-L9.js → chunk-ZZ45TVLE-Bh1QIv8N.js} +1 -1
  167. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DJqbNcuC.js +1 -0
  168. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CGglqfQA.js +1 -0
  169. package/payload/server/public/assets/clone-BiTMTzp_.js +1 -0
  170. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C8Rl4ykh.js → cose-bilkent-S5V4N54A-DgLEaApT.js} +1 -1
  171. package/payload/server/public/assets/{dagre-KV5264BT-C0YrJPF3.js → dagre-KV5264BT-BfkUPrEV.js} +1 -1
  172. package/payload/server/public/assets/{dagre-DTlv-FFy.js → dagre-unhMIyVo.js} +1 -1
  173. package/payload/server/public/assets/data-CJbC_cVy.js +1 -0
  174. package/payload/server/public/assets/{diagram-5BDNPKRD-Dfk5oepI.js → diagram-5BDNPKRD-BjjwcTD9.js} +1 -1
  175. package/payload/server/public/assets/{diagram-G4DWMVQ6-QRj55DiB.js → diagram-G4DWMVQ6-BbgWtVSS.js} +1 -1
  176. package/payload/server/public/assets/{diagram-MMDJMWI5-BAPKsohU.js → diagram-MMDJMWI5-DyAnkWGy.js} +1 -1
  177. package/payload/server/public/assets/{diagram-TYMM5635-C3iZXPkC.js → diagram-TYMM5635-SAMMoLxq.js} +1 -1
  178. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DIbI7g_5.js → erDiagram-SMLLAGMA-Cvo-5-fD.js} +1 -1
  179. package/payload/server/public/assets/{flatten-DlAJeN0L.js → flatten-BXzaHgtL.js} +1 -1
  180. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-l3eJJr8R.js → flowDiagram-DWJPFMVM-y3o2Ycld.js} +1 -1
  181. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-aaEzHxIq.js → ganttDiagram-T4ZO3ILL-BEm_CQaK.js} +1 -1
  182. package/payload/server/public/assets/gitGraph-7Q5UKJZL-C_-XjxLd.js +1 -0
  183. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di-1eg_m.js → gitGraphDiagram-UUTBAWPF-CqmOxS7F.js} +1 -1
  184. package/payload/server/public/assets/{graph-CNgt7ygi.js → graph-kaxS9a_7.js} +2 -2
  185. package/payload/server/public/assets/graph-labels-GgejRtgV.js +1 -0
  186. package/payload/server/public/assets/{graphlib-D5awC2yp.js → graphlib-_HfbQcBf.js} +1 -1
  187. package/payload/server/public/assets/info-OMHHGYJF-BH8yDCfL.js +1 -0
  188. package/payload/server/public/assets/infoDiagram-42DDH7IO-DyDXyJ6U.js +2 -0
  189. package/payload/server/public/assets/{isEmpty-Dh1dF-tM.js → isEmpty-uB1dEu_Z.js} +1 -1
  190. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CLOd_cNf.js → ishikawaDiagram-UXIWVN3A-C_d0vcZC.js} +1 -1
  191. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-cGyfW6Tg.js → journeyDiagram-VCZTEJTY-DsjRBqlW.js} +1 -1
  192. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CRTKWPlh.js → kanban-definition-6JOO6SKY-BwtK6LS-.js} +1 -1
  193. package/payload/server/public/assets/{line-B8Brm0TD.js → line-szBCFOLJ.js} +1 -1
  194. package/payload/server/public/assets/{linear-BHnSKXBe.js → linear-X3O7lwK3.js} +1 -1
  195. package/payload/server/public/assets/{maximize-2-k3Fu5hdP.js → maximize-2-CPq3nwZA.js} +1 -1
  196. package/payload/server/public/assets/{mermaid-parser.core-BtNpdQct.js → mermaid-parser.core-BavvMAhU.js} +2 -2
  197. package/payload/server/public/assets/{mermaid.core-BY87anQZ.js → mermaid.core-Br2F9Hdm.js} +3 -3
  198. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-rUSv9qN1.js → mindmap-definition-QFDTVHPH-4qlwXoJ-.js} +1 -1
  199. package/payload/server/public/assets/operator-CldXXilj.js +1 -0
  200. package/payload/server/public/assets/{ordinal-Bnl5GL-x.js → ordinal-CZY9laxg.js} +1 -1
  201. package/payload/server/public/assets/packet-4T2RLAQJ-Bajcns4r.js +1 -0
  202. package/payload/server/public/assets/{page-Bvx0A2jB.js → page-BE6DkJP9.js} +1 -1
  203. package/payload/server/public/assets/{page-DhwPG5Zf.js → page-BIwdWXnv.js} +3 -3
  204. package/payload/server/public/assets/{pdf-render-CrpAi4Ba.js → pdf-render-H884L3EI.js} +1 -1
  205. package/payload/server/public/assets/pie-ZZUOXDRM-D7hjM5w1.js +1 -0
  206. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Cjn9IP-C.js → pieDiagram-DEJITSTG-BbXryIg0.js} +1 -1
  207. package/payload/server/public/assets/{public-BJ5Oo2Fw.js → public-BKQEQGPE.js} +1 -1
  208. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BkSHkSsn.js → quadrantDiagram-34T5L4WZ-BHLjcvh3.js} +1 -1
  209. package/payload/server/public/assets/radar-PYXPWWZC-B46d5x7a.js +1 -0
  210. package/payload/server/public/assets/{reduce-Bp5oNw1n.js → reduce-BGQzAenn.js} +1 -1
  211. package/payload/server/public/assets/{requirementDiagram-MS252O5E-hLwgyB-F.js → requirementDiagram-MS252O5E-BXaVO0lk.js} +1 -1
  212. package/payload/server/public/assets/{rotate-ccw-D-ov5jM4.js → rotate-ccw-DieZzl10.js} +1 -1
  213. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D6Dl1RIa.js → sankeyDiagram-XADWPNL6-CmN1s17Q.js} +1 -1
  214. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-j0QBqkbL.js → sequenceDiagram-FGHM5R23-DC08e5NY.js} +1 -1
  215. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-TvEBNsuV.js → stateDiagram-FHFEXIEX-DnoQMuOW.js} +1 -1
  216. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CrstH34p.js +1 -0
  217. package/payload/server/public/assets/{tasks-DHMxTZVD.js → tasks-DYyCGz8W.js} +1 -1
  218. package/payload/server/public/assets/{time-entry-format-EmV5lz6o.js → time-entry-format-CC_9jFuR.js} +1 -1
  219. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BfivOP-j.js → timeline-definition-GMOUNBTQ-eTn3uRaH.js} +1 -1
  220. package/payload/server/public/assets/treeView-SZITEDCU-3XQh5xQ1.js +1 -0
  221. package/payload/server/public/assets/treemap-W4RFUUIX-DwR-_l5z.js +1 -0
  222. package/payload/server/public/assets/{triangle-alert-CivyPfAL.js → triangle-alert-7mQfCQWP.js} +1 -1
  223. package/payload/server/public/assets/{useCopyFeedback-hbnPS-r9.js → useCopyFeedback-os7N0yCx.js} +1 -1
  224. package/payload/server/public/assets/useSubAccountSwitcher-Bgfqig1V.css +1 -0
  225. package/payload/server/public/assets/{useVoiceRecorder-QN9_GqB_.js → useVoiceRecorder-ZFwt5i6d.js} +2 -2
  226. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Bvfw9aFJ.js → vennDiagram-DHZGUBPP-DNx6o_ZQ.js} +1 -1
  227. package/payload/server/public/assets/wardley-RL74JXVD-DZp9oCGv.js +1 -0
  228. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C9h04G6b.js → wardleyDiagram-NUSXRM2D-BZOchqF3.js} +1 -1
  229. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BMvi2m2q.js → xychartDiagram-5P7HB3ND-ZBEGH6ZI.js} +1 -1
  230. package/payload/server/public/browser.html +4 -4
  231. package/payload/server/public/calendar.html +6 -6
  232. package/payload/server/public/chat.html +14 -14
  233. package/payload/server/public/data.html +11 -11
  234. package/payload/server/public/graph.html +10 -11
  235. package/payload/server/public/index.html +16 -17
  236. package/payload/server/public/operator.html +15 -16
  237. package/payload/server/public/public.html +14 -14
  238. package/payload/server/public/tasks.html +6 -6
  239. package/payload/server/server.js +1836 -998
  240. package/payload/server/{src-JJ6PHO5I.js → src-UYDFMWF4.js} +1 -1
  241. package/payload/server/public/assets/AdminLoginScreens-Dic_fPt6.js +0 -1
  242. package/payload/server/public/assets/AdminShell-JN7rZ8aG.js +0 -2
  243. package/payload/server/public/assets/Checkbox-CB74kINA.js +0 -1
  244. package/payload/server/public/assets/activity-DbfpHg3i.js +0 -1
  245. package/payload/server/public/assets/admin-CThbXaYH.js +0 -1
  246. package/payload/server/public/assets/architecture-YZFGNWBL-CGvD4r9F.js +0 -1
  247. package/payload/server/public/assets/channel-Bpg-ADC0.js +0 -1
  248. package/payload/server/public/assets/chat-DcefIV9q.js +0 -1
  249. package/payload/server/public/assets/chevron-down-Blma_CKl.js +0 -1
  250. package/payload/server/public/assets/chevron-left-xeYDxFiC.js +0 -1
  251. package/payload/server/public/assets/chevron-right-BonbtjqJ.js +0 -1
  252. package/payload/server/public/assets/chunk-426QAEUC-1poG7Nd3.js +0 -1
  253. package/payload/server/public/assets/chunk-QZHKN3VN-GN1tIO8J.js +0 -1
  254. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DMGVq8Jv.js +0 -1
  255. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfddT8UV.js +0 -1
  256. package/payload/server/public/assets/clone-Cc9vFIEk.js +0 -1
  257. package/payload/server/public/assets/data-D5dp6nj6.js +0 -1
  258. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DtjXE0UI.js +0 -1
  259. package/payload/server/public/assets/graph-labels-DA48HrRR.js +0 -1
  260. package/payload/server/public/assets/info-OMHHGYJF-CmArhGYf.js +0 -1
  261. package/payload/server/public/assets/infoDiagram-42DDH7IO-utWz7oZy.js +0 -2
  262. package/payload/server/public/assets/operator-8NVpNLeD.js +0 -1
  263. package/payload/server/public/assets/packet-4T2RLAQJ-BTfK0Zw4.js +0 -1
  264. package/payload/server/public/assets/pie-ZZUOXDRM-BvtPeMQY.js +0 -1
  265. package/payload/server/public/assets/radar-PYXPWWZC-D2UXvQgn.js +0 -1
  266. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ur4eM6rb.js +0 -1
  267. package/payload/server/public/assets/treeView-SZITEDCU-0dE7FvM2.js +0 -1
  268. package/payload/server/public/assets/treemap-W4RFUUIX-BrwlXpr5.js +0 -1
  269. package/payload/server/public/assets/useSubAccountSwitcher-kwhG-aJj.css +0 -1
  270. package/payload/server/public/assets/wardley-RL74JXVD-CO9HdfJ-.js +0 -1
  271. /package/payload/server/public/assets/{_baseFor-BBkmqPEk.js → _baseFor-CMWvz0uA.js} +0 -0
  272. /package/payload/server/public/assets/{array-Bjtb2C_5.js → array-Cny_NT6T.js} +0 -0
  273. /package/payload/server/public/assets/{cytoscape.esm-B1dQ664k.js → cytoscape.esm-De0CCmB2.js} +0 -0
  274. /package/payload/server/public/assets/{defaultLocale-iwYA7ne8.js → defaultLocale-BahRht8U.js} +0 -0
  275. /package/payload/server/public/assets/{dist-CLmrZbY7.js → dist-C9D_xz81.js} +0 -0
  276. /package/payload/server/public/assets/{init-CxnBAgcf.js → init-DoIvjoJ_.js} +0 -0
  277. /package/payload/server/public/assets/{katex-DojHoLIr.js → katex-Bfn1OZEl.js} +0 -0
  278. /package/payload/server/public/assets/{path-JZiojYzQ.js → path-CTezU8L5.js} +0 -0
  279. /package/payload/server/public/assets/{preload-helper-CT1Z6Pdu.js → preload-helper-D_sPnjeL.js} +0 -0
  280. /package/payload/server/public/assets/{rough.esm-D5NinLFK.js → rough.esm-BJo4pvXu.js} +0 -0
  281. /package/payload/server/public/assets/{src-DQ0MSD6q.js → src-BTKk8KAG.js} +0 -0
  282. /package/payload/server/public/assets/{useSubAccountSwitcher-DotTrvX3.js → useSubAccountSwitcher-Q941cZYV.js} +0 -0
@@ -0,0 +1,211 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ MAX_PART_BYTES,
4
+ MAX_TOTAL_BYTES,
5
+ processMultipartComplete,
6
+ processMultipartCreate,
7
+ processMultipartPart,
8
+ } from '../../skills/data-portal/template/functions/api/upload'
9
+ import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
10
+
11
+ function harness(opts: { folders?: string[] } = {}) {
12
+ const folders = opts.folders ?? ['jobs']
13
+ const uploads = new Map<string, { key: string; parts: Map<number, number>; aborted: boolean }>()
14
+ const objects = new Map<string, number>()
15
+ const rows = new Map<string, Record<string, unknown>>()
16
+ const lines: string[] = []
17
+ let seq = 0
18
+
19
+ function session(id: string) {
20
+ const rec = uploads.get(id)!
21
+ return {
22
+ uploadId: id,
23
+ async uploadPart(n: number, body: Uint8Array) {
24
+ rec.parts.set(n, body.length)
25
+ return { partNumber: n, etag: `etag-${n}` }
26
+ },
27
+ async complete(parts: { partNumber: number; etag: string }[]) {
28
+ let total = 0
29
+ for (const p of parts) total += rec.parts.get(p.partNumber) ?? 0
30
+ objects.set(rec.key, total)
31
+ return { size: total }
32
+ },
33
+ async abort() {
34
+ rec.aborted = true
35
+ },
36
+ }
37
+ }
38
+
39
+ const env = {
40
+ BUCKET: {
41
+ async createMultipartUpload(key: string) {
42
+ const id = `r2-${++seq}`
43
+ uploads.set(id, { key, parts: new Map(), aborted: false })
44
+ return session(id)
45
+ },
46
+ resumeMultipartUpload(_key: string, id: string) {
47
+ return session(id)
48
+ },
49
+ async head(key: string) {
50
+ const s = objects.get(key)
51
+ return s === undefined ? null : { size: s }
52
+ },
53
+ },
54
+ DB: {
55
+ prepare(sql: string) {
56
+ return {
57
+ bind(...args: unknown[]) {
58
+ return {
59
+ async first() {
60
+ if (sql.includes('FROM sessions')) {
61
+ return { ownerId: 'alice', accountId: 'acct', folders: '' }
62
+ }
63
+ if (sql.includes('isDir = 1')) {
64
+ return folders.includes(String(args[1])) ? { relPath: args[1] } : null
65
+ }
66
+ if (sql.includes('SELECT fileId FROM manifest')) {
67
+ return rows.get(String(args[0])) ?? null
68
+ }
69
+ return null
70
+ },
71
+ async run() {
72
+ rows.set(String(args[3]), { fileId: args[0], size: args[4] })
73
+ },
74
+ }
75
+ },
76
+ }
77
+ },
78
+ },
79
+ } as unknown as PortalEnv
80
+
81
+ let n = 0
82
+ return {
83
+ env, uploads, objects, rows, lines,
84
+ newId: () => `uid-${++n}`,
85
+ log: (l: string) => lines.push(l),
86
+ }
87
+ }
88
+
89
+ describe('multipart create', () => {
90
+ it('refuses a declared size over the total cap without opening a session', async () => {
91
+ // Checked before createMultipartUpload: an over-cap declaration must not
92
+ // leave an orphan multipart session accruing storage in R2 forever.
93
+ const h = harness()
94
+ const res = await processMultipartCreate(
95
+ 's', 'big.zip', '', MAX_TOTAL_BYTES + 1, h.env, h.log, h.newId, () => 1,
96
+ )
97
+ expect(res.status).toBe(413)
98
+ expect(h.uploads.size).toBe(0)
99
+ })
100
+
101
+ it('opens a session under the cap and returns its ids', async () => {
102
+ const h = harness()
103
+ const res = await processMultipartCreate(
104
+ 's', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1,
105
+ )
106
+ expect(res.status).toBe(200)
107
+ expect(res.payload.r2UploadId).toBe('r2-1')
108
+ expect(res.payload.key).toBe('alice/big.zip')
109
+ })
110
+
111
+ it('applies the same target rule as a single-shot upload', async () => {
112
+ const h = harness({ folders: [] })
113
+ const res = await processMultipartCreate(
114
+ 's', 'big.zip', 'jobs', 1024, h.env, h.log, h.newId, () => 1,
115
+ )
116
+ expect(res.status).toBe(403)
117
+ expect(h.uploads.size).toBe(0)
118
+ })
119
+
120
+ it('keys a targeted session under its folder', async () => {
121
+ const h = harness()
122
+ const res = await processMultipartCreate(
123
+ 's', 'big.zip', 'jobs', 1024, h.env, h.log, h.newId, () => 1,
124
+ )
125
+ expect(res.payload.key).toBe('alice/jobs/big.zip')
126
+ })
127
+ })
128
+
129
+ describe('multipart part', () => {
130
+ it('refuses a part over the per-part gate', async () => {
131
+ const h = harness()
132
+ await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
133
+ const res = await processMultipartPart(
134
+ 's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(MAX_PART_BYTES + 1), h.env, h.log,
135
+ )
136
+ expect(res.status).toBe(413)
137
+ })
138
+
139
+ it('refuses a part whose key is not the caller own', async () => {
140
+ // The key arrives on EVERY part request, so it is re-authorized every time.
141
+ // A session that may write alice/ must not be able to append to bob/.
142
+ const h = harness()
143
+ const res = await processMultipartPart(
144
+ 's', 'bob/big.zip', 'r2-1', 1, new Uint8Array(4), h.env, h.log,
145
+ )
146
+ expect(res.status).toBe(403)
147
+ })
148
+
149
+ it('accepts a part within the gate and returns its etag', async () => {
150
+ const h = harness()
151
+ await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
152
+ const res = await processMultipartPart(
153
+ 's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(8), h.env, h.log,
154
+ )
155
+ expect(res.status).toBe(200)
156
+ expect(res.payload.etag).toBe('etag-1')
157
+ })
158
+ })
159
+
160
+ describe('multipart complete', () => {
161
+ it('assembles the object and writes one manifest row', async () => {
162
+ const h = harness()
163
+ await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
164
+ await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(60), h.env, h.log)
165
+ await processMultipartPart('s', 'alice/big.zip', 'r2-1', 2, new Uint8Array(40), h.env, h.log)
166
+ const res = await processMultipartComplete(
167
+ 's', 'alice/big.zip', 'r2-1', '',
168
+ [{ partNumber: 1, etag: 'etag-1' }, { partNumber: 2, etag: 'etag-2' }],
169
+ h.env, h.log, h.newId, () => 1,
170
+ )
171
+ expect(res.status).toBe(200)
172
+ expect(h.objects.get('alice/big.zip')).toBe(100)
173
+ expect(h.rows.size).toBe(1)
174
+ })
175
+
176
+ it('aborts rather than assembling when the summed parts exceed the cap', async () => {
177
+ // The declared size is a claim; the summed parts are the measurement. A
178
+ // client that under-declares must not get a 1 GiB+ object stored anyway.
179
+ const h = harness()
180
+ await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
181
+ const res = await processMultipartComplete(
182
+ 's', 'alice/big.zip', 'r2-1', '',
183
+ [{ partNumber: 1, etag: 'e', size: MAX_TOTAL_BYTES }, { partNumber: 2, etag: 'e', size: 1 }],
184
+ h.env, h.log, h.newId, () => 1,
185
+ )
186
+ expect(res.status).toBe(413)
187
+ expect(h.uploads.get('r2-1')!.aborted).toBe(true)
188
+ expect(h.rows.size).toBe(0)
189
+ })
190
+
191
+ it('refuses a complete for a key that is not the caller own', async () => {
192
+ const h = harness()
193
+ const res = await processMultipartComplete(
194
+ 's', 'bob/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'e' }], h.env, h.log, h.newId, () => 1,
195
+ )
196
+ expect(res.status).toBe(403)
197
+ })
198
+
199
+ it('logs op=complete with a re-read post-condition', async () => {
200
+ const h = harness()
201
+ await processMultipartCreate('s', 'big.zip', '', 10, h.env, h.log, h.newId, () => 1)
202
+ await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(10), h.env, h.log)
203
+ await processMultipartComplete(
204
+ 's', 'alice/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'etag-1' }],
205
+ h.env, h.log, h.newId, () => 1,
206
+ )
207
+ expect(
208
+ h.lines.some((l) => l.includes('op=complete') && l.includes('objectPresent=true')),
209
+ ).toBe(true)
210
+ })
211
+ })
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { processUpload } from '../../skills/data-portal/template/functions/api/upload'
3
+ import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
4
+
5
+ /**
6
+ * The manifest is a Map keyed on objectKey, so the schema's `objectKey UNIQUE`
7
+ * is real rather than assumed. Task 1704's lesson: a fake that accepts what the
8
+ * store rejects passes a test the production path fails.
9
+ */
10
+ function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
11
+ const folders = opts.folders ?? ['jobs']
12
+ const objects = new Map<string, Uint8Array>()
13
+ const rows = new Map<string, Record<string, unknown>>()
14
+ const lines: string[] = []
15
+
16
+ const env = {
17
+ BUCKET: {
18
+ async put(key: string, body: Uint8Array) {
19
+ objects.set(key, body)
20
+ },
21
+ async head(key: string) {
22
+ const b = objects.get(key)
23
+ return b ? { size: b.length } : null
24
+ },
25
+ },
26
+ DB: {
27
+ prepare(sql: string) {
28
+ return {
29
+ bind(...args: unknown[]) {
30
+ return {
31
+ async first() {
32
+ if (sql.includes('FROM sessions')) {
33
+ return {
34
+ ownerId: 'alice',
35
+ accountId: 'acct',
36
+ folders: (opts.grant ?? []).join(','),
37
+ }
38
+ }
39
+ if (sql.includes('isDir = 1')) {
40
+ return folders.includes(String(args[1])) ? { relPath: args[1] } : null
41
+ }
42
+ if (sql.includes('SELECT fileId FROM manifest')) {
43
+ return rows.get(String(args[0])) ?? null
44
+ }
45
+ return null
46
+ },
47
+ async run() {
48
+ // The upsert: conflict on objectKey replaces in place, which is
49
+ // what the real ON CONFLICT (objectKey) does.
50
+ const key = String(args[3])
51
+ rows.set(key, {
52
+ fileId: args[0],
53
+ ownerId: args[1],
54
+ filename: args[2],
55
+ objectKey: key,
56
+ size: args[4],
57
+ ingested: 0,
58
+ targetPath: args[7],
59
+ })
60
+ },
61
+ }
62
+ },
63
+ }
64
+ },
65
+ },
66
+ } as unknown as PortalEnv
67
+
68
+ let n = 0
69
+ return {
70
+ env,
71
+ objects,
72
+ rows,
73
+ lines,
74
+ newId: () => `id-${++n}`,
75
+ log: (l: string) => lines.push(l),
76
+ }
77
+ }
78
+
79
+ describe('targeted upload', () => {
80
+ it('stores under the target and records it on the row', async () => {
81
+ const h = harness()
82
+ const res = await processUpload(
83
+ 's', 'a.pdf', 'jobs', new Uint8Array([1, 2]), h.env, h.log, h.newId, () => 1000,
84
+ )
85
+ expect(res.status).toBe(200)
86
+ expect([...h.objects.keys()]).toEqual(['alice/jobs/a.pdf'])
87
+ expect(h.rows.get('alice/jobs/a.pdf')!.targetPath).toBe('jobs')
88
+ })
89
+
90
+ it('stores at the owner root when untargeted', async () => {
91
+ const h = harness()
92
+ const res = await processUpload(
93
+ 's', 'a.pdf', '', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
94
+ )
95
+ expect(res.status).toBe(200)
96
+ expect([...h.objects.keys()]).toEqual(['alice/a.pdf'])
97
+ })
98
+
99
+ it('keeps the same filename in two folders as two distinct objects', async () => {
100
+ // The reason the target is in the key. A flat key would collide these and
101
+ // the upsert would destroy the first upload.
102
+ const h = harness({ folders: ['jobs', 'output'] })
103
+ await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
104
+ await processUpload('s', 'a.pdf', 'output', new Uint8Array([2]), h.env, h.log, h.newId, () => 1000)
105
+ expect([...h.objects.keys()].sort()).toEqual(['alice/jobs/a.pdf', 'alice/output/a.pdf'])
106
+ expect(h.rows.size).toBe(2)
107
+ })
108
+
109
+ it('replaces on the same owner, folder and filename', async () => {
110
+ const h = harness()
111
+ await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
112
+ await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([2, 2]), h.env, h.log, h.newId, () => 2000)
113
+ expect(h.rows.size).toBe(1)
114
+ expect(h.rows.get('alice/jobs/a.pdf')!.size).toBe(2)
115
+ })
116
+
117
+ it('refuses an out-of-grant target and stores nothing', async () => {
118
+ const h = harness({ grant: ['output'] })
119
+ const res = await processUpload(
120
+ 's', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
121
+ )
122
+ expect(res.status).toBe(403)
123
+ expect(h.objects.size).toBe(0)
124
+ expect(h.rows.size).toBe(0)
125
+ expect(
126
+ h.lines.some((l) => l.includes('op=target-check') && l.includes('result=denied-grant')),
127
+ ).toBe(true)
128
+ })
129
+
130
+ it('refuses a target the index does not publish', async () => {
131
+ const h = harness({ folders: [] })
132
+ const res = await processUpload(
133
+ 's', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
134
+ )
135
+ expect(res.status).toBe(403)
136
+ expect(h.objects.size).toBe(0)
137
+ expect(h.lines.some((l) => l.includes('result=denied-index'))).toBe(true)
138
+ })
139
+
140
+ it('refuses a traversal target and stores nothing', async () => {
141
+ const h = harness()
142
+ const res = await processUpload(
143
+ 's', 'a.pdf', 'jobs/../..', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
144
+ )
145
+ expect(res.status).toBe(403)
146
+ expect(h.objects.size).toBe(0)
147
+ expect(h.lines.some((l) => l.includes('result=denied-segment'))).toBe(true)
148
+ })
149
+
150
+ it('logs op=target-check result=allowed on the happy path', async () => {
151
+ const h = harness()
152
+ await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
153
+ expect(
154
+ h.lines.some((l) => l.includes('op=target-check') && l.includes('result=allowed')),
155
+ ).toBe(true)
156
+ })
157
+
158
+ it('checks the target before any store write', async () => {
159
+ // Ordering is the point: a denied caller must not be able to cause an R2
160
+ // put, the same rule the owner gate already follows.
161
+ const h = harness({ folders: [] })
162
+ await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
163
+ const targetAt = h.lines.findIndex((l) => l.includes('op=target-check'))
164
+ const putAt = h.lines.findIndex((l) => l.includes('op=r2-put'))
165
+ expect(targetAt).toBeGreaterThanOrEqual(0)
166
+ expect(putAt).toBe(-1)
167
+ })
168
+ })
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { processFiles, shouldListUpload } from '../../skills/data-portal/template/functions/api/files'
3
+ import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
4
+
5
+ const visible = new Set(['jobs', 'output'])
6
+
7
+ /**
8
+ * A directory fake that HONOURS the generation pointer, unlike the older
9
+ * listing fake which matches `FROM directory` and returns every row whatever
10
+ * the query says.
11
+ *
12
+ * The distinction is the whole atomicity guarantee: rows staged at a generation
13
+ * the pointer does not name must be invisible. A fake that ignores the join
14
+ * would pass whether or not files.ts actually reads through it — Task 1704's
15
+ * lesson, that a fake modelling only the happy path proves nothing.
16
+ */
17
+ function envWithGenerations(
18
+ rows: { relPath: string; isDir: number; generation: number }[],
19
+ currentGeneration: number | null,
20
+ manifest: Record<string, unknown>[] = [],
21
+ ) {
22
+ return {
23
+ DB: {
24
+ prepare(sql: string) {
25
+ return {
26
+ bind(...bound: unknown[]) {
27
+ return {
28
+ async first() {
29
+ if (sql.includes('FROM sessions')) {
30
+ return { ownerId: 'alice', accountId: 'acct', folders: '' }
31
+ }
32
+ return null
33
+ },
34
+ async all() {
35
+ if (/FROM manifest/i.test(sql)) return { results: manifest }
36
+ if (/FROM directory/i.test(sql)) {
37
+ // The JOIN: no pointer row means nothing is live at all.
38
+ if (currentGeneration === null) return { results: [] }
39
+ if (!/JOIN\s+directory_state/i.test(sql)) {
40
+ throw new Error('directory read must join the generation pointer')
41
+ }
42
+ return {
43
+ results: rows.filter(
44
+ (r) => bound[0] === 'acct' && r.generation === currentGeneration,
45
+ ),
46
+ }
47
+ }
48
+ return { results: [] }
49
+ },
50
+ }
51
+ },
52
+ }
53
+ },
54
+ },
55
+ } as unknown as PortalEnv
56
+ }
57
+
58
+ describe('the listing reads through the generation pointer', () => {
59
+ it('shows only the generation the pointer names', async () => {
60
+ const env = envWithGenerations(
61
+ [
62
+ { relPath: 'jobs', isDir: 1, generation: 1 },
63
+ { relPath: 'output', isDir: 1, generation: 2 },
64
+ ],
65
+ 1,
66
+ )
67
+ const res = await processFiles('s', env, () => {}, 1000)
68
+ const entries = (res.payload as { entries: { name: string }[] }).entries
69
+ expect(entries.map((e) => e.name)).toEqual(['jobs'])
70
+ })
71
+
72
+ it('shows the whole new tree the instant the pointer flips, never a mixture', async () => {
73
+ const rows = [
74
+ { relPath: 'jobs', isDir: 1, generation: 1 },
75
+ { relPath: 'output', isDir: 1, generation: 2 },
76
+ { relPath: 'quotes', isDir: 1, generation: 2 },
77
+ ]
78
+ const before = await processFiles('s', envWithGenerations(rows, 1), () => {}, 1000)
79
+ const after = await processFiles('s', envWithGenerations(rows, 2), () => {}, 1000)
80
+ expect((before.payload as { entries: unknown[] }).entries.length).toBe(1)
81
+ expect((after.payload as { entries: unknown[] }).entries.length).toBe(2)
82
+ })
83
+
84
+ it('shows nothing for an account that has never completed a publish', async () => {
85
+ const env = envWithGenerations([{ relPath: 'jobs', isDir: 1, generation: 1 }], null)
86
+ const res = await processFiles('s', env, () => {}, 1000)
87
+ expect((res.payload as { entries: unknown[] }).entries).toEqual([])
88
+ })
89
+ })
90
+
91
+ describe('the uploads panel and the tree agree on what is reachable', () => {
92
+ it('hides a moved row whose folder is live and lists one whose folder is not', async () => {
93
+ const env = envWithGenerations(
94
+ [{ relPath: 'jobs', isDir: 1, generation: 1 }],
95
+ 1,
96
+ [
97
+ { fileId: 'f1', filename: 'a.pdf', devicePath: 'jobs/a.pdf', deleted: 0 },
98
+ { fileId: 'f2', filename: 'b.pdf', devicePath: 'uploads/alice/b.pdf', deleted: 0 },
99
+ { fileId: 'f3', filename: 'c.pdf', devicePath: '', deleted: 0 },
100
+ ],
101
+ )
102
+ const res = await processFiles('s', env, () => {}, 1000)
103
+ const files = (res.payload as { files: { fileId: string }[] }).files
104
+ expect(files.map((f) => f.fileId)).toEqual(['f2', 'f3'])
105
+ })
106
+ })
107
+
108
+ describe('shouldListUpload', () => {
109
+ it('lists a row still in transit', () => {
110
+ expect(shouldListUpload({ devicePath: '', deleted: 0 }, visible)).toBe(true)
111
+ })
112
+
113
+ it('hides a moved row whose folder the client can browse', () => {
114
+ // It is not gone, it is shown where it now lives. Listing it here too
115
+ // would put the same file on the page twice.
116
+ expect(shouldListUpload({ devicePath: 'jobs/a.pdf', deleted: 0 }, visible)).toBe(false)
117
+ })
118
+
119
+ it('lists a moved row the client cannot reach in the tree', () => {
120
+ // An untargeted upload lands in uploads/, which is never exposed. Hiding
121
+ // it here would make the file the client just sent unreachable entirely.
122
+ expect(shouldListUpload({ devicePath: 'uploads/alice/a.pdf', deleted: 0 }, visible)).toBe(true)
123
+ })
124
+
125
+ it('hides a withdrawn row while the device removal is pending', () => {
126
+ expect(shouldListUpload({ devicePath: 'uploads/alice/a.pdf', deleted: 1 }, visible)).toBe(false)
127
+ })
128
+
129
+ it('hides a withdrawn row that has not moved either', () => {
130
+ expect(shouldListUpload({ devicePath: '', deleted: 1 }, visible)).toBe(false)
131
+ })
132
+
133
+ it('matches on the whole top segment, never a prefix', () => {
134
+ // `job` must not satisfy a visible `jobs`, the same whole-segment rule
135
+ // grantAllows follows.
136
+ expect(shouldListUpload({ devicePath: 'job/a.pdf', deleted: 0 }, visible)).toBe(true)
137
+ })
138
+ })