@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
|
@@ -143,6 +143,16 @@ describe('device binding', () => {
|
|
|
143
143
|
expect(text).toMatch(/UNIQUE \(accountId, relPath\)/)
|
|
144
144
|
expect(text).toMatch(/accountId\s+TEXT NOT NULL DEFAULT ''/)
|
|
145
145
|
})
|
|
146
|
+
|
|
147
|
+
// IF NOT EXISTS is a no-op on an existing table, so a portal deployed before
|
|
148
|
+
// this column will NOT gain it from a re-apply. The push names isDir in its
|
|
149
|
+
// INSERT, so such a table fails loudly rather than silently publishing
|
|
150
|
+
// nothing — the ALTER has to be findable at the point that happens.
|
|
151
|
+
it('declares isDir and carries the ALTER for a table that predates it', () => {
|
|
152
|
+
const text = schema()
|
|
153
|
+
expect(text).toMatch(/isDir\s+INTEGER NOT NULL DEFAULT 0/)
|
|
154
|
+
expect(text).toContain('ALTER TABLE directory ADD COLUMN isDir INTEGER NOT NULL DEFAULT 0')
|
|
155
|
+
})
|
|
146
156
|
})
|
|
147
157
|
|
|
148
158
|
const portalJs = () => readFileSync(join(TEMPLATE, 'portal.js'), 'utf8')
|
|
@@ -170,3 +180,172 @@ describe('login error surfacing and passcode reveal (Task 1872)', () => {
|
|
|
170
180
|
expect(portalJs()).toMatch(/dp-reveal/)
|
|
171
181
|
})
|
|
172
182
|
})
|
|
183
|
+
|
|
184
|
+
// Task 1900. The device pushes a new tree hourly, so a client who stays signed
|
|
185
|
+
// in reads an index frozen at sign-in. These assertions pin the control that
|
|
186
|
+
// re-reads it: the button in the pinned header, and the click that drives it.
|
|
187
|
+
describe('refresh control', () => {
|
|
188
|
+
it('puts a refresh button inside the pinned header', () => {
|
|
189
|
+
const header = indexHtml().match(/<header class="dp-header">[\s\S]*?<\/header>/)
|
|
190
|
+
expect(header).not.toBeNull()
|
|
191
|
+
expect(header![0]).toMatch(/id="dp-refresh"/)
|
|
192
|
+
expect(header![0]).toMatch(/class="dp-iconbtn"/)
|
|
193
|
+
expect(header![0]).toMatch(/aria-label="Refresh"/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('sits between the crumbs and the upload control', () => {
|
|
197
|
+
const text = indexHtml()
|
|
198
|
+
expect(text.indexOf('id="dp-crumbs"')).toBeLessThan(text.indexOf('id="dp-refresh"'))
|
|
199
|
+
expect(text.indexOf('id="dp-refresh"')).toBeLessThan(text.indexOf('id="dp-upload-open"'))
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('binds a click on it', () => {
|
|
203
|
+
expect(portalJs()).toMatch(/refreshEl\.addEventListener\('click'/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('drives refresh() rather than reloading the page', () => {
|
|
207
|
+
expect(portalJs()).not.toMatch(/location\.reload/)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// The status line has to survive a failed re-read. refresh() catches its own
|
|
211
|
+
// failure, so the promise resolves either way; only a reported outcome tells
|
|
212
|
+
// the handler whether clearing the status would erase an error.
|
|
213
|
+
it('has refresh() report its outcome on all three exits', () => {
|
|
214
|
+
const body = portalJs().match(/function refresh\(\)[\s\S]*?\n }\n/)
|
|
215
|
+
expect(body).not.toBeNull()
|
|
216
|
+
expect(body![0]).toMatch(/return true/)
|
|
217
|
+
expect((body![0].match(/return false/g) || []).length).toBe(2)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Task 1900 asserted "never polls", on the reasoning that a poll against an
|
|
221
|
+
// hourly tree is waste. Task 1910 moved the push to a 60s loop, so that
|
|
222
|
+
// premise no longer holds: the poll is now the thing that makes a device-side
|
|
223
|
+
// change appear without the client clicking anything.
|
|
224
|
+
//
|
|
225
|
+
// What still must hold is that the poll cannot run away. It is gated on tab
|
|
226
|
+
// visibility and on a re-entry guard, so a background tab costs nothing and a
|
|
227
|
+
// slow response cannot stack reads on a portal running on a free plan.
|
|
228
|
+
it('polls once a minute, gated on visibility and not re-entrant', () => {
|
|
229
|
+
const text = portalJs()
|
|
230
|
+
const timer = text.match(/setInterval\(function \(\)[\s\S]*?\}, 60000\)/)
|
|
231
|
+
expect(timer).not.toBeNull()
|
|
232
|
+
expect(timer![0]).toMatch(/document\.hidden/)
|
|
233
|
+
expect(timer![0]).toMatch(/polling/)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// refresh() now writes its own failure text, which would bury the outcome of
|
|
238
|
+
// the delete or upload the client just asked for. Both callers compose instead,
|
|
239
|
+
// so a stale list is reported without losing what happened to the file.
|
|
240
|
+
describe('an action that outlives its re-list', () => {
|
|
241
|
+
it('keeps the delete and upload outcome when the re-list fails', () => {
|
|
242
|
+
const text = portalJs()
|
|
243
|
+
expect((text.match(/The list could not be reloaded\./g) || []).length).toBe(2)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('has both callers read the boolean refresh() returns', () => {
|
|
247
|
+
const text = portalJs()
|
|
248
|
+
const removeBody = text.match(/function remove\(key\)[\s\S]*?\n }\n/)
|
|
249
|
+
const uploadBody = text.match(/function uploadFile\(file, target\)[\s\S]*?\n }\n/)
|
|
250
|
+
expect(removeBody).not.toBeNull()
|
|
251
|
+
expect(uploadBody).not.toBeNull()
|
|
252
|
+
expect(removeBody![0]).toMatch(/refresh\(\)\.then\(function \(ok\)/)
|
|
253
|
+
expect(uploadBody![0]).toMatch(/refresh\(\)\.then\(function \(ok\)/)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// The two-way client (Task 1910). These are text assertions over the shipped
|
|
258
|
+
// template, which is the only harness this dependency-free page has — there is
|
|
259
|
+
// no bundler and no DOM test runner for it. They pin the behaviours that make
|
|
260
|
+
// the exchange two-way; the live drag-and-drop itself is an on-device check.
|
|
261
|
+
describe('the client can send a file to a folder', () => {
|
|
262
|
+
it('makes every folder row a drop target', () => {
|
|
263
|
+
// The interaction the whole task exists for: drop onto the folder, not
|
|
264
|
+
// into a modal that decides for you.
|
|
265
|
+
const text = portalJs()
|
|
266
|
+
expect(text).toMatch(/li\.addEventListener\('drop'/)
|
|
267
|
+
expect(text).toMatch(/dp-drop-target/)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('sends the row own path as the target, not the folder merely open', () => {
|
|
271
|
+
const text = portalJs()
|
|
272
|
+
expect(text).toMatch(/uploadMany\(dt\.files, e\.relPath\)/)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('does not upload a row drop twice', () => {
|
|
276
|
+
// A drop on a folder row bubbles to the listing, whose handler would
|
|
277
|
+
// otherwise upload the same file again into whichever folder is open.
|
|
278
|
+
const text = portalJs()
|
|
279
|
+
expect(text).toMatch(/ev\.dpHandled = true/)
|
|
280
|
+
expect(text).toMatch(/if \(e\.dpHandled\) return/)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('takes every dropped file, not the first', () => {
|
|
284
|
+
const text = portalJs()
|
|
285
|
+
expect(text).not.toMatch(/dt\.files\[0\]/)
|
|
286
|
+
expect(text).toMatch(/function uploadMany\(files, target\)/)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('accepts a multiple selection in the picker', () => {
|
|
290
|
+
expect(indexHtml()).toMatch(/id="dp-file"[^>]*multiple/)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('names the destination folder in the modal', () => {
|
|
294
|
+
// An upload whose folder the client has to infer is the defect this task
|
|
295
|
+
// removes; stating it is part of the fix, not decoration.
|
|
296
|
+
expect(portalJs()).toMatch(/Uploading into /)
|
|
297
|
+
expect(indexHtml()).toMatch(/id="dp-upload-target"/)
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('the client can take a file out', () => {
|
|
302
|
+
it('offers drag-out on the client own uploads, not only device files', () => {
|
|
303
|
+
// Before this there were two stores and only one of them could be dragged
|
|
304
|
+
// out of. DownloadURL is what makes the OS write real bytes.
|
|
305
|
+
const text = portalJs()
|
|
306
|
+
expect((text.match(/setData\(\s*'DownloadURL'/g) || []).length).toBe(2)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
describe('the client can send a large file', () => {
|
|
311
|
+
it('slices at the same 25 MiB bound the server gates on', () => {
|
|
312
|
+
const text = portalJs()
|
|
313
|
+
expect(text).toMatch(/PART_BYTES = 25 \* 1024 \* 1024/)
|
|
314
|
+
expect(text).toMatch(/MAX_TOTAL_BYTES = 1024 \* 1024 \* 1024/)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('checks the size before sending rather than after', () => {
|
|
318
|
+
// The browser should not spend minutes sending a gigabyte only to be told
|
|
319
|
+
// at the end that it was too large.
|
|
320
|
+
expect(portalJs()).toMatch(/file\.size > MAX_TOTAL_BYTES/)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('runs the three multipart stages', () => {
|
|
324
|
+
const text = portalJs()
|
|
325
|
+
for (const action of ['create', 'part', 'complete']) {
|
|
326
|
+
expect(text).toContain(`'action', '${action}'`)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('shows progress across the parts', () => {
|
|
331
|
+
expect(portalJs()).toMatch(/showProgress\(i \/ total\)/)
|
|
332
|
+
expect(indexHtml()).toMatch(/id="dp-progress-bar"/)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('a failed upload says which failure it was', () => {
|
|
337
|
+
it('maps each status to its own message', () => {
|
|
338
|
+
// Every failure used to read 'Upload failed.', which tells a client nothing
|
|
339
|
+
// about whether to retry, sign in again, or pick a smaller file.
|
|
340
|
+
const text = portalJs()
|
|
341
|
+
const fn = text.match(/function uploadFailureMessage\(status\)[\s\S]*?\n }\n/)
|
|
342
|
+
expect(fn).not.toBeNull()
|
|
343
|
+
for (const status of ['401', '403', '413', '400']) {
|
|
344
|
+
expect(fn![0]).toContain(status)
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('distinguishes a network failure from a server refusal', () => {
|
|
349
|
+
expect(portalJs()).toMatch(/Could not reach the server/)
|
|
350
|
+
})
|
|
351
|
+
})
|
|
@@ -78,7 +78,7 @@ describe('processUpload', () => {
|
|
|
78
78
|
const res = await processUpload(
|
|
79
79
|
'sess-a',
|
|
80
80
|
'invoice.pdf',
|
|
81
|
-
new Uint8Array([1, 2, 3]),
|
|
81
|
+
'', new Uint8Array([1, 2, 3]),
|
|
82
82
|
env,
|
|
83
83
|
(l) => lines.push(l),
|
|
84
84
|
() => 'up-1',
|
|
@@ -94,7 +94,7 @@ describe('processUpload', () => {
|
|
|
94
94
|
await processUpload(
|
|
95
95
|
'sess-a',
|
|
96
96
|
'invoice.pdf',
|
|
97
|
-
new Uint8Array([1]),
|
|
97
|
+
'', new Uint8Array([1]),
|
|
98
98
|
env,
|
|
99
99
|
() => {},
|
|
100
100
|
() => 'up-1',
|
|
@@ -110,7 +110,7 @@ describe('processUpload', () => {
|
|
|
110
110
|
await processUpload(
|
|
111
111
|
'sess-a',
|
|
112
112
|
'invoice.pdf',
|
|
113
|
-
new Uint8Array([1]),
|
|
113
|
+
'', new Uint8Array([1]),
|
|
114
114
|
env,
|
|
115
115
|
(l) => lines.push(l),
|
|
116
116
|
() => 'up-1',
|
|
@@ -128,7 +128,7 @@ describe('processUpload', () => {
|
|
|
128
128
|
const res = await processUpload(
|
|
129
129
|
'',
|
|
130
130
|
'invoice.pdf',
|
|
131
|
-
new Uint8Array([1]),
|
|
131
|
+
'', new Uint8Array([1]),
|
|
132
132
|
env,
|
|
133
133
|
() => {},
|
|
134
134
|
() => 'up-1',
|
|
@@ -144,7 +144,7 @@ describe('processUpload', () => {
|
|
|
144
144
|
const res = await processUpload(
|
|
145
145
|
'sess-a',
|
|
146
146
|
'../bob/evil.pdf',
|
|
147
|
-
new Uint8Array([1]),
|
|
147
|
+
'', new Uint8Array([1]),
|
|
148
148
|
env,
|
|
149
149
|
() => {},
|
|
150
150
|
() => 'up-1',
|
|
@@ -162,7 +162,7 @@ describe('processUpload', () => {
|
|
|
162
162
|
await processUpload(
|
|
163
163
|
'sess-a',
|
|
164
164
|
'',
|
|
165
|
-
new Uint8Array([1]),
|
|
165
|
+
'', new Uint8Array([1]),
|
|
166
166
|
env,
|
|
167
167
|
() => {},
|
|
168
168
|
() => 'up-1',
|
|
@@ -179,6 +179,7 @@ describe('processUpload', () => {
|
|
|
179
179
|
const res = await processUpload(
|
|
180
180
|
'sess-a',
|
|
181
181
|
'huge.bin',
|
|
182
|
+
'',
|
|
182
183
|
tooBig,
|
|
183
184
|
env,
|
|
184
185
|
(l) => lines.push(l),
|
|
@@ -198,7 +199,7 @@ describe('processUpload', () => {
|
|
|
198
199
|
await processUpload(
|
|
199
200
|
'sess-a',
|
|
200
201
|
'invoice.pdf',
|
|
201
|
-
new Uint8Array([1]),
|
|
202
|
+
'', new Uint8Array([1]),
|
|
202
203
|
env,
|
|
203
204
|
(l) => lines.push(l),
|
|
204
205
|
() => 'up-1',
|
|
@@ -217,11 +218,11 @@ describe('processUpload', () => {
|
|
|
217
218
|
describe('re-uploading the same filename', () => {
|
|
218
219
|
it('replaces the object, keeps exactly one row, and succeeds', async () => {
|
|
219
220
|
const { env, manifestRows, objects } = makeEnv()
|
|
220
|
-
await processUpload('sess-a', 'invoice.pdf', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
221
|
+
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
221
222
|
const second = await processUpload(
|
|
222
223
|
'sess-a',
|
|
223
224
|
'invoice.pdf',
|
|
224
|
-
new Uint8Array([9, 9, 9]),
|
|
225
|
+
'', new Uint8Array([9, 9, 9]),
|
|
225
226
|
env,
|
|
226
227
|
() => {},
|
|
227
228
|
() => 'up-2',
|
|
@@ -234,8 +235,8 @@ describe('processUpload', () => {
|
|
|
234
235
|
|
|
235
236
|
it('updates the row to describe the new object, not the old one', async () => {
|
|
236
237
|
const { env, manifestRows } = makeEnv()
|
|
237
|
-
await processUpload('sess-a', 'invoice.pdf', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
238
|
-
await processUpload('sess-a', 'invoice.pdf', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
238
|
+
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
239
|
+
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
239
240
|
// Column order: fileId, ownerId, filename, objectKey, size, uploadedAt, ingested
|
|
240
241
|
expect(manifestRows[0][4]).toBe(3)
|
|
241
242
|
expect(manifestRows[0][5]).toBe(new Date(2_000).toISOString())
|
|
@@ -246,9 +247,9 @@ describe('processUpload', () => {
|
|
|
246
247
|
// ingested=1, a replacement that left the flag set would never be
|
|
247
248
|
// ingested, and nothing would ever say so.
|
|
248
249
|
const { env, manifestRows } = makeEnv()
|
|
249
|
-
await processUpload('sess-a', 'invoice.pdf', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
250
|
+
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
250
251
|
manifestRows[0][6] = 1 // the device-side sweep marks it ingested
|
|
251
|
-
await processUpload('sess-a', 'invoice.pdf', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
252
|
+
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
252
253
|
expect(manifestRows[0][6]).toBe(0)
|
|
253
254
|
})
|
|
254
255
|
})
|
|
@@ -13,9 +13,11 @@ references with `plugin-read` before issuing their commands.
|
|
|
13
13
|
## What this skill produces, and what it does not
|
|
14
14
|
|
|
15
15
|
It produces a Pages project on a custom domain, an R2 bucket holding one prefix per enrolled person,
|
|
16
|
-
and a D1 database holding the manifest, the enrolment records, and the sessions. It does not
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
and a D1 database holding the manifest, the enrolment records, and the sessions. It does not carry a
|
|
17
|
+
dropped file onto the device: the `portal-uploads-pull` loop in the admin server does that, every
|
|
18
|
+
minute, writing each un-ingested manifest row's object to `uploads/<ownerName>/` and setting
|
|
19
|
+
`ingested = 1` only once the bytes are on disk at the right size. This skill delivers the manifest
|
|
20
|
+
that loop consumes. Nothing here or there ingests a pulled file into the graph.
|
|
19
21
|
|
|
20
22
|
## Why the portal is on Pages and not the install
|
|
21
23
|
|
|
@@ -110,6 +112,17 @@ with the same `bin/cf-store-name.sh` scheme as the bucket, and apply
|
|
|
110
112
|
`platform/plugins/cloudflare/skills/data-portal/schema.sql` to the remote database from there. It is
|
|
111
113
|
`IF NOT EXISTS` throughout, so re-applying is safe.
|
|
112
114
|
|
|
115
|
+
Safe, but not a migration: `IF NOT EXISTS` adds no column to a table that already exists. A portal
|
|
116
|
+
whose `directory` table predates the `isDir` column keeps failing every index push, because the push
|
|
117
|
+
names the column in its `INSERT`. Add it once by hand against that database:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx wrangler d1 execute <portalDbName> --remote \
|
|
121
|
+
--command "ALTER TABLE directory ADD COLUMN isDir INTEGER NOT NULL DEFAULT 0"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
No backfill follows it. The push replaces every row for the account each cycle.
|
|
125
|
+
|
|
113
126
|
The copied `portal.css` carries the template's default palette, which is nobody's brand. Apply the
|
|
114
127
|
install's before handing the tree to the deploy:
|
|
115
128
|
|
|
@@ -164,7 +177,7 @@ anyway. That check is what contains a stale index or a compromised project, and
|
|
|
164
177
|
|
|
165
178
|
## Publish the folder index
|
|
166
179
|
|
|
167
|
-
The device publishes file metadata — names, sizes, times, never bytes:
|
|
180
|
+
The device publishes file and folder metadata — names, sizes, times, never bytes:
|
|
168
181
|
|
|
169
182
|
```bash
|
|
170
183
|
node platform/plugins/cloudflare/bin/portal-index-push.mjs \
|
|
@@ -178,6 +191,13 @@ brand with a different vertical publishes different folders with no change to th
|
|
|
178
191
|
data (`documents/`, `contacts/`, `projects/`), the client's own `uploads/`, plugin workspaces and
|
|
179
192
|
everything platform-internal are in neither source and are never published.
|
|
180
193
|
|
|
194
|
+
Every walked folder gets a row of its own, not only the files inside it. An exposed folder holding
|
|
195
|
+
nothing at any depth is therefore a real entry: it renders in the tree and opens to an empty listing,
|
|
196
|
+
rather than reading as absent to the person who has to put the first file into it. A folder row
|
|
197
|
+
carries no bytes, so the download path refuses it and logs `result=is-directory`. A folder named in
|
|
198
|
+
the schema but absent from the device's disk still publishes nothing, so the tree never offers a
|
|
199
|
+
folder the device does not have.
|
|
200
|
+
|
|
181
201
|
An account with no `SCHEMA.md` publishes nothing at all. That is deliberate and it is the opposite of
|
|
182
202
|
what the write-path guard does — the guard fails open so an unseeded account is not write-blocked,
|
|
183
203
|
while a surface facing a client must fail closed.
|
|
@@ -190,15 +210,25 @@ dot-prefixed. These extras are added on top of the schema-derived set and, unlik
|
|
|
190
210
|
are not intersected with the `allowed-top-level` block. An empty or absent list behaves exactly as
|
|
191
211
|
before.
|
|
192
212
|
|
|
193
|
-
The
|
|
213
|
+
The admin server runs this every sixty seconds for every account carrying a `data-portal.json`
|
|
214
|
+
. It used to be an hourly spawn from the scheduling heartbeat, which had never been
|
|
215
|
+
observed running on a device and left the client's tree up to an hour stale. Run it once by hand
|
|
194
216
|
after assembly so the client has something to see on their first sign-in, and check the output names
|
|
195
217
|
the folders you expect:
|
|
196
218
|
|
|
197
219
|
```
|
|
198
220
|
[portal-index] op=resolve account=<id> schemaPresent=true allowed=22 domainBuckets=quotes extraFolders=1 exposed=inbound-invoices,output,quotes
|
|
221
|
+
[portal-index] op=stage account=<id> generation=7 inserted=41
|
|
222
|
+
[portal-index] op=flip account=<id> generation=7
|
|
199
223
|
[portal-index] op=verify account=<id> rowsAfter=41 expected=41
|
|
200
224
|
```
|
|
201
225
|
|
|
226
|
+
The publish stages every row at the next generation, moves a pointer in one statement, then sweeps
|
|
227
|
+
the superseded rows. That flip is the atomic point: a client listing mid-publish reads the old tree
|
|
228
|
+
or the new one and never a half-built one. Before this the replace was delete-then-insert, so the
|
|
229
|
+
tree was empty for the length of the insert run, which was tolerable hourly and is not at sixty
|
|
230
|
+
seconds.
|
|
231
|
+
|
|
202
232
|
`exposed=none` on an account that has deliverables means the schema was missing or its ontology
|
|
203
233
|
region was absent — not that the walk found nothing. `extraFolders=<n>` counts the folders the
|
|
204
234
|
operator requested in `exposeFolders`; a non-zero `extraFolders` with none of those names in
|
|
@@ -208,9 +238,26 @@ operator requested in `exposeFolders`; a non-zero `extraFolders` with none of th
|
|
|
208
238
|
|
|
209
239
|
Signed in, the panel has a pinned header that stays put while the file lists
|
|
210
240
|
scroll under it. The header carries the folder breadcrumbs, an up control that
|
|
211
|
-
moves to the parent folder (disabled at the root),
|
|
212
|
-
render on one line: the stem truncates with an ellipsis
|
|
213
|
-
pinned, so `.pdf` and `.html` on two same-named orders
|
|
241
|
+
moves to the parent folder (disabled at the root), a refresh control, and an
|
|
242
|
+
upload icon. Filenames render on one line: the stem truncates with an ellipsis
|
|
243
|
+
and the extension stays pinned, so `.pdf` and `.html` on two same-named orders
|
|
244
|
+
are still told apart.
|
|
245
|
+
|
|
246
|
+
Refresh re-reads the index in the folder that is already open. The page also
|
|
247
|
+
re-reads itself every sixty seconds while its tab is visible, which is what
|
|
248
|
+
makes a file the device published appear without the client clicking anything.
|
|
249
|
+
That poll was deliberately absent while the push was hourly, when it would have
|
|
250
|
+
been waste; it arrived with the sixty-second push. It is skipped for a hidden
|
|
251
|
+
tab and while a read is already in flight, so a background tab costs nothing and
|
|
252
|
+
a slow response cannot stack requests. The refresh button is inert while its own
|
|
253
|
+
request is in flight, and the status line clears on success or keeps the failure
|
|
254
|
+
message on screen.
|
|
255
|
+
|
|
256
|
+
Delete and upload both re-read the list when they finish. If that re-read
|
|
257
|
+
fails, the status keeps the outcome and adds to it, so a client reads
|
|
258
|
+
`Deleted. The list could not be reloaded.` rather than a bare load error that
|
|
259
|
+
would leave them unsure whether the file went. The rows on screen are then
|
|
260
|
+
known to be stale, and refresh is how they clear that.
|
|
214
261
|
|
|
215
262
|
A shared (device-held) file downloads three ways, all through the same signed
|
|
216
263
|
`/api/download?path=` route: a left click, a right-click that opens a one-item
|
|
@@ -220,11 +267,38 @@ saves the file rather than a JSON body. The three download methods share the one
|
|
|
220
267
|
`op=fetch-through` log line, so a broken method shows as a missing line under the
|
|
221
268
|
same signal.
|
|
222
269
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
270
|
+
A client's own uploaded file drags out the same way, so both lists behave alike
|
|
271
|
+
rather than only the device-held one supporting it.
|
|
272
|
+
|
|
273
|
+
**Uploads go to a folder the client chooses.** Dropping files onto a
|
|
274
|
+
folder row sends them into that folder; dropping onto the open listing sends
|
|
275
|
+
them into the folder currently open; the modal states which folder it will use.
|
|
276
|
+
An upload with no folder chosen lands in the client's own uploads area, which
|
|
277
|
+
stays unexposed to the folder tree. The client still sees and downloads it from
|
|
278
|
+
their own list, through a signed link scoped to them.
|
|
279
|
+
|
|
280
|
+
A legal target is one the client can already see: it must pass their folder
|
|
281
|
+
grant and carry a row in the published index. The device re-derives that set
|
|
282
|
+
itself and refuses anything outside it, so a stale index or a compromised Pages
|
|
283
|
+
project cannot route a file into a folder the device does not expose. A refused
|
|
284
|
+
target leaves the file pending and logs the exact reason; it is never quietly
|
|
285
|
+
rerouted somewhere the client did not choose.
|
|
286
|
+
|
|
287
|
+
Several files may be dropped or picked at once and upload one after another.
|
|
288
|
+
Anything over 25 MiB is sliced into parts against a multipart session, up to
|
|
289
|
+
1 GB, with a progress bar; a file over the limit is refused before any bytes are
|
|
290
|
+
sent. Failures now read differently by cause: an expired session, a folder that
|
|
291
|
+
is not allowed, a file that is too large, and an unreachable server are four
|
|
292
|
+
messages, not one.
|
|
293
|
+
|
|
294
|
+
Once the device has collected an upload it deletes the portal's copy, so the
|
|
295
|
+
file lives in one place rather than two. A targeted file then appears in its
|
|
296
|
+
folder rather than in the uploads list; an untargeted one stays in the uploads
|
|
297
|
+
list and downloads through the device. Deleting a file the device already
|
|
298
|
+
collected removes it from the device too, within a minute; the client is told it
|
|
299
|
+
is pending rather than gone, because for that minute it is.
|
|
300
|
+
|
|
301
|
+
The modal dismisses on the close button, a click on the backdrop, or Escape.
|
|
228
302
|
|
|
229
303
|
## Deploy through site-deploy
|
|
230
304
|
|
|
@@ -58,9 +58,40 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
58
58
|
|
|
59
59
|
CREATE INDEX IF NOT EXISTS sessions_owner ON sessions (ownerId);
|
|
60
60
|
|
|
61
|
-
-- One row per stored object
|
|
62
|
-
--
|
|
63
|
-
--
|
|
61
|
+
-- One row per stored object, carrying a three-state lifecycle (Task 1910).
|
|
62
|
+
--
|
|
63
|
+
-- in transit ingested=0, devicePath='' bytes in R2
|
|
64
|
+
-- moved ingested=1, devicePath set bytes on the device, R2 object gone
|
|
65
|
+
-- withdrawn deleted=1 device file pending removal
|
|
66
|
+
--
|
|
67
|
+
-- `ingested` is the claim flag the admin server's `portal-uploads-pull` loop
|
|
68
|
+
-- reads and sets; a row stays 0 until its object is on the device at the size
|
|
69
|
+
-- recorded here, so the backlog survives the device being offline and a
|
|
70
|
+
-- half-finished transfer never reads as delivered.
|
|
71
|
+
--
|
|
72
|
+
-- `targetPath` is the exposed folder the client chose at upload, '' when they
|
|
73
|
+
-- chose none. It is part of `objectKey` (ownerId/targetPath/filename), because
|
|
74
|
+
-- a flat key collides the same filename dropped into two folders: the upsert
|
|
75
|
+
-- below would overwrite the first object, reset ingested, and the first file
|
|
76
|
+
-- would never land on the device at all.
|
|
77
|
+
--
|
|
78
|
+
-- `devicePath` is written only after a verified device write, so its presence
|
|
79
|
+
-- IS the proof the move happened. A boolean would say a move occurred without
|
|
80
|
+
-- saying where, and the audit's unpublishedRouted check needs the path to
|
|
81
|
+
-- compare against the published index.
|
|
82
|
+
--
|
|
83
|
+
-- `deleted` marks a withdrawal of an already-moved file, whose bytes are on the
|
|
84
|
+
-- device and whose R2 object is already gone. The row is removed outright once
|
|
85
|
+
-- the device file is confirmed absent — not kept as a tombstone, which would
|
|
86
|
+
-- grow this table without bound and force every listing read to filter it while
|
|
87
|
+
-- describing bytes that no longer exist anywhere.
|
|
88
|
+
--
|
|
89
|
+
-- IF NOT EXISTS adds no column to an existing table. A portal deployed before
|
|
90
|
+
-- this needs the three run by hand, once:
|
|
91
|
+
-- ALTER TABLE manifest ADD COLUMN targetPath TEXT NOT NULL DEFAULT '';
|
|
92
|
+
-- ALTER TABLE manifest ADD COLUMN devicePath TEXT NOT NULL DEFAULT '';
|
|
93
|
+
-- ALTER TABLE manifest ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
|
|
94
|
+
-- Their absence is loud: upload names targetPath in its INSERT.
|
|
64
95
|
CREATE TABLE IF NOT EXISTS manifest (
|
|
65
96
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
97
|
fileId TEXT UNIQUE NOT NULL,
|
|
@@ -69,11 +100,17 @@ CREATE TABLE IF NOT EXISTS manifest (
|
|
|
69
100
|
objectKey TEXT UNIQUE NOT NULL,
|
|
70
101
|
size INTEGER NOT NULL,
|
|
71
102
|
uploadedAt TEXT NOT NULL,
|
|
72
|
-
ingested INTEGER NOT NULL DEFAULT 0
|
|
103
|
+
ingested INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
targetPath TEXT NOT NULL DEFAULT '',
|
|
105
|
+
devicePath TEXT NOT NULL DEFAULT '',
|
|
106
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
73
107
|
);
|
|
74
108
|
|
|
75
109
|
CREATE INDEX IF NOT EXISTS manifest_uningested ON manifest (ingested);
|
|
76
110
|
CREATE INDEX IF NOT EXISTS manifest_owner ON manifest (ownerId);
|
|
111
|
+
-- The pull selects `ingested = 0 OR deleted = 1`, so the withdrawal half needs
|
|
112
|
+
-- its own index or a withdrawal costs a full scan every sixty seconds.
|
|
113
|
+
CREATE INDEX IF NOT EXISTS manifest_deleted ON manifest (deleted);
|
|
77
114
|
|
|
78
115
|
-- Fixed-window auth attempt counter. Pages Functions are stateless, so the
|
|
79
116
|
-- counter cannot live in process memory the way every other rate limiter in
|
|
@@ -88,16 +125,36 @@ CREATE TABLE IF NOT EXISTS auth_attempts (
|
|
|
88
125
|
|
|
89
126
|
CREATE UNIQUE INDEX IF NOT EXISTS auth_attempts_scope_window ON auth_attempts (scope, windowStart);
|
|
90
127
|
|
|
91
|
-
-- One row per file the device has published to this portal. Metadata
|
|
92
|
-
-- bytes stay on the device and are fetched on demand through a
|
|
93
|
-
-- signed link, which is what lets the folder tree render while the
|
|
94
|
-
-- offline.
|
|
128
|
+
-- One row per file OR folder the device has published to this portal. Metadata
|
|
129
|
+
-- only: the bytes stay on the device and are fetched on demand through a
|
|
130
|
+
-- short-lived signed link, which is what lets the folder tree render while the
|
|
131
|
+
-- device is offline.
|
|
95
132
|
--
|
|
96
133
|
-- Written by a full replace per account (delete-all, then insert the current
|
|
97
134
|
-- walk), which is why there is no tombstone column: a file deleted or renamed on
|
|
98
135
|
-- the device simply stops being inserted on the next cycle. The UNIQUE key is
|
|
99
136
|
-- what makes that replace safe to retry — a double insert of the same path
|
|
100
137
|
-- fails loudly instead of silently doubling the row.
|
|
138
|
+
--
|
|
139
|
+
-- `isDir` marks a folder row. Folders are stored rather than derived from file
|
|
140
|
+
-- path prefixes, because a folder holding no files at any depth has no prefix
|
|
141
|
+
-- to derive it from and would not exist to the client at all. A folder row
|
|
142
|
+
-- carries sizeBytes 0 and the folder's own modified time, and the download path
|
|
143
|
+
-- refuses to mint a signed link for one.
|
|
144
|
+
--
|
|
145
|
+
-- Like `people.accountId` and `people.folders`, IF NOT EXISTS will NOT add this
|
|
146
|
+
-- column to a `directory` table that predates it, and the push names `isDir` in
|
|
147
|
+
-- its INSERT, so such a table fails loudly on the next cycle. Add it by hand in
|
|
148
|
+
-- that case:
|
|
149
|
+
-- ALTER TABLE directory ADD COLUMN isDir INTEGER NOT NULL DEFAULT 0;
|
|
150
|
+
-- No backfill follows it: the push replaces every row each cycle.
|
|
151
|
+
--
|
|
152
|
+
-- `generation` is which staging round a row belongs to (Task 1910). The push
|
|
153
|
+
-- writes the next generation, flips the pointer below in ONE statement, then
|
|
154
|
+
-- deletes rows of earlier generations. Uniqueness is scoped by generation
|
|
155
|
+
-- because two of them hold the same relPath between the write and the flip;
|
|
156
|
+
-- UNIQUE (accountId, relPath) would reject the staged write outright.
|
|
157
|
+
-- ALTER TABLE directory ADD COLUMN generation INTEGER NOT NULL DEFAULT 0;
|
|
101
158
|
CREATE TABLE IF NOT EXISTS directory (
|
|
102
159
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
160
|
accountId TEXT NOT NULL,
|
|
@@ -105,7 +162,28 @@ CREATE TABLE IF NOT EXISTS directory (
|
|
|
105
162
|
sizeBytes INTEGER NOT NULL,
|
|
106
163
|
modifiedAt TEXT NOT NULL,
|
|
107
164
|
indexedAt TEXT NOT NULL,
|
|
108
|
-
|
|
165
|
+
isDir INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
167
|
+
UNIQUE (accountId, relPath, generation)
|
|
109
168
|
);
|
|
110
169
|
|
|
111
170
|
CREATE INDEX IF NOT EXISTS directory_account ON directory (accountId);
|
|
171
|
+
|
|
172
|
+
-- Which generation of an account's rows is the live one.
|
|
173
|
+
--
|
|
174
|
+
-- The flip of this single row is the atomic point of a publish: a client
|
|
175
|
+
-- listing that races a push reads the old tree or the new one, and never a
|
|
176
|
+
-- half-built one. Cleanup of superseded rows afterwards need not be atomic,
|
|
177
|
+
-- because nothing reads a generation this pointer does not name.
|
|
178
|
+
--
|
|
179
|
+
-- This replaces the delete-then-insert window recorded as backlog Task 1842.
|
|
180
|
+
-- At sixty-second cadence that window would be hit sixty times more often than
|
|
181
|
+
-- it was hourly, which is what turned a known defect into a blocking one.
|
|
182
|
+
--
|
|
183
|
+
-- An account with no row here has never completed a publish, and every reader
|
|
184
|
+
-- joins through this table, so it correctly shows nothing rather than showing
|
|
185
|
+
-- a staged generation that no flip ever blessed.
|
|
186
|
+
CREATE TABLE IF NOT EXISTS directory_state (
|
|
187
|
+
accountId TEXT PRIMARY KEY,
|
|
188
|
+
currentGeneration INTEGER NOT NULL
|
|
189
|
+
);
|
|
@@ -25,6 +25,25 @@ function message(accountId, relPath, expiresAtMs) {
|
|
|
25
25
|
return `${accountId}\n${relPath}\n${expiresAtMs}`
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The own-upload message (Task 1910).
|
|
30
|
+
*
|
|
31
|
+
* A SECOND shape rather than an extended first one, for two reasons.
|
|
32
|
+
*
|
|
33
|
+
* The literal `uploads` discriminator is what stops an exposed-folder signature
|
|
34
|
+
* ever admitting an uploads path, so the owner binding is load-bearing rather
|
|
35
|
+
* than decorative. And the two ends redeploy at different times — the device
|
|
36
|
+
* through an installer publish, the portal through site-deploy — so a single
|
|
37
|
+
* extended message would break every existing download in the window between
|
|
38
|
+
* them. An already-deployed portal never mints this type, so nothing regresses.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} accountId @param {string} ownerId @param {string} relPath
|
|
41
|
+
* @param {number} expiresAtMs
|
|
42
|
+
*/
|
|
43
|
+
function ownUploadMessage(accountId, ownerId, relPath, expiresAtMs) {
|
|
44
|
+
return `${accountId}\nuploads\n${ownerId}\n${relPath}\n${expiresAtMs}`
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/** @param {string} secret */
|
|
29
48
|
async function keyFor(secret) {
|
|
30
49
|
return crypto.subtle.importKey(
|
|
@@ -85,3 +104,58 @@ export async function buildSignedUrl(origin, secret, accountId, relPath, expires
|
|
|
85
104
|
url.searchParams.set('sig', sig)
|
|
86
105
|
return url.toString()
|
|
87
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sign one own-upload path. Lowercase hex, same algorithm as signPath — only
|
|
110
|
+
* the message differs.
|
|
111
|
+
* @param {string} secret @param {string} accountId @param {string} ownerId
|
|
112
|
+
* @param {string} relPath @param {number} expiresAtMs @returns {Promise<string>}
|
|
113
|
+
*/
|
|
114
|
+
export async function signOwnUpload(secret, accountId, ownerId, relPath, expiresAtMs) {
|
|
115
|
+
const key = await keyFor(secret)
|
|
116
|
+
const mac = await crypto.subtle.sign(
|
|
117
|
+
'HMAC',
|
|
118
|
+
key,
|
|
119
|
+
new TextEncoder().encode(ownUploadMessage(accountId, ownerId, relPath, expiresAtMs)),
|
|
120
|
+
)
|
|
121
|
+
return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} secret @param {string} accountId @param {string} ownerId
|
|
126
|
+
* @param {string} relPath @param {number} expiresAtMs @param {string} signature
|
|
127
|
+
* @param {number} nowMs @returns {Promise<boolean>}
|
|
128
|
+
*/
|
|
129
|
+
export async function verifyOwnUpload(
|
|
130
|
+
secret,
|
|
131
|
+
accountId,
|
|
132
|
+
ownerId,
|
|
133
|
+
relPath,
|
|
134
|
+
expiresAtMs,
|
|
135
|
+
signature,
|
|
136
|
+
nowMs,
|
|
137
|
+
) {
|
|
138
|
+
if (!secret || !signature) return false
|
|
139
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) return false
|
|
140
|
+
const expected = await signOwnUpload(secret, accountId, ownerId, relPath, expiresAtMs)
|
|
141
|
+
return constantTimeEqual(expected, signature)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The owner travels as a query parameter as well as inside the HMAC: the device
|
|
146
|
+
* needs to know which folder is being claimed before it can verify the claim.
|
|
147
|
+
* The parameter is never trusted on its own — a mismatch simply fails to verify.
|
|
148
|
+
* @param {string} origin @param {string} secret @param {string} accountId
|
|
149
|
+
* @param {string} ownerId @param {string} relPath @param {number} expiresAtMs
|
|
150
|
+
* @returns {Promise<string>}
|
|
151
|
+
*/
|
|
152
|
+
export async function buildOwnUploadUrl(origin, secret, accountId, ownerId, relPath, expiresAtMs) {
|
|
153
|
+
const sig = await signOwnUpload(secret, accountId, ownerId, relPath, expiresAtMs)
|
|
154
|
+
const url = new URL('/api/portal/fetch', origin)
|
|
155
|
+
url.searchParams.set('accountId', accountId)
|
|
156
|
+
url.searchParams.set('owner', ownerId)
|
|
157
|
+
url.searchParams.set('path', relPath)
|
|
158
|
+
url.searchParams.set('exp', String(expiresAtMs))
|
|
159
|
+
url.searchParams.set('sig', sig)
|
|
160
|
+
return url.toString()
|
|
161
|
+
}
|