@rubytech/create-maxy-code 0.1.489 → 0.1.491
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-1910-two-way-portal-exchange.md +1716 -0
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +103 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +29 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +25 -2
- 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/audit.ts +59 -1
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +48 -0
- package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +34 -5
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +28 -8
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +133 -35
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +95 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +141 -9
- 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 +111 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +14 -13
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +52 -14
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +70 -7
- 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 +59 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +58 -9
- 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 +9 -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 +216 -20
- 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/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-UFXRUKXO.js → chunk-KZFE3MOY.js} +49 -3
- 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-Cyh6FWaV.js → graph-kaxS9a_7.js} +2 -2
- package/payload/server/public/assets/{graph-labels-CUQtXFKW.js → graph-labels-GgejRtgV.js} +1 -1
- 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-Cno8NNMk.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 +1488 -895
- package/payload/server/{src-YJF3UJNR.js → src-UYDFMWF4.js} +1 -1
- package/payload/platform/lib/active-rules/package-lock.json +0 -12
- 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-DvQPTxqe.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-Bu-5SUmR.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-DtjXE0UI.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
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Handler, Logger, PortalEnv } from './_lib/types'
|
|
2
2
|
import { readSessionCookie, resolveSession } from './_lib/session'
|
|
3
3
|
import { q } from './_lib/log'
|
|
4
|
-
import { authorizeKey
|
|
4
|
+
import { authorizeKey } from './_lib/authorize'
|
|
5
|
+
import { checkTarget, uploadKey } from './_lib/target'
|
|
5
6
|
|
|
6
7
|
const MAX_FILENAME = 200
|
|
7
8
|
|
|
@@ -14,9 +15,28 @@ const MAX_FILENAME = 200
|
|
|
14
15
|
// isolate, and the two must move independently.
|
|
15
16
|
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The per-part ceiling (Task 1910).
|
|
20
|
+
*
|
|
21
|
+
* Identical to MAX_UPLOAD_BYTES and for exactly the same reason: a part is read
|
|
22
|
+
* wholly into the Function's memory just as a single-shot body is. Parts do not
|
|
23
|
+
* relax the isolate bound, they let a large file respect it repeatedly.
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_PART_BYTES = 25 * 1024 * 1024
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The whole-file ceiling.
|
|
29
|
+
*
|
|
30
|
+
* Not an isolate limit — no single request ever carries this much. It bounds
|
|
31
|
+
* what one client may push through one portal in one go, so a mistaken drag of
|
|
32
|
+
* a disk image is refused before it consumes an account's storage.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_TOTAL_BYTES = 1024 * 1024 * 1024
|
|
35
|
+
|
|
17
36
|
export async function processUpload(
|
|
18
37
|
sessionId: string,
|
|
19
38
|
filename: string,
|
|
39
|
+
targetPath: string,
|
|
20
40
|
bytes: Uint8Array,
|
|
21
41
|
env: PortalEnv,
|
|
22
42
|
log: Logger,
|
|
@@ -41,7 +61,22 @@ export async function processUpload(
|
|
|
41
61
|
return { status: 400, payload: { ok: false, error: 'filename required' } }
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
|
|
64
|
+
// The target is resolved BEFORE any R2 call, the same ordering rule the owner
|
|
65
|
+
// gate follows: a denied caller must never be able to cause a store write.
|
|
66
|
+
const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
|
|
67
|
+
if (!target.ok) {
|
|
68
|
+
log(
|
|
69
|
+
`[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
|
|
70
|
+
`targetPath=${q(targetPath)} result=denied-${target.reason}`,
|
|
71
|
+
)
|
|
72
|
+
return { status: 403, payload: { ok: false, error: 'target not allowed' } }
|
|
73
|
+
}
|
|
74
|
+
log(
|
|
75
|
+
`[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
|
|
76
|
+
`targetPath=${q(targetPath)} result=allowed`,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const key = uploadKey(owner, targetPath, filename)
|
|
45
80
|
// The key is derived from the session owner, then re-checked through the same
|
|
46
81
|
// gate the other routes use — a filename carrying '..' must not escape.
|
|
47
82
|
if (!authorizeKey(owner, key)) {
|
|
@@ -63,7 +98,8 @@ export async function processUpload(
|
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
log(
|
|
66
|
-
`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)}
|
|
101
|
+
`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)} ` +
|
|
102
|
+
`targetPath=${q(targetPath)} bytes=${bytes.byteLength}`,
|
|
67
103
|
)
|
|
68
104
|
|
|
69
105
|
try {
|
|
@@ -85,17 +121,15 @@ export async function processUpload(
|
|
|
85
121
|
// person their upload failed. Resetting `ingested` is the load-bearing
|
|
86
122
|
// part: without it a replacement of an already-swept file would never be
|
|
87
123
|
// ingested, and nothing would ever say so.
|
|
88
|
-
await env
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
.bind(fileId, owner, filename, key, bytes.byteLength, new Date(startedMs).toISOString(), 0)
|
|
98
|
-
.run()
|
|
124
|
+
await writeManifestRow(env, {
|
|
125
|
+
fileId,
|
|
126
|
+
owner,
|
|
127
|
+
filename,
|
|
128
|
+
key,
|
|
129
|
+
size: bytes.byteLength,
|
|
130
|
+
at: new Date(startedMs).toISOString(),
|
|
131
|
+
targetPath,
|
|
132
|
+
})
|
|
99
133
|
log(`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=ok`)
|
|
100
134
|
} catch (err) {
|
|
101
135
|
log(
|
|
@@ -118,29 +152,337 @@ export async function processUpload(
|
|
|
118
152
|
return { status: 200, payload: { ok: true, fileId, objectPresent, rowPresent } }
|
|
119
153
|
}
|
|
120
154
|
|
|
155
|
+
/** The manifest upsert, shared by the single-shot and multipart paths so the
|
|
156
|
+
* two can never drift on what a stored row means. */
|
|
157
|
+
async function writeManifestRow(
|
|
158
|
+
env: PortalEnv,
|
|
159
|
+
row: { fileId: string; owner: string; filename: string; key: string; size: number; at: string; targetPath: string },
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
await env.DB.prepare(
|
|
162
|
+
`INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
|
|
163
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
+
ON CONFLICT (objectKey) DO UPDATE SET
|
|
165
|
+
fileId = excluded.fileId,
|
|
166
|
+
size = excluded.size,
|
|
167
|
+
uploadedAt = excluded.uploadedAt,
|
|
168
|
+
targetPath = excluded.targetPath,
|
|
169
|
+
ingested = 0,
|
|
170
|
+
devicePath = '',
|
|
171
|
+
deleted = 0`,
|
|
172
|
+
)
|
|
173
|
+
.bind(row.fileId, row.owner, row.filename, row.key, row.size, row.at, 0, row.targetPath)
|
|
174
|
+
.run()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Open a multipart session.
|
|
179
|
+
*
|
|
180
|
+
* The declared size is checked BEFORE the session is opened, so an over-cap
|
|
181
|
+
* declaration never leaves an orphan multipart upload accruing storage that
|
|
182
|
+
* nothing would ever clean up.
|
|
183
|
+
*/
|
|
184
|
+
export async function processMultipartCreate(
|
|
185
|
+
sessionId: string,
|
|
186
|
+
filename: string,
|
|
187
|
+
targetPath: string,
|
|
188
|
+
declaredSize: number,
|
|
189
|
+
env: PortalEnv,
|
|
190
|
+
log: Logger,
|
|
191
|
+
newId: () => string,
|
|
192
|
+
now: () => number,
|
|
193
|
+
): Promise<Handler> {
|
|
194
|
+
const startedMs = now()
|
|
195
|
+
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
196
|
+
if (!session) {
|
|
197
|
+
log('[data-portal] op=mp-create owner=none result=denied')
|
|
198
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
199
|
+
}
|
|
200
|
+
const owner = session.ownerId
|
|
201
|
+
const uploadId = newId()
|
|
202
|
+
|
|
203
|
+
if (!filename || filename.length > MAX_FILENAME) {
|
|
204
|
+
log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
205
|
+
return { status: 400, payload: { ok: false, error: 'filename required' } }
|
|
206
|
+
}
|
|
207
|
+
if (!Number.isFinite(declaredSize) || declaredSize < 0 || declaredSize > MAX_TOTAL_BYTES) {
|
|
208
|
+
log(
|
|
209
|
+
`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} ` +
|
|
210
|
+
`declared=${declaredSize} result=too-large limit=${MAX_TOTAL_BYTES}`,
|
|
211
|
+
)
|
|
212
|
+
return {
|
|
213
|
+
status: 413,
|
|
214
|
+
payload: { ok: false, error: `file is over the ${MAX_TOTAL_BYTES}-byte limit` },
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
|
|
219
|
+
if (!target.ok) {
|
|
220
|
+
log(
|
|
221
|
+
`[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
|
|
222
|
+
`targetPath=${q(targetPath)} result=denied-${target.reason}`,
|
|
223
|
+
)
|
|
224
|
+
return { status: 403, payload: { ok: false, error: 'target not allowed' } }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const key = uploadKey(owner, targetPath, filename)
|
|
228
|
+
if (!authorizeKey(owner, key)) {
|
|
229
|
+
log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
230
|
+
return { status: 400, payload: { ok: false, error: 'invalid filename' } }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const mp = await env.BUCKET.createMultipartUpload(key)
|
|
234
|
+
log(
|
|
235
|
+
`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} key=${q(key)} ` +
|
|
236
|
+
`declared=${declaredSize} parts=${Math.ceil(declaredSize / MAX_PART_BYTES)} result=ok`,
|
|
237
|
+
)
|
|
238
|
+
return { status: 200, payload: { ok: true, uploadId, key, r2UploadId: mp.uploadId } }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** One part. The key arrives from the client on every request, so it is
|
|
242
|
+
* re-authorized on every request — a session that may write `alice/` must not
|
|
243
|
+
* be able to append a part to `bob/`. */
|
|
244
|
+
export async function processMultipartPart(
|
|
245
|
+
sessionId: string,
|
|
246
|
+
key: string,
|
|
247
|
+
r2UploadId: string,
|
|
248
|
+
partNumber: number,
|
|
249
|
+
bytes: Uint8Array,
|
|
250
|
+
env: PortalEnv,
|
|
251
|
+
log: Logger,
|
|
252
|
+
): Promise<Handler> {
|
|
253
|
+
const session = await resolveSession(env.DB, sessionId, Date.now())
|
|
254
|
+
if (!session) {
|
|
255
|
+
log('[data-portal] op=r2-put owner=none result=denied')
|
|
256
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
257
|
+
}
|
|
258
|
+
if (!authorizeKey(session.ownerId, key)) {
|
|
259
|
+
log(`[data-portal] op=r2-put owner=${q(session.ownerId)} result=denied reason=not-owner`)
|
|
260
|
+
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
261
|
+
}
|
|
262
|
+
if (bytes.byteLength > MAX_PART_BYTES) {
|
|
263
|
+
log(
|
|
264
|
+
`[data-portal] op=r2-put owner=${q(session.ownerId)} part=${partNumber} ` +
|
|
265
|
+
`bytes=${bytes.byteLength} result=too-large limit=${MAX_PART_BYTES}`,
|
|
266
|
+
)
|
|
267
|
+
return { status: 413, payload: { ok: false, error: 'part too large' } }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const mp = env.BUCKET.resumeMultipartUpload(key, r2UploadId)
|
|
272
|
+
const part = await mp.uploadPart(partNumber, bytes)
|
|
273
|
+
log(
|
|
274
|
+
`[data-portal] op=r2-put owner=${q(session.ownerId)} key=${q(key)} ` +
|
|
275
|
+
`part=${partNumber} bytes=${bytes.byteLength} result=ok`,
|
|
276
|
+
)
|
|
277
|
+
return { status: 200, payload: { ok: true, partNumber, etag: part.etag } }
|
|
278
|
+
} catch (err) {
|
|
279
|
+
log(
|
|
280
|
+
`[data-portal] op=r2-put owner=${q(session.ownerId)} key=${q(key)} part=${partNumber} ` +
|
|
281
|
+
`result=failed err="${(err as Error).message}"`,
|
|
282
|
+
)
|
|
283
|
+
return { status: 502, payload: { ok: false, error: 'store failed' } }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface CompletedPart {
|
|
288
|
+
partNumber: number
|
|
289
|
+
etag: string
|
|
290
|
+
size?: number
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Assemble the parts and record the row.
|
|
295
|
+
*
|
|
296
|
+
* The declared size at create time is a claim; the summed part sizes are the
|
|
297
|
+
* measurement, so the cap is enforced again here. Over the cap the session is
|
|
298
|
+
* ABORTED rather than completed: assembling first and deleting after would
|
|
299
|
+
* store the oversized object, however briefly, and a failure between the two
|
|
300
|
+
* would leave it there permanently.
|
|
301
|
+
*/
|
|
302
|
+
export async function processMultipartComplete(
|
|
303
|
+
sessionId: string,
|
|
304
|
+
key: string,
|
|
305
|
+
r2UploadId: string,
|
|
306
|
+
targetPath: string,
|
|
307
|
+
parts: CompletedPart[],
|
|
308
|
+
env: PortalEnv,
|
|
309
|
+
log: Logger,
|
|
310
|
+
newId: () => string,
|
|
311
|
+
now: () => number,
|
|
312
|
+
): Promise<Handler> {
|
|
313
|
+
const startedMs = now()
|
|
314
|
+
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
315
|
+
if (!session) {
|
|
316
|
+
log('[data-portal] op=mp-complete owner=none result=denied')
|
|
317
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
318
|
+
}
|
|
319
|
+
const owner = session.ownerId
|
|
320
|
+
const uploadId = newId()
|
|
321
|
+
if (!authorizeKey(owner, key)) {
|
|
322
|
+
log(`[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} result=denied reason=not-owner`)
|
|
323
|
+
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const mp = env.BUCKET.resumeMultipartUpload(key, r2UploadId)
|
|
327
|
+
const declaredTotal = parts.reduce((n, p) => n + (p.size ?? 0), 0)
|
|
328
|
+
if (declaredTotal > MAX_TOTAL_BYTES) {
|
|
329
|
+
await mp.abort().catch(() => {})
|
|
330
|
+
log(
|
|
331
|
+
`[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} ` +
|
|
332
|
+
`bytes=${declaredTotal} result=too-large limit=${MAX_TOTAL_BYTES} aborted=true`,
|
|
333
|
+
)
|
|
334
|
+
return { status: 413, payload: { ok: false, error: 'file too large' } }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let size = declaredTotal
|
|
338
|
+
try {
|
|
339
|
+
const done = await mp.complete(parts.map((p) => ({ partNumber: p.partNumber, etag: p.etag })))
|
|
340
|
+
if (done && typeof done.size === 'number') size = done.size
|
|
341
|
+
log(`[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} parts=${parts.length} result=ok`)
|
|
342
|
+
} catch (err) {
|
|
343
|
+
log(
|
|
344
|
+
`[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} ` +
|
|
345
|
+
`result=failed err="${(err as Error).message}"`,
|
|
346
|
+
)
|
|
347
|
+
return { status: 502, payload: { ok: false, error: 'store failed' } }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const fileId = newId()
|
|
351
|
+
const filename = key.split('/').pop() ?? ''
|
|
352
|
+
try {
|
|
353
|
+
await writeManifestRow(env, {
|
|
354
|
+
fileId, owner, filename, key, size,
|
|
355
|
+
at: new Date(startedMs).toISOString(),
|
|
356
|
+
targetPath,
|
|
357
|
+
})
|
|
358
|
+
log(`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=ok`)
|
|
359
|
+
} catch (err) {
|
|
360
|
+
log(
|
|
361
|
+
`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} ` +
|
|
362
|
+
`result=failed err="${(err as Error).message}"`,
|
|
363
|
+
)
|
|
364
|
+
return { status: 502, payload: { ok: false, error: 'manifest failed' } }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const objectPresent = (await env.BUCKET.head(key)) !== null
|
|
368
|
+
const rowPresent =
|
|
369
|
+
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?').bind(key).first()) !==
|
|
370
|
+
null
|
|
371
|
+
log(
|
|
372
|
+
`[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} ` +
|
|
373
|
+
`objectPresent=${objectPresent} rowPresent=${rowPresent}`,
|
|
374
|
+
)
|
|
375
|
+
return { status: 200, payload: { ok: true, fileId, objectPresent, rowPresent } }
|
|
376
|
+
}
|
|
377
|
+
|
|
121
378
|
interface PagesContext {
|
|
122
379
|
request: Request
|
|
123
380
|
env: PortalEnv
|
|
124
381
|
}
|
|
125
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The multipart stages, routed off an `action` field.
|
|
385
|
+
*
|
|
386
|
+
* No action at all is today's single-shot upload, so the common case — a
|
|
387
|
+
* document well under one part — stays exactly one request. Only a file that
|
|
388
|
+
* needs slicing pays for three round trips.
|
|
389
|
+
*/
|
|
390
|
+
async function onMultipart(context: PagesContext, form: FormData, action: string): Promise<Response> {
|
|
391
|
+
const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
|
|
392
|
+
const log = (line: string) => console.log(line)
|
|
393
|
+
const newId = () => crypto.randomUUID()
|
|
394
|
+
|
|
395
|
+
if (action === 'create') {
|
|
396
|
+
const file = form.get('filename')
|
|
397
|
+
const { status, payload } = await processMultipartCreate(
|
|
398
|
+
sessionId,
|
|
399
|
+
typeof file === 'string' ? file : '',
|
|
400
|
+
String(form.get('targetPath') ?? ''),
|
|
401
|
+
Number(form.get('size') ?? NaN),
|
|
402
|
+
context.env,
|
|
403
|
+
log,
|
|
404
|
+
newId,
|
|
405
|
+
() => Date.now(),
|
|
406
|
+
)
|
|
407
|
+
return Response.json(payload, { status })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (action === 'part') {
|
|
411
|
+
const part = form.get('part')
|
|
412
|
+
if (!(part instanceof File)) {
|
|
413
|
+
return Response.json({ ok: false, error: 'part required' }, { status: 400 })
|
|
414
|
+
}
|
|
415
|
+
const { status, payload } = await processMultipartPart(
|
|
416
|
+
sessionId,
|
|
417
|
+
String(form.get('key') ?? ''),
|
|
418
|
+
String(form.get('r2UploadId') ?? ''),
|
|
419
|
+
Number(form.get('partNumber') ?? NaN),
|
|
420
|
+
new Uint8Array(await part.arrayBuffer()),
|
|
421
|
+
context.env,
|
|
422
|
+
log,
|
|
423
|
+
)
|
|
424
|
+
return Response.json(payload, { status })
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let parts: CompletedPart[]
|
|
428
|
+
try {
|
|
429
|
+
parts = JSON.parse(String(form.get('parts') ?? '[]')) as CompletedPart[]
|
|
430
|
+
if (!Array.isArray(parts)) throw new Error('parts must be an array')
|
|
431
|
+
} catch {
|
|
432
|
+
return Response.json({ ok: false, error: 'invalid parts' }, { status: 400 })
|
|
433
|
+
}
|
|
434
|
+
const { status, payload } = await processMultipartComplete(
|
|
435
|
+
sessionId,
|
|
436
|
+
String(form.get('key') ?? ''),
|
|
437
|
+
String(form.get('r2UploadId') ?? ''),
|
|
438
|
+
String(form.get('targetPath') ?? ''),
|
|
439
|
+
parts,
|
|
440
|
+
context.env,
|
|
441
|
+
log,
|
|
442
|
+
newId,
|
|
443
|
+
() => Date.now(),
|
|
444
|
+
)
|
|
445
|
+
return Response.json(payload, { status })
|
|
446
|
+
}
|
|
447
|
+
|
|
126
448
|
export async function onRequestPost(context: PagesContext): Promise<Response> {
|
|
127
449
|
const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
|
|
128
450
|
let filename = ''
|
|
451
|
+
let targetPath = ''
|
|
129
452
|
let bytes: Uint8Array
|
|
453
|
+
|
|
454
|
+
// Parsing the form is the only thing this catch may cover. A multipart stage
|
|
455
|
+
// dispatched from inside it would report its own failures as "invalid form",
|
|
456
|
+
// which is the silent-failure shape: one message standing in for every cause.
|
|
457
|
+
let form: FormData
|
|
458
|
+
try {
|
|
459
|
+
form = await context.request.formData()
|
|
460
|
+
} catch {
|
|
461
|
+
return Response.json({ ok: false, error: 'invalid form' }, { status: 400 })
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const action = String(form.get('action') ?? '')
|
|
465
|
+
if (action === 'create' || action === 'part' || action === 'complete') {
|
|
466
|
+
return onMultipart(context, form, action)
|
|
467
|
+
}
|
|
468
|
+
|
|
130
469
|
try {
|
|
131
|
-
const form = await context.request.formData()
|
|
132
470
|
const file = form.get('file')
|
|
133
471
|
if (!(file instanceof File)) {
|
|
134
472
|
return Response.json({ ok: false, error: 'file required' }, { status: 400 })
|
|
135
473
|
}
|
|
136
474
|
filename = file.name
|
|
475
|
+
// Absent means the default uploads area, which is always legal. An absent
|
|
476
|
+
// field and an empty one are the same request.
|
|
477
|
+
targetPath = String(form.get('targetPath') ?? '')
|
|
137
478
|
bytes = new Uint8Array(await file.arrayBuffer())
|
|
138
479
|
} catch {
|
|
139
|
-
return Response.json({ ok: false, error: 'invalid
|
|
480
|
+
return Response.json({ ok: false, error: 'invalid file' }, { status: 400 })
|
|
140
481
|
}
|
|
141
482
|
const { status, payload } = await processUpload(
|
|
142
483
|
sessionId,
|
|
143
484
|
filename,
|
|
485
|
+
targetPath,
|
|
144
486
|
bytes,
|
|
145
487
|
context.env,
|
|
146
488
|
(line) => console.log(line),
|
|
@@ -72,10 +72,18 @@
|
|
|
72
72
|
<div class="dp-modal-panel" role="dialog" aria-modal="true" aria-label="Upload a file">
|
|
73
73
|
<button class="dp-modal-close" id="dp-modal-close" type="button" aria-label="Close">×</button>
|
|
74
74
|
<h2 class="dp-subtitle">Upload a file</h2>
|
|
75
|
+
<!-- `multiple`: a person dragging three invoices in expects three
|
|
76
|
+
uploads, not the first one silently. -->
|
|
75
77
|
<form id="dp-upload" class="dp-upload">
|
|
76
|
-
<input class="dp-file" id="dp-file" type="file" required />
|
|
78
|
+
<input class="dp-file" id="dp-file" type="file" multiple required />
|
|
77
79
|
<button class="dp-btn" type="submit">Upload</button>
|
|
78
80
|
</form>
|
|
81
|
+
<!-- Names the folder the upload will land in, so the destination is
|
|
82
|
+
never a guess. Filled by portal.js from the open folder. -->
|
|
83
|
+
<p class="dp-upload-target" id="dp-upload-target"></p>
|
|
84
|
+
<div class="dp-progress" id="dp-progress" hidden>
|
|
85
|
+
<div class="dp-progress-bar" id="dp-progress-bar"></div>
|
|
86
|
+
</div>
|
|
79
87
|
</div>
|
|
80
88
|
</div>
|
|
81
89
|
</section>
|
|
@@ -308,6 +308,51 @@ body {
|
|
|
308
308
|
outline-offset: 3px;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* The open listing while a file is dragged over it: this drop targets the
|
|
312
|
+
folder currently open. Same dashed treatment as the upload area, so one
|
|
313
|
+
visual language means "release here". */
|
|
314
|
+
.dp-files.dp-dropping {
|
|
315
|
+
outline: 2px dashed var(--dp-accent);
|
|
316
|
+
outline-offset: 3px;
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* One folder row under the pointer. Filled rather than outlined, because the
|
|
321
|
+
question a client is asking mid-drag is "which of these rows will take it",
|
|
322
|
+
and an outline on a dense list is easy to misread by one row. */
|
|
323
|
+
.dp-file-row.dp-drop-target {
|
|
324
|
+
background: color-mix(in srgb, var(--dp-accent) 12%, transparent);
|
|
325
|
+
outline: 2px solid var(--dp-accent);
|
|
326
|
+
outline-offset: -2px;
|
|
327
|
+
border-radius: 6px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Which folder the modal will upload into. Stated rather than implied: an
|
|
331
|
+
upload whose destination the client has to infer is the defect the
|
|
332
|
+
folder-targeted exchange exists to remove. */
|
|
333
|
+
.dp-upload-target {
|
|
334
|
+
margin: 0.5rem 0 0;
|
|
335
|
+
font-size: 0.85rem;
|
|
336
|
+
color: var(--dp-muted);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Per-file upload progress. Hidden between uploads via the `hidden` attribute,
|
|
340
|
+
which [hidden]{display:none!important} keeps authoritative. */
|
|
341
|
+
.dp-progress {
|
|
342
|
+
margin-top: 0.6rem;
|
|
343
|
+
height: 4px;
|
|
344
|
+
background: var(--dp-rule);
|
|
345
|
+
border-radius: 2px;
|
|
346
|
+
overflow: hidden;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.dp-progress-bar {
|
|
350
|
+
height: 100%;
|
|
351
|
+
width: 0;
|
|
352
|
+
background: var(--dp-accent);
|
|
353
|
+
transition: width 120ms linear;
|
|
354
|
+
}
|
|
355
|
+
|
|
311
356
|
/* Pinned nav row. position:sticky keeps it in flow, so it reserves its own
|
|
312
357
|
height and never overlaps the first file row (which position:fixed would). An
|
|
313
358
|
opaque paper background hides the rows that scroll under it. Reuses existing
|