@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.
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +748 -0
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +1716 -0
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +121 -0
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +103 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +51 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +2 -2
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +20 -4
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +55 -0
- package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +19 -4
- package/payload/platform/lib/graph-style/dist/index.d.ts +24 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +25 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/additional-base-labels.test.ts +22 -0
- package/payload/platform/lib/graph-style/src/index.ts +27 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +15 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +34 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +28 -4
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +22 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +21 -0
- package/payload/platform/lib/storage-broker/src/audit.ts +67 -3
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +48 -0
- package/payload/platform/neo4j/schema.cypher +105 -0
- package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +34 -5
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -2
- package/payload/platform/plugins/cloudflare/PLUGIN.md +22 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +161 -41
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +95 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +75 -13
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +216 -16
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +38 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +83 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +48 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +62 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +103 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +211 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +168 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +138 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +179 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +14 -13
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +87 -13
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +87 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +74 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +91 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +23 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +45 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +72 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +80 -13
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +358 -16
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +12 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +45 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +258 -21
- package/payload/platform/plugins/docs/references/calendar-booking.md +5 -1
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +61 -31
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +70 -5
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js +31 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +31 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +18 -7
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts +15 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +24 -11
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +5 -122
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +84 -5
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/scripts/__tests__/quarantine-restore.test.sh +143 -0
- package/payload/platform/scripts/quarantine-restore.mjs +175 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +24 -7
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +52 -74
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +11 -8
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +32 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +32 -2
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +6 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +6 -0
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +7 -4
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/agents/passive/IDENTITY.md +6 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +6 -0
- package/payload/platform/templates/specialists/agents/citation-auditor.md +6 -0
- package/payload/platform/templates/specialists/agents/coding-assistant.md +6 -0
- package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +6 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +6 -0
- package/payload/platform/templates/specialists/agents/data-manager.md +6 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -0
- package/payload/platform/templates/specialists/agents/librarian.md +6 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -0
- package/payload/platform/templates/specialists/agents/project-manager.md +6 -0
- package/payload/platform/templates/specialists/agents/public-session-reviewer.md +6 -0
- package/payload/platform/templates/specialists/agents/research-assistant.md +7 -1
- package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +6 -0
- package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +6 -0
- package/payload/server/{chunk-REWXLAPC.js → chunk-KZFE3MOY.js} +52 -4
- package/payload/server/public/activity.html +5 -6
- package/payload/server/public/assets/AdminLoginScreens-DgVFsHtd.js +1 -0
- package/payload/server/public/assets/AdminShell-BwNqwb4k.js +2 -0
- package/payload/server/public/assets/activity-2DL2DwLr.js +1 -0
- package/payload/server/public/assets/admin-COe0dT2U.js +1 -0
- package/payload/server/public/assets/{arc-D-QzyndF.js → arc-CFMJyMm-.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-Cypt5qHL.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-W7f0Us4S.js → architectureDiagram-Q4EWVU46-CiSSgdL8.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CfPS4FXg.js → blockDiagram-DXYQGD6D-_sXtIW5c.js} +1 -1
- package/payload/server/public/assets/{bot-DnuyRsLS.js → bot-CpBuDQlF.js} +1 -1
- package/payload/server/public/assets/{browser-q4EFqKvR.js → browser-CSApuZrv.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Dq60cLVI.js → c4Diagram-AHTNJAMY-Bh7OAMXs.js} +1 -1
- package/payload/server/public/assets/{calendar-C-q9kWmA.js → calendar-DlvwZnS7.js} +1 -1
- package/payload/server/public/assets/channel-BMyXE4zc.js +1 -0
- package/payload/server/public/assets/chat-DWvYv9jq.js +1 -0
- package/payload/server/public/assets/chevron-down-Dnuuw6Q2.js +1 -0
- package/payload/server/public/assets/chevron-left-CC4zmWOr.js +1 -0
- package/payload/server/public/assets/chevron-right-CG_lHTx_.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-CXDjuDN0.js → chunk-2KRD3SAO-D4fktAwW.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-926ox0tj.js → chunk-336JU56O-CcQoKH2G.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-Xu6JKP4d.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-C2nrPCfN.js → chunk-4BX2VUAB-C13Wz3zS.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-CuW3FOR6.js → chunk-4TB4RGXK-BL48D7gN.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-B4_-In90.js → chunk-55IACEB6-TmOKdy8a.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DnYGr9CP.js → chunk-5FUZZQ4R-DZ_jSF6N.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BYCOi9bf.js → chunk-5PVQY5BW-De4G1vfV.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-CBxsC4JP.js → chunk-67CJDMHE-Cif9Uu5C.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-CAACaqFr.js → chunk-7N4EOEYR-WjTHCXnd.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-BoamZdS6.js → chunk-AA7GKIK3-CwCfdQmb.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-CHk82eFy.js → chunk-BSJP7CBP-9A67yXwT.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-DdeW9zxl.js → chunk-CIAEETIT-DPMiWSBi.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DJqSRv2w.js → chunk-EDXVE4YY-C2f_Qvx-.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-BRi4BwM5.js → chunk-ENJZ2VHE-B-Q9EbSQ.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-OJp2CtUE.js → chunk-FMBD7UC4-Bmt_ZRlV.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-BUAQ6spG.js → chunk-FOC6F5B3-DfGJGxpX.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DeRggD-Q.js → chunk-ICPOFSXX-Wyym1Q8K.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-EHiXZ8Py.js → chunk-K5T4RW27-DMpoaqw2.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-tOajPcpn.js → chunk-KGLVRYIC-DYUFgri2.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-uwO6oHIU.js → chunk-LIHQZDEY-x3LmeKmx.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-Dpa1_BCA.js → chunk-ORNJ4GCN-BeuNAWkW.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-ChdDqaQL.js → chunk-OYMX7WX6-BUWVAfQA.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-7_kS3RBy.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-5JgWosPv.js → chunk-U2HBQHQK-DKS5adQo.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-BGJqysB2.js → chunk-X2U36JSP-CT_UM4IN.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-GERE2ekx.js → chunk-XPW4576I-ByFnN4JO.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BskqX1V5.js → chunk-YZCP3GAM-DCetC20J.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-DG1iY-L9.js → chunk-ZZ45TVLE-Bh1QIv8N.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DJqbNcuC.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CGglqfQA.js +1 -0
- package/payload/server/public/assets/clone-BiTMTzp_.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C8Rl4ykh.js → cose-bilkent-S5V4N54A-DgLEaApT.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-C0YrJPF3.js → dagre-KV5264BT-BfkUPrEV.js} +1 -1
- package/payload/server/public/assets/{dagre-DTlv-FFy.js → dagre-unhMIyVo.js} +1 -1
- package/payload/server/public/assets/data-CJbC_cVy.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-Dfk5oepI.js → diagram-5BDNPKRD-BjjwcTD9.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-QRj55DiB.js → diagram-G4DWMVQ6-BbgWtVSS.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-BAPKsohU.js → diagram-MMDJMWI5-DyAnkWGy.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-C3iZXPkC.js → diagram-TYMM5635-SAMMoLxq.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DIbI7g_5.js → erDiagram-SMLLAGMA-Cvo-5-fD.js} +1 -1
- package/payload/server/public/assets/{flatten-DlAJeN0L.js → flatten-BXzaHgtL.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-l3eJJr8R.js → flowDiagram-DWJPFMVM-y3o2Ycld.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-aaEzHxIq.js → ganttDiagram-T4ZO3ILL-BEm_CQaK.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-C_-XjxLd.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di-1eg_m.js → gitGraphDiagram-UUTBAWPF-CqmOxS7F.js} +1 -1
- package/payload/server/public/assets/{graph-CNgt7ygi.js → graph-kaxS9a_7.js} +2 -2
- package/payload/server/public/assets/graph-labels-GgejRtgV.js +1 -0
- package/payload/server/public/assets/{graphlib-D5awC2yp.js → graphlib-_HfbQcBf.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-BH8yDCfL.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-DyDXyJ6U.js +2 -0
- package/payload/server/public/assets/{isEmpty-Dh1dF-tM.js → isEmpty-uB1dEu_Z.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CLOd_cNf.js → ishikawaDiagram-UXIWVN3A-C_d0vcZC.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-cGyfW6Tg.js → journeyDiagram-VCZTEJTY-DsjRBqlW.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CRTKWPlh.js → kanban-definition-6JOO6SKY-BwtK6LS-.js} +1 -1
- package/payload/server/public/assets/{line-B8Brm0TD.js → line-szBCFOLJ.js} +1 -1
- package/payload/server/public/assets/{linear-BHnSKXBe.js → linear-X3O7lwK3.js} +1 -1
- package/payload/server/public/assets/{maximize-2-k3Fu5hdP.js → maximize-2-CPq3nwZA.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BtNpdQct.js → mermaid-parser.core-BavvMAhU.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-BY87anQZ.js → mermaid.core-Br2F9Hdm.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-rUSv9qN1.js → mindmap-definition-QFDTVHPH-4qlwXoJ-.js} +1 -1
- package/payload/server/public/assets/operator-CldXXilj.js +1 -0
- package/payload/server/public/assets/{ordinal-Bnl5GL-x.js → ordinal-CZY9laxg.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-Bajcns4r.js +1 -0
- package/payload/server/public/assets/{page-Bvx0A2jB.js → page-BE6DkJP9.js} +1 -1
- package/payload/server/public/assets/{page-DhwPG5Zf.js → page-BIwdWXnv.js} +3 -3
- package/payload/server/public/assets/{pdf-render-CrpAi4Ba.js → pdf-render-H884L3EI.js} +1 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-D7hjM5w1.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-Cjn9IP-C.js → pieDiagram-DEJITSTG-BbXryIg0.js} +1 -1
- package/payload/server/public/assets/{public-BJ5Oo2Fw.js → public-BKQEQGPE.js} +1 -1
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BkSHkSsn.js → quadrantDiagram-34T5L4WZ-BHLjcvh3.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-B46d5x7a.js +1 -0
- package/payload/server/public/assets/{reduce-Bp5oNw1n.js → reduce-BGQzAenn.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-hLwgyB-F.js → requirementDiagram-MS252O5E-BXaVO0lk.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-D-ov5jM4.js → rotate-ccw-DieZzl10.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D6Dl1RIa.js → sankeyDiagram-XADWPNL6-CmN1s17Q.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-j0QBqkbL.js → sequenceDiagram-FGHM5R23-DC08e5NY.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-TvEBNsuV.js → stateDiagram-FHFEXIEX-DnoQMuOW.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CrstH34p.js +1 -0
- package/payload/server/public/assets/{tasks-DHMxTZVD.js → tasks-DYyCGz8W.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-EmV5lz6o.js → time-entry-format-CC_9jFuR.js} +1 -1
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BfivOP-j.js → timeline-definition-GMOUNBTQ-eTn3uRaH.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-3XQh5xQ1.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-DwR-_l5z.js +1 -0
- package/payload/server/public/assets/{triangle-alert-CivyPfAL.js → triangle-alert-7mQfCQWP.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-hbnPS-r9.js → useCopyFeedback-os7N0yCx.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-Bgfqig1V.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-QN9_GqB_.js → useVoiceRecorder-ZFwt5i6d.js} +2 -2
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Bvfw9aFJ.js → vennDiagram-DHZGUBPP-DNx6o_ZQ.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DZp9oCGv.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C9h04G6b.js → wardleyDiagram-NUSXRM2D-BZOchqF3.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BMvi2m2q.js → xychartDiagram-5P7HB3ND-ZBEGH6ZI.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +14 -14
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +10 -11
- package/payload/server/public/index.html +16 -17
- package/payload/server/public/operator.html +15 -16
- package/payload/server/public/public.html +14 -14
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +1836 -998
- package/payload/server/{src-JJ6PHO5I.js → src-UYDFMWF4.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-Dic_fPt6.js +0 -1
- package/payload/server/public/assets/AdminShell-JN7rZ8aG.js +0 -2
- package/payload/server/public/assets/Checkbox-CB74kINA.js +0 -1
- package/payload/server/public/assets/activity-DbfpHg3i.js +0 -1
- package/payload/server/public/assets/admin-CThbXaYH.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-CGvD4r9F.js +0 -1
- package/payload/server/public/assets/channel-Bpg-ADC0.js +0 -1
- package/payload/server/public/assets/chat-DcefIV9q.js +0 -1
- package/payload/server/public/assets/chevron-down-Blma_CKl.js +0 -1
- package/payload/server/public/assets/chevron-left-xeYDxFiC.js +0 -1
- package/payload/server/public/assets/chevron-right-BonbtjqJ.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-1poG7Nd3.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-GN1tIO8J.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DMGVq8Jv.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfddT8UV.js +0 -1
- package/payload/server/public/assets/clone-Cc9vFIEk.js +0 -1
- package/payload/server/public/assets/data-D5dp6nj6.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-DtjXE0UI.js +0 -1
- package/payload/server/public/assets/graph-labels-DA48HrRR.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-CmArhGYf.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-utWz7oZy.js +0 -2
- package/payload/server/public/assets/operator-8NVpNLeD.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-BTfK0Zw4.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-BvtPeMQY.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-D2UXvQgn.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ur4eM6rb.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-0dE7FvM2.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-BrwlXpr5.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-kwhG-aJj.css +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-CO9HdfJ-.js +0 -1
- /package/payload/server/public/assets/{_baseFor-BBkmqPEk.js → _baseFor-CMWvz0uA.js} +0 -0
- /package/payload/server/public/assets/{array-Bjtb2C_5.js → array-Cny_NT6T.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-B1dQ664k.js → cytoscape.esm-De0CCmB2.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-iwYA7ne8.js → defaultLocale-BahRht8U.js} +0 -0
- /package/payload/server/public/assets/{dist-CLmrZbY7.js → dist-C9D_xz81.js} +0 -0
- /package/payload/server/public/assets/{init-CxnBAgcf.js → init-DoIvjoJ_.js} +0 -0
- /package/payload/server/public/assets/{katex-DojHoLIr.js → katex-Bfn1OZEl.js} +0 -0
- /package/payload/server/public/assets/{path-JZiojYzQ.js → path-CTezU8L5.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-CT1Z6Pdu.js → preload-helper-D_sPnjeL.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-D5NinLFK.js → rough.esm-BJo4pvXu.js} +0 -0
- /package/payload/server/public/assets/{src-DQ0MSD6q.js → src-BTKk8KAG.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DotTrvX3.js → useSubAccountSwitcher-Q941cZYV.js} +0 -0
|
@@ -57,14 +57,23 @@ export async function readExposeFolders(accountDir) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Files under `exposed` dirs, at any depth
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Files AND directories under `exposed` dirs, at any depth, each row carrying
|
|
61
|
+
* `isDir`. Directories are published rather than left to be derived from file
|
|
62
|
+
* prefixes, because a folder holding no files at any depth has no prefix to be
|
|
63
|
+
* derived from and would not exist to the client at all.
|
|
64
|
+
*
|
|
65
|
+
* A directory row is emitted only after its own `readdir` succeeds, so an
|
|
66
|
+
* exposed name that is absent or unreadable on disk still contributes nothing
|
|
67
|
+
* and the portal never renders a folder the device does not have.
|
|
68
|
+
*
|
|
69
|
+
* Dot-prefixed entries are skipped at every level: `.uploads-tmp/` holds
|
|
70
|
+
* in-flight upload partials (files.ts:25) and must never be published as a
|
|
71
|
+
* deliverable, as a file row or as a folder.
|
|
63
72
|
* @param {string} accountDir @param {string[]} exposed
|
|
64
|
-
* @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string}[]>}
|
|
73
|
+
* @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string, isDir: boolean}[]>}
|
|
65
74
|
*/
|
|
66
75
|
export async function walkExposed(accountDir, exposed) {
|
|
67
|
-
/** @type {{relPath: string, sizeBytes: number, modifiedAt: string}[]} */
|
|
76
|
+
/** @type {{relPath: string, sizeBytes: number, modifiedAt: string, isDir: boolean}[]} */
|
|
68
77
|
const out = []
|
|
69
78
|
|
|
70
79
|
/** @param {string} rel */
|
|
@@ -75,6 +84,14 @@ export async function walkExposed(accountDir, exposed) {
|
|
|
75
84
|
} catch {
|
|
76
85
|
return // absent or unreadable dir contributes nothing
|
|
77
86
|
}
|
|
87
|
+
try {
|
|
88
|
+
const st = await stat(join(accountDir, rel))
|
|
89
|
+
// sizeBytes 0 rather than the directory inode's own size: the column
|
|
90
|
+
// means "bytes a client can fetch", and a folder has none.
|
|
91
|
+
out.push({ relPath: rel, sizeBytes: 0, modifiedAt: st.mtime.toISOString(), isDir: true })
|
|
92
|
+
} catch {
|
|
93
|
+
// vanished between the readdir and the stat; the next cycle settles it
|
|
94
|
+
}
|
|
78
95
|
for (const e of entries) {
|
|
79
96
|
if (e.name.startsWith('.')) continue
|
|
80
97
|
const childRel = `${rel}/${e.name}`
|
|
@@ -83,7 +100,12 @@ export async function walkExposed(accountDir, exposed) {
|
|
|
83
100
|
} else if (e.isFile()) {
|
|
84
101
|
try {
|
|
85
102
|
const st = await stat(join(accountDir, childRel))
|
|
86
|
-
out.push({
|
|
103
|
+
out.push({
|
|
104
|
+
relPath: childRel,
|
|
105
|
+
sizeBytes: st.size,
|
|
106
|
+
modifiedAt: st.mtime.toISOString(),
|
|
107
|
+
isDir: false,
|
|
108
|
+
})
|
|
87
109
|
} catch {
|
|
88
110
|
// vanished mid-walk; the next cycle settles it
|
|
89
111
|
}
|
|
@@ -156,22 +178,71 @@ export async function pushAccount(opts) {
|
|
|
156
178
|
|
|
157
179
|
const rows = await walkExposed(accountDir, r.exposed)
|
|
158
180
|
|
|
159
|
-
|
|
181
|
+
// Stage, flip, then sweep (Task 1910, superseding backlog Task 1842).
|
|
182
|
+
//
|
|
183
|
+
// The replace used to be DELETE-then-INSERT, which left the account's tree
|
|
184
|
+
// empty or half-built for the whole insert run. A client listing inside that
|
|
185
|
+
// window saw a portal with nothing in it. At sixty-second cadence that window
|
|
186
|
+
// is hit sixty times more often than it was hourly, which is what turned a
|
|
187
|
+
// known defect into a blocking one.
|
|
188
|
+
//
|
|
189
|
+
// Now every row is written at the NEXT generation, one statement moves the
|
|
190
|
+
// pointer, and only then are superseded rows removed. The flip is the atomic
|
|
191
|
+
// point: a reader sees the old tree or the new one, never a mixture. The
|
|
192
|
+
// sweep afterwards need not be atomic, because nothing reads a generation the
|
|
193
|
+
// pointer does not name.
|
|
194
|
+
const state = await client.query(
|
|
195
|
+
'SELECT currentGeneration FROM directory_state WHERE accountId = ?',
|
|
196
|
+
[accountId],
|
|
197
|
+
)
|
|
198
|
+
const current = Number(state[0]?.currentGeneration ?? 0)
|
|
199
|
+
const next = current + 1
|
|
200
|
+
|
|
160
201
|
for (const row of rows) {
|
|
161
202
|
await client.query(
|
|
162
|
-
'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt
|
|
163
|
-
|
|
203
|
+
'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt, isDir, generation) ' +
|
|
204
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
205
|
+
[accountId, row.relPath, row.sizeBytes, row.modifiedAt, nowIso, row.isDir ? 1 : 0, next],
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
log(`${TAG} op=stage account=${accountId} generation=${next} inserted=${rows.length}`)
|
|
209
|
+
|
|
210
|
+
// THE atomic point. Upsert rather than insert-or-update, so an account
|
|
211
|
+
// publishing for the first time and one republishing take the same path.
|
|
212
|
+
await client.query(
|
|
213
|
+
'INSERT INTO directory_state (accountId, currentGeneration) VALUES (?, ?) ' +
|
|
214
|
+
'ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration',
|
|
215
|
+
[accountId, next],
|
|
216
|
+
)
|
|
217
|
+
log(`${TAG} op=flip account=${accountId} generation=${next}`)
|
|
218
|
+
|
|
219
|
+
// Housekeeping only. A failure here leaves stale rows nothing reads, which is
|
|
220
|
+
// waste rather than incorrectness — so it must not fail the publish that has
|
|
221
|
+
// already succeeded.
|
|
222
|
+
try {
|
|
223
|
+
await client.query('DELETE FROM directory WHERE accountId = ? AND generation != ?', [
|
|
224
|
+
accountId,
|
|
225
|
+
next,
|
|
226
|
+
])
|
|
227
|
+
} catch (err) {
|
|
228
|
+
log(
|
|
229
|
+
`${TAG} op=sweep account=${accountId} generation=${next} result=failed ` +
|
|
230
|
+
`err="${err instanceof Error ? err.message : String(err)}"`,
|
|
164
231
|
)
|
|
165
232
|
}
|
|
166
|
-
log(`${TAG} op=replace account=${accountId} deleted=all inserted=${rows.length}`)
|
|
167
233
|
|
|
168
234
|
// Read back rather than trusting the write: "issued the inserts" is not "the
|
|
169
|
-
// rows are there", and a half-applied batch is otherwise invisible.
|
|
170
|
-
|
|
235
|
+
// rows are there", and a half-applied batch is otherwise invisible. Counted
|
|
236
|
+
// through the pointer, so this measures what a client would actually see.
|
|
237
|
+
const check = await client.query(
|
|
238
|
+
'SELECT COUNT(*) AS n FROM directory d JOIN directory_state s ' +
|
|
239
|
+
'ON s.accountId = d.accountId AND s.currentGeneration = d.generation WHERE d.accountId = ?',
|
|
240
|
+
[accountId],
|
|
241
|
+
)
|
|
171
242
|
const after = Number(check[0]?.n ?? -1)
|
|
172
243
|
log(`${TAG} op=verify account=${accountId} rowsAfter=${after} expected=${rows.length}`)
|
|
173
244
|
|
|
174
|
-
return { exposed: r.exposed, rows: rows.length, schemaPresent: true }
|
|
245
|
+
return { exposed: r.exposed, rows: rows.length, schemaPresent: true, generation: next }
|
|
175
246
|
}
|
|
176
247
|
|
|
177
248
|
/**
|
|
@@ -301,39 +372,44 @@ function resolveCloudflareCredentials(platformRoot) {
|
|
|
301
372
|
return { cfAccountId: house.CLOUDFLARE_ACCOUNT_ID, token }
|
|
302
373
|
}
|
|
303
374
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Publish every portal-serving account's index.
|
|
377
|
+
*
|
|
378
|
+
* Exported so the admin server's 60s loop and this script's CLI entry run the
|
|
379
|
+
* SAME code (Task 1910). The loop replaced an hourly heartbeat spawn whose own
|
|
380
|
+
* header recorded it had never been observed running on a device; two
|
|
381
|
+
* implementations of the publish would have reintroduced exactly that class of
|
|
382
|
+
* problem one layer along.
|
|
383
|
+
*
|
|
384
|
+
* Returns a summary rather than exiting, so a caller inside a long-lived
|
|
385
|
+
* process can report it. Throwing is reserved for a genuine break — an absent
|
|
386
|
+
* house credential where portals exist — which the loop registry surfaces as a
|
|
387
|
+
* failed run rather than a silent one.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} platformRoot
|
|
390
|
+
* @returns {Promise<{accounts: number, pushed: number, failed: number}>}
|
|
391
|
+
*/
|
|
392
|
+
export async function pushAllAccounts(platformRoot) {
|
|
393
|
+
const accountsRoot = resolve(platformRoot, '..', 'data', 'accounts')
|
|
394
|
+
const targets = await portalAccounts(accountsRoot)
|
|
395
|
+
console.error(`${TAG} op=enumerate accountsRoot="${accountsRoot}" accounts=${targets.length}`)
|
|
396
|
+
// Before minting a credential, exactly as the pull and the audit do: most
|
|
397
|
+
// installs have neither a portal nor a house credential, and minting
|
|
398
|
+
// unconditionally would make this loop report an error on every one forever.
|
|
399
|
+
if (targets.length === 0) return { accounts: 0, pushed: 0, failed: 0 }
|
|
310
400
|
|
|
311
401
|
const cred = resolveCloudflareCredentials(platformRoot)
|
|
312
402
|
if ('error' in cred) {
|
|
313
|
-
|
|
314
|
-
process.exit(1)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** @type {{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]} */ let targets
|
|
318
|
-
if (process.argv.includes('--all-accounts')) {
|
|
319
|
-
// Same derivation as the platform's own DATA_ROOT (data-path.ts:19), from
|
|
320
|
-
// the PLATFORM_ROOT the heartbeat already passes.
|
|
321
|
-
const accountsRoot = resolve(platformRoot, '..', 'data', 'accounts')
|
|
322
|
-
targets = await portalAccounts(accountsRoot)
|
|
323
|
-
console.error(`${TAG} op=enumerate accountsRoot="${accountsRoot}" accounts=${targets.length}`)
|
|
324
|
-
} else {
|
|
325
|
-
const accountDir = arg('--account-dir')
|
|
326
|
-
const accountId = arg('--account')
|
|
327
|
-
const dbName = arg('--db')
|
|
328
|
-
if (!accountDir || !accountId || !dbName) {
|
|
329
|
-
console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
|
|
330
|
-
process.exit(1)
|
|
331
|
-
}
|
|
332
|
-
targets = [{ accountDir, accountId, dbName, exposeFolders: await readExposeFolders(accountDir) }]
|
|
403
|
+
throw new Error(cred.error)
|
|
333
404
|
}
|
|
405
|
+
return runTargets(platformRoot, cred, targets)
|
|
406
|
+
}
|
|
334
407
|
|
|
408
|
+
/** @returns {Promise<{accounts: number, pushed: number, failed: number}>} */
|
|
409
|
+
async function runTargets(platformRoot, cred, targets) {
|
|
335
410
|
const state = readState(platformRoot)
|
|
336
411
|
let failed = 0
|
|
412
|
+
let pushed = 0
|
|
337
413
|
for (const t of targets) {
|
|
338
414
|
try {
|
|
339
415
|
// cfAccountId addresses the D1 endpoint; t.accountId scopes the rows.
|
|
@@ -360,6 +436,7 @@ async function main() {
|
|
|
360
436
|
exposed: r.exposed,
|
|
361
437
|
})
|
|
362
438
|
}
|
|
439
|
+
pushed++
|
|
363
440
|
console.error(`${TAG} op=done account=${t.accountId} rows=${r.rows}`)
|
|
364
441
|
} catch (err) {
|
|
365
442
|
// One account's failure must not stop the rest, and it must never be
|
|
@@ -370,10 +447,53 @@ async function main() {
|
|
|
370
447
|
)
|
|
371
448
|
}
|
|
372
449
|
}
|
|
373
|
-
|
|
450
|
+
return { accounts: targets.length, pushed, failed }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function main() {
|
|
454
|
+
const platformRoot = process.env.PLATFORM_ROOT
|
|
455
|
+
if (!platformRoot) {
|
|
456
|
+
console.error(`${TAG} op=usage reason=no-platform-root PLATFORM_ROOT is required`)
|
|
457
|
+
process.exit(1)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (process.argv.includes('--all-accounts')) {
|
|
461
|
+
try {
|
|
462
|
+
const r = await pushAllAccounts(platformRoot)
|
|
463
|
+
if (r.failed > 0) process.exit(1)
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.error(`${TAG} op=skip reason=${err instanceof Error ? err.message : String(err)}`)
|
|
466
|
+
process.exit(1)
|
|
467
|
+
}
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const accountDir = arg('--account-dir')
|
|
472
|
+
const accountId = arg('--account')
|
|
473
|
+
const dbName = arg('--db')
|
|
474
|
+
if (!accountDir || !accountId || !dbName) {
|
|
475
|
+
console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
|
|
476
|
+
process.exit(1)
|
|
477
|
+
}
|
|
478
|
+
const cred = resolveCloudflareCredentials(platformRoot)
|
|
479
|
+
if ('error' in cred) {
|
|
480
|
+
console.error(`${TAG} op=skip reason=${cred.error}`)
|
|
481
|
+
process.exit(1)
|
|
482
|
+
}
|
|
483
|
+
const r = await runTargets(platformRoot, cred, [
|
|
484
|
+
{ accountDir, accountId, dbName, exposeFolders: await readExposeFolders(accountDir) },
|
|
485
|
+
])
|
|
486
|
+
if (r.failed > 0) process.exit(1)
|
|
374
487
|
}
|
|
375
488
|
|
|
376
489
|
// Only when invoked directly, so the module stays importable by tests.
|
|
377
|
-
|
|
490
|
+
// The basename check is what survives the UI bundler inlining this file into
|
|
491
|
+
// server.js: when bundled, `process.argv[1]` (server.js) and `import.meta.url`
|
|
492
|
+
// (server.js after esbuild's transform) resolve to the same path, and a pure
|
|
493
|
+
// path-equality guard fires main() at server boot, exiting the payload before
|
|
494
|
+
// its first line served. Matching on the mjs basename keeps this cheap CLI
|
|
495
|
+
// entry point from tripping in-process.
|
|
496
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ''
|
|
497
|
+
if (invokedPath && invokedPath.endsWith('/portal-index-push.mjs') && invokedPath === resolve(fileURLToPath(import.meta.url))) {
|
|
378
498
|
void main()
|
|
379
499
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { processDelete } from '../../skills/data-portal/template/functions/api/delete'
|
|
3
|
+
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
4
|
+
|
|
5
|
+
/** `row` is what the manifest holds for the key under test, or null when the
|
|
6
|
+
* key is unknown. The fake tracks which of the three writes actually happened
|
|
7
|
+
* so a test can assert the R2 object was never touched. */
|
|
8
|
+
function harness(row: { devicePath: string } | null) {
|
|
9
|
+
const state = { objectDeleted: false, rowDeleted: false, marked: false }
|
|
10
|
+
const lines: string[] = []
|
|
11
|
+
|
|
12
|
+
const env = {
|
|
13
|
+
BUCKET: {
|
|
14
|
+
async delete() {
|
|
15
|
+
state.objectDeleted = true
|
|
16
|
+
},
|
|
17
|
+
async head() {
|
|
18
|
+
return state.objectDeleted ? null : { size: 1 }
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
DB: {
|
|
22
|
+
prepare(sql: string) {
|
|
23
|
+
return {
|
|
24
|
+
bind() {
|
|
25
|
+
return {
|
|
26
|
+
async first() {
|
|
27
|
+
if (sql.includes('FROM sessions')) {
|
|
28
|
+
return { ownerId: 'alice', accountId: 'acct', folders: '' }
|
|
29
|
+
}
|
|
30
|
+
if (sql.includes('SELECT devicePath')) return row
|
|
31
|
+
if (sql.includes('SELECT deleted')) return { deleted: state.marked ? 1 : 0 }
|
|
32
|
+
if (sql.includes('SELECT fileId FROM manifest')) {
|
|
33
|
+
return state.rowDeleted || !row ? null : { fileId: 'f1' }
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
},
|
|
37
|
+
async run() {
|
|
38
|
+
if (/^UPDATE manifest/.test(sql)) state.marked = true
|
|
39
|
+
if (/^DELETE FROM manifest/.test(sql)) state.rowDeleted = true
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
} as unknown as PortalEnv
|
|
47
|
+
|
|
48
|
+
return { env, state, lines, log: (l: string) => lines.push(l) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('delete branches on whether the file has moved', () => {
|
|
52
|
+
it('deletes object and row outright when the file is still in transit', async () => {
|
|
53
|
+
const h = harness({ devicePath: '' })
|
|
54
|
+
const res = await processDelete('s', 'alice/a.pdf', h.env, h.log, () => 1)
|
|
55
|
+
expect(res.status).toBe(200)
|
|
56
|
+
expect(h.state.objectDeleted).toBe(true)
|
|
57
|
+
expect(h.state.rowDeleted).toBe(true)
|
|
58
|
+
expect(h.state.marked).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('marks a moved file for withdrawal instead of touching R2', async () => {
|
|
62
|
+
// The bytes are on the device and the R2 object is already gone. Deleting
|
|
63
|
+
// here would report success while the client's file stayed on the device
|
|
64
|
+
// forever — the exact failure this branch exists to prevent.
|
|
65
|
+
const h = harness({ devicePath: 'jobs/a.pdf' })
|
|
66
|
+
const res = await processDelete('s', 'alice/jobs/a.pdf', h.env, h.log, () => 1)
|
|
67
|
+
expect(res.status).toBe(200)
|
|
68
|
+
expect(h.state.marked).toBe(true)
|
|
69
|
+
expect(h.state.rowDeleted).toBe(false)
|
|
70
|
+
expect(h.state.objectDeleted).toBe(false)
|
|
71
|
+
expect(h.lines.some((l) => l.includes('op=withdraw') && l.includes('result=marked'))).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('tells the client the withdrawal is pending, not complete', async () => {
|
|
75
|
+
const h = harness({ devicePath: 'jobs/a.pdf' })
|
|
76
|
+
const res = await processDelete('s', 'alice/jobs/a.pdf', h.env, h.log, () => 1)
|
|
77
|
+
expect(res.payload).toMatchObject({ ok: true, pending: true })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('reports an unknown key as not found without writing anything', async () => {
|
|
81
|
+
const h = harness(null)
|
|
82
|
+
const res = await processDelete('s', 'alice/nope.pdf', h.env, h.log, () => 1)
|
|
83
|
+
expect(res.status).toBe(404)
|
|
84
|
+
expect(h.state.objectDeleted).toBe(false)
|
|
85
|
+
expect(h.state.marked).toBe(false)
|
|
86
|
+
expect(h.state.rowDeleted).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('still refuses another person key before reading anything', async () => {
|
|
90
|
+
const h = harness({ devicePath: '' })
|
|
91
|
+
const res = await processDelete('s', 'bob/a.pdf', h.env, h.log, () => 1)
|
|
92
|
+
expect(res.status).toBe(403)
|
|
93
|
+
expect(h.state.objectDeleted).toBe(false)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -2,19 +2,28 @@ import { describe, it, expect } from 'vitest'
|
|
|
2
2
|
import { processFiles, entriesUnder } from '../../skills/data-portal/template/functions/api/files'
|
|
3
3
|
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
4
4
|
|
|
5
|
+
// Mirrors what the push writes: a row per folder as well as per file. `jobs` is
|
|
6
|
+
// exposed and holds nothing, which is the case that had no row at all before.
|
|
5
7
|
const DIRECTORY = [
|
|
6
|
-
{ accountId: 'acc-a', relPath: 'output
|
|
7
|
-
{ accountId: 'acc-a', relPath: '
|
|
8
|
-
{ accountId: 'acc-a', relPath: 'quotes
|
|
9
|
-
{ accountId: 'acc-
|
|
8
|
+
{ accountId: 'acc-a', relPath: 'output', sizeBytes: 0, modifiedAt: '2026-07-20T10:00:00.000Z', isDir: 1 },
|
|
9
|
+
{ accountId: 'acc-a', relPath: 'output/report.pdf', sizeBytes: 10, modifiedAt: '2026-07-20T10:00:00.000Z', isDir: 0 },
|
|
10
|
+
{ accountId: 'acc-a', relPath: 'quotes', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
11
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2969', sizeBytes: 0, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 1 },
|
|
12
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2969/quote.pdf', sizeBytes: 20, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 0 },
|
|
13
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2974', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
14
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2974/quote.pdf', sizeBytes: 30, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 0 },
|
|
15
|
+
{ accountId: 'acc-a', relPath: 'jobs', sizeBytes: 0, modifiedAt: '2026-07-20T14:00:00.000Z', isDir: 1 },
|
|
16
|
+
{ accountId: 'acc-b', relPath: 'output/secret.pdf', sizeBytes: 40, modifiedAt: '2026-07-20T13:00:00.000Z', isDir: 0 },
|
|
10
17
|
]
|
|
11
18
|
|
|
12
19
|
function makeEnv(
|
|
13
20
|
sessions: Record<string, { ownerId: string; accountId: string; folders?: string }>,
|
|
21
|
+
onQuery: (query: string) => void = () => {},
|
|
14
22
|
) {
|
|
15
23
|
return {
|
|
16
24
|
DB: {
|
|
17
25
|
prepare(query: string) {
|
|
26
|
+
onQuery(query)
|
|
18
27
|
let bound: unknown[] = []
|
|
19
28
|
const stmt = {
|
|
20
29
|
bind(...v: unknown[]) {
|
|
@@ -47,9 +56,9 @@ type Entry = { name: string; kind: string; relPath: string; sizeBytes: number |
|
|
|
47
56
|
describe('entriesUnder', () => {
|
|
48
57
|
const rows = DIRECTORY.filter((d) => d.accountId === 'acc-a')
|
|
49
58
|
|
|
50
|
-
it('
|
|
59
|
+
it('lists top-level folders, including one holding no files', () => {
|
|
51
60
|
const e = entriesUnder(rows, '')
|
|
52
|
-
expect(e.map((x) => x.name)).toEqual(['output', 'quotes'])
|
|
61
|
+
expect(e.map((x) => x.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
53
62
|
expect(e.every((x) => x.kind === 'directory')).toBe(true)
|
|
54
63
|
})
|
|
55
64
|
|
|
@@ -57,6 +66,42 @@ describe('entriesUnder', () => {
|
|
|
57
66
|
expect(entriesUnder(rows, 'quotes').map((x) => x.name)).toEqual(['CR2969', 'CR2974'])
|
|
58
67
|
})
|
|
59
68
|
|
|
69
|
+
// An empty folder must open, not read as missing. Its own row is what puts it
|
|
70
|
+
// in the parent listing; inside it there is nothing to show.
|
|
71
|
+
it('opens an empty folder to an empty listing', () => {
|
|
72
|
+
expect(entriesUnder(rows, 'jobs')).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// The explicit row and the row implied by a nested file name the same folder.
|
|
76
|
+
// Two entries for one folder would render it twice.
|
|
77
|
+
it('collapses an explicit folder row and one implied by a nested file', () => {
|
|
78
|
+
const both = [
|
|
79
|
+
{ relPath: 'quotes', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
80
|
+
{ relPath: 'quotes/CR2969/quote.pdf', sizeBytes: 20, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 0 },
|
|
81
|
+
]
|
|
82
|
+
const e = entriesUnder(both, '')
|
|
83
|
+
expect(e.length).toBe(1)
|
|
84
|
+
expect(e[0]).toEqual({
|
|
85
|
+
name: 'quotes',
|
|
86
|
+
kind: 'directory',
|
|
87
|
+
relPath: 'quotes',
|
|
88
|
+
sizeBytes: null,
|
|
89
|
+
modifiedAt: null,
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Without the isDir check a folder row has no slash left after the prefix and
|
|
94
|
+
// lands in the file list, where clicking it would try to download a folder.
|
|
95
|
+
it('never renders a folder row as a file', () => {
|
|
96
|
+
const e = entriesUnder(
|
|
97
|
+
[{ relPath: 'jobs', sizeBytes: 0, modifiedAt: '2026-07-20T14:00:00.000Z', isDir: 1 }],
|
|
98
|
+
'',
|
|
99
|
+
)
|
|
100
|
+
expect(e).toEqual([
|
|
101
|
+
{ name: 'jobs', kind: 'directory', relPath: 'jobs', sizeBytes: null, modifiedAt: null },
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
60
105
|
it('lists files inside a leaf folder with size and time', () => {
|
|
61
106
|
expect(entriesUnder(rows, 'quotes/CR2969')).toEqual([
|
|
62
107
|
{
|
|
@@ -71,9 +116,9 @@ describe('entriesUnder', () => {
|
|
|
71
116
|
|
|
72
117
|
it('puts folders before files and files newest first', () => {
|
|
73
118
|
const mixed = [
|
|
74
|
-
{ relPath: 'output/old.pdf', sizeBytes: 1, modifiedAt: '2026-01-01T00:00:00.000Z' },
|
|
75
|
-
{ relPath: 'output/new.pdf', sizeBytes: 1, modifiedAt: '2026-09-01T00:00:00.000Z' },
|
|
76
|
-
{ relPath: 'output/sub/x.pdf', sizeBytes: 1, modifiedAt: '2026-05-01T00:00:00.000Z' },
|
|
119
|
+
{ relPath: 'output/old.pdf', sizeBytes: 1, modifiedAt: '2026-01-01T00:00:00.000Z', isDir: 0 },
|
|
120
|
+
{ relPath: 'output/new.pdf', sizeBytes: 1, modifiedAt: '2026-09-01T00:00:00.000Z', isDir: 0 },
|
|
121
|
+
{ relPath: 'output/sub/x.pdf', sizeBytes: 1, modifiedAt: '2026-05-01T00:00:00.000Z', isDir: 0 },
|
|
77
122
|
]
|
|
78
123
|
expect(entriesUnder(mixed, 'output').map((x) => x.name)).toEqual(['sub', 'new.pdf', 'old.pdf'])
|
|
79
124
|
})
|
|
@@ -85,9 +130,18 @@ describe('entriesUnder', () => {
|
|
|
85
130
|
})
|
|
86
131
|
|
|
87
132
|
describe('processFiles directory listing', () => {
|
|
88
|
-
it('returns
|
|
133
|
+
it('returns the account folders, including one holding no files', async () => {
|
|
89
134
|
const r = await processFiles('sess-a', makeEnv(SESSIONS), () => {}, 0, '')
|
|
90
|
-
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['output', 'quotes'])
|
|
135
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// The column is what tells a folder row from a file row; a SELECT that drops
|
|
139
|
+
// it renders every folder as an unopenable file.
|
|
140
|
+
it('reads isDir in the directory select', async () => {
|
|
141
|
+
const queries: string[] = []
|
|
142
|
+
const env = makeEnv(SESSIONS, (q) => queries.push(q))
|
|
143
|
+
await processFiles('sess-a', env, () => {}, 0, '')
|
|
144
|
+
expect(queries.some((q) => /FROM directory/i.test(q) && /isDir/.test(q))).toBe(true)
|
|
91
145
|
})
|
|
92
146
|
|
|
93
147
|
it('never leaks another account rows', async () => {
|
|
@@ -129,15 +183,23 @@ describe('processFiles directory listing', () => {
|
|
|
129
183
|
it('an empty grant shows the full set', async () => {
|
|
130
184
|
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: '' } })
|
|
131
185
|
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
132
|
-
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['output', 'quotes'])
|
|
186
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
133
187
|
})
|
|
134
188
|
|
|
135
189
|
it('a grant naming an absent folder shows nothing', async () => {
|
|
136
|
-
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: '
|
|
190
|
+
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'parts' } })
|
|
137
191
|
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
138
192
|
expect(r.payload.entries).toEqual([])
|
|
139
193
|
})
|
|
140
194
|
|
|
195
|
+
// The grant narrows the account's exposed set, and an empty folder is inside
|
|
196
|
+
// that set like any other: outside the grant it stays hidden.
|
|
197
|
+
it('an empty folder outside the grant stays hidden', async () => {
|
|
198
|
+
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'quotes' } })
|
|
199
|
+
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
200
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).not.toContain('jobs')
|
|
201
|
+
})
|
|
202
|
+
|
|
141
203
|
it('the grant holds when browsing into a granted folder', async () => {
|
|
142
204
|
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'quotes' } })
|
|
143
205
|
const r = await processFiles('sess-a', env, () => {}, 0, 'quotes')
|