@rubytech/create-maxy-code 0.1.478 → 0.1.480
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +830 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +163 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +22 -0
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
- package/payload/platform/lib/graph-style/src/index.ts +20 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +155 -12
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +3 -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 +68 -28
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +194 -16
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +74 -29
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +13 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
- package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +41 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +249 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js +127 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +17 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +26 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts +37 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js +126 -5
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +18 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +123 -18
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +45 -2
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +122 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/server/{chunk-3XLLTG6R.js → chunk-REWXLAPC.js} +30 -10
- package/payload/server/package.json +1 -1
- package/payload/server/public/activity.html +7 -6
- package/payload/server/public/assets/{AdminLoginScreens-DR5eLF-a.js → AdminLoginScreens-DXYJWXaX.js} +1 -1
- package/payload/server/public/assets/AdminShell-UDgNN4CA.js +2 -0
- package/payload/server/public/assets/Checkbox-C08Ulc_a.js +1 -0
- package/payload/server/public/assets/activity-06gsRDVv.js +1 -0
- package/payload/server/public/assets/admin-BhiHyORH.js +1 -0
- package/payload/server/public/assets/{arc-BrHWN2Kr.js → arc-BHXuzTwg.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-Ct5q6ks1.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Clt9NOr0.js → architectureDiagram-Q4EWVU46-DsN_vxau.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BOe_jmXi.js → blockDiagram-DXYQGD6D-CvNat1uB.js} +1 -1
- package/payload/server/public/assets/browser-Cz4IKS47.js +1 -0
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-B64FaKMV.js → c4Diagram-AHTNJAMY-E9yqTPTW.js} +1 -1
- package/payload/server/public/assets/calendar-D0xtNqH8.js +1 -0
- package/payload/server/public/assets/channel-PE9vBKEQ.js +1 -0
- package/payload/server/public/assets/chat-DwLJ830n.js +1 -0
- package/payload/server/public/assets/chevron-left-Cv-TFuEf.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-BGAUfWvM.js → chunk-2KRD3SAO-DpzI_0EY.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-PFi6uQZ0.js → chunk-336JU56O-eR_I9Eu4.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-BDNZtJsz.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-CkxXXCiF.js → chunk-4BX2VUAB-D-6xk1t8.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-B-7tN4IA.js → chunk-4TB4RGXK-BkqUcFpL.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-CBkNmqrt.js → chunk-55IACEB6-ChCl2pSP.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-wa9ujn7i.js → chunk-5FUZZQ4R-DasGsg54.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-DIk-xBaE.js → chunk-5PVQY5BW-DXlHUX1h.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-BbMV1lgW.js → chunk-67CJDMHE-BZfVpTsE.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-C49Mv-nQ.js → chunk-7N4EOEYR-C6U5TIgT.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-Dy_pJOp_.js → chunk-AA7GKIK3-CA7cVDmw.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-Ce-OmPqn.js → chunk-BSJP7CBP-DxN2GC3p.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-tnjq-APC.js → chunk-CIAEETIT-C0-4wA8y.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-Bvv_deZ2.js → chunk-EDXVE4YY--kyb1rLc.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CmxCPKyU.js → chunk-ENJZ2VHE-FBN6hR7V.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-Top6y08S.js → chunk-FMBD7UC4-Lpge4M5g.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-B9LPNCUS.js → chunk-FOC6F5B3-CM1rwOou.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-Bi-_c351.js → chunk-ICPOFSXX-JdYLY3yu.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-DAnRM04h.js → chunk-K5T4RW27-DCUblGBr.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-C0QYcbLe.js → chunk-KGLVRYIC-BP_cide2.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-BjvA5ykt.js → chunk-LIHQZDEY-DjMa1lOl.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-Cs46Gj7z.js → chunk-ORNJ4GCN-D51eJAoV.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BBpimab5.js → chunk-OYMX7WX6-4hWIO544.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-B_Hris8x.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-CQUSBHoK.js → chunk-U2HBQHQK-BwdQf9cS.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-Dm8TevG0.js → chunk-X2U36JSP-DmDxwLOg.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-B10QuxYb.js → chunk-XPW4576I-JQyAB7ND.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-vaGsu4Wk.js → chunk-YZCP3GAM-BonLdCzI.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-D1Qi22bL.js → chunk-ZZ45TVLE-BXKB2wSk.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-D8Q05iMI.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Dm3hAxfm.js +1 -0
- package/payload/server/public/assets/clone-C-wOwvpY.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-CRDvLRXs.js → cose-bilkent-S5V4N54A-BDnd4Blw.js} +1 -1
- package/payload/server/public/assets/{dagre-CEmqJTt3.js → dagre-BmexzuER.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-LXMTo7Fp.js → dagre-KV5264BT-D4WVuwPj.js} +1 -1
- package/payload/server/public/assets/data-DUDJfnjI.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-zyis6nPt.js → diagram-5BDNPKRD-Bbf4mT8a.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BiU0XAAD.js → diagram-G4DWMVQ6-BpqVTUmx.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-rGFiz1SX.js → diagram-MMDJMWI5-DY-k5k6r.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-B0wak1ZJ.js → diagram-TYMM5635-CXHxwacS.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D8AUxLyX.js → erDiagram-SMLLAGMA-BNKEc3l8.js} +1 -1
- package/payload/server/public/assets/{flatten-naJ5PJ4f.js → flatten-jn8bZgcI.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BtdUf68b.js → flowDiagram-DWJPFMVM-CbAeuJsi.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-C-lkzDmf.js → ganttDiagram-T4ZO3ILL-CeeNnL4l.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-BBHsY16R.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DRUKumq2.js → gitGraphDiagram-UUTBAWPF-DG8RCtfg.js} +1 -1
- package/payload/server/public/assets/{graph-DRloNic4.js → graph-BAoktBcx.js} +19 -19
- package/payload/server/public/assets/graph-labels-LeIhl7ZG.js +1 -0
- package/payload/server/public/assets/{graphlib-zRsaoFEC.js → graphlib-BkDExPQi.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-C1j5I_zx.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-ZWkwHWEu.js +2 -0
- package/payload/server/public/assets/{isEmpty-Bnpg7b8f.js → isEmpty-Dkk6_L-B.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BguMgoR8.js → ishikawaDiagram-UXIWVN3A-D_iMHV64.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CXhyK1n-.js → journeyDiagram-VCZTEJTY-BvPRpulx.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-C0UCWVx-.js → kanban-definition-6JOO6SKY-CkWaXWo2.js} +1 -1
- package/payload/server/public/assets/{line--uhS9YDE.js → line-BGQT9unx.js} +1 -1
- package/payload/server/public/assets/{linear-CAOWPodQ.js → linear-DzrYI8-8.js} +1 -1
- package/payload/server/public/assets/{maximize-2-CzDaBugs.js → maximize-2-2MgPSwlx.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-D6kPx7vc.js → mermaid-parser.core-DrMY7FMe.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-CTd4DS6O.js → mermaid.core-BWdf6fZ7.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-_AyvyG8C.js → mindmap-definition-QFDTVHPH-CgqM9h97.js} +1 -1
- package/payload/server/public/assets/operator-CZifmNhp.js +1 -0
- package/payload/server/public/assets/{ordinal-Wx4m9zHk.js → ordinal-DuJYM7p1.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-CGiTbe0N.js +1 -0
- package/payload/server/public/assets/{page-ef-6rRPL.js → page-B4kllGd_.js} +6 -6
- package/payload/server/public/assets/page-DbfE7QwJ.js +1 -0
- package/payload/server/public/assets/{pdf-render-CeX6d2v7.js → pdf-render-riHufgrn.js} +1 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-BrK023dE.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-6TsUDkkQ.js → pieDiagram-DEJITSTG-BeUGoYS7.js} +1 -1
- package/payload/server/public/assets/public-VtmyYz8E.js +1 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CWw9gOI4.js → quadrantDiagram-34T5L4WZ-CX82WgKy.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-p7NRamXX.js +1 -0
- package/payload/server/public/assets/{reduce-BUmc8RDR.js → reduce-COyotL2V.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DCOlxNvx.js → requirementDiagram-MS252O5E-CzqebQXj.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-DXEKUhXP.js → rotate-ccw-DaZdB2cz.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CW4Xbir3.js → sankeyDiagram-XADWPNL6-CaHi1yzU.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-vhb_pBLG.js → sequenceDiagram-FGHM5R23-BF2AeiKW.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Cb1WExEX.js → stateDiagram-FHFEXIEX-DtUCA8Ih.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-a4FvB3L8.js +1 -0
- package/payload/server/public/assets/tasks-CvqVDbfO.js +1 -0
- package/payload/server/public/assets/{time-entry-format-CsYAjplS.js → time-entry-format-TD-PPEIu.js} +1 -1
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BAHTf_AF.js → timeline-definition-GMOUNBTQ-D3Q1YrKY.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-CoWk8VoG.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-BhWeGj8Y.js +1 -0
- package/payload/server/public/assets/{triangle-alert-0lSBIUg-.js → triangle-alert-BaLhVNnn.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-BhjPzRa3.js +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CWRpVt1-.css +1 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-BZnzc5C3.js → useSubAccountSwitcher-HEVLb6Z9.js} +2 -2
- package/payload/server/public/assets/useVoiceRecorder-CfGgj3vm.js +2 -0
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DtEttvW4.js → vennDiagram-DHZGUBPP-DnjzQDGf.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-C0q3Hqng.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-K06y6CVJ.js → wardleyDiagram-NUSXRM2D-T3WHrUvi.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D66c40lp.js → xychartDiagram-5P7HB3ND-2Efyv4h2.js} +1 -1
- package/payload/server/public/brand-defaults.css +1 -0
- package/payload/server/public/browser.html +4 -5
- package/payload/server/public/calendar.html +5 -6
- package/payload/server/public/chat.html +12 -13
- package/payload/server/public/data.html +11 -12
- package/payload/server/public/graph.html +10 -11
- package/payload/server/public/index.html +14 -15
- package/payload/server/public/operator.html +14 -15
- package/payload/server/public/privacy.html +1 -0
- package/payload/server/public/public.html +12 -13
- package/payload/server/public/tasks.html +7 -5
- package/payload/server/public/vnc-popout.html +1 -0
- package/payload/server/server.js +833 -389
- package/payload/server/{src-XQ63FPRE.js → src-JJ6PHO5I.js} +1 -1
- package/payload/server/public/assets/AdminShell-D9zaUxPd.js +0 -2
- package/payload/server/public/assets/Checkbox-DcbbAIe5.js +0 -1
- package/payload/server/public/assets/activity-C9P5NjEZ.js +0 -1
- package/payload/server/public/assets/admin-D2KLZnQW.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-CZQzGRSM.js +0 -1
- package/payload/server/public/assets/browser-CDWqEiCD.js +0 -1
- package/payload/server/public/assets/calendar-BG7fOpbB.js +0 -1
- package/payload/server/public/assets/channel-zDUAN9Ks.js +0 -1
- package/payload/server/public/assets/chat-RyH-WYQC.js +0 -1
- package/payload/server/public/assets/chevron-left-DmXCr6nB.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-iJnqNRug.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Qlc2PFAj.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CFsKL52p.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BWBUUCiO.js +0 -1
- package/payload/server/public/assets/clone-h5XhkDUL.js +0 -1
- package/payload/server/public/assets/data-BaY_LkLR.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CBZPQJLj.js +0 -1
- package/payload/server/public/assets/graph-labels-DD-GqS4v.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-1vIoX-kP.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-KHcXRaan.js +0 -2
- package/payload/server/public/assets/operator-BZeeyait.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-CTU0gCGb.js +0 -1
- package/payload/server/public/assets/page-B0obcg3b.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-D-EEHxhC.js +0 -1
- package/payload/server/public/assets/public-9D3MMOkC.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-DHXATZGB.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CxOIKvXh.js +0 -1
- package/payload/server/public/assets/tasks-BB0L7e5a.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-CC7ouMFd.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-CaCg27im.js +0 -1
- package/payload/server/public/assets/useCopyFeedback-jTBQ2h0V.js +0 -1
- package/payload/server/public/assets/useSelectionMode-BooArySZ.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-D9knHOKq.css +0 -1
- package/payload/server/public/assets/useVoiceRecorder-PUT8q_sm.js +0 -2
- package/payload/server/public/assets/wardley-RL74JXVD-bMPsqQZ7.js +0 -1
- /package/payload/server/public/assets/{_baseFor-k8hYPe-U.js → _baseFor-Bjj9It3Q.js} +0 -0
- /package/payload/server/public/assets/{array-BFDiaBgf.js → array-BrrxEo12.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-Ch0xptA4.js → cytoscape.esm-CTWJxUxo.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-ZeknFqNe.js → defaultLocale-CFDbRV4k.js} +0 -0
- /package/payload/server/public/assets/{dist-B2N0v8hZ.js → dist-BATfy7Sn.js} +0 -0
- /package/payload/server/public/assets/{init-0VJVrkRJ.js → init-dXCzGgLP.js} +0 -0
- /package/payload/server/public/assets/{katex-Br7bH10N.js → katex-CHA1JFYJ.js} +0 -0
- /package/payload/server/public/assets/{path-INs8XTPH.js → path-CCRpdXhK.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-D_sPnjeL.js → preload-helper-CH6UZRzu.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-DU9bZLvZ.js → rough.esm-Bud4g4fh.js} +0 -0
- /package/payload/server/public/assets/{src-DHSQDYx-.js → src-DNdBRXXx.js} +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Publish a sub-account's deliverable file INDEX to its portal's D1.
|
|
3
|
+
//
|
|
4
|
+
// Metadata only. The bytes stay on the device and are fetched on demand through
|
|
5
|
+
// a short-lived signed link, which is what lets the portal render the folder
|
|
6
|
+
// tree while the device is offline.
|
|
7
|
+
//
|
|
8
|
+
// Full replace per account rather than a diff: a delete or a rename on the
|
|
9
|
+
// device converges on the next cycle with no tombstone protocol to get wrong.
|
|
10
|
+
//
|
|
11
|
+
// Runnable standalone as well as from the heartbeat, because the heartbeat spawn
|
|
12
|
+
// this copies has never been observed running on a device (Task 1828). If the
|
|
13
|
+
// hourly run is silently dead, this script still works by hand.
|
|
14
|
+
//
|
|
15
|
+
// node portal-index-push.mjs --account-dir <dir> --account <id> --db <name>
|
|
16
|
+
// node portal-index-push.mjs --all-accounts
|
|
17
|
+
//
|
|
18
|
+
// Credentials come from the house file, never a flag; PLATFORM_ROOT locates it.
|
|
19
|
+
|
|
20
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
import { join, resolve } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import { resolveExposedDirs } from './schema-exposed-dirs.mjs'
|
|
26
|
+
import { createD1Client } from './d1-http.mjs'
|
|
27
|
+
|
|
28
|
+
const TAG = '[portal-index]'
|
|
29
|
+
|
|
30
|
+
/** Read `KEY=value` pairs from a secrets file without polluting process.env.
|
|
31
|
+
* Values are trimmed, matching the readers in reconcile-bookings.ts and
|
|
32
|
+
* house-scoped-token.ts — a CRLF file would otherwise leave a stray "\r" that
|
|
33
|
+
* reaches Cloudflare as an opaque invalid-header error.
|
|
34
|
+
* @param {string} path @returns {Record<string, string>} */
|
|
35
|
+
function readEnvFile(path) {
|
|
36
|
+
/** @type {Record<string, string>} */ const out = {}
|
|
37
|
+
if (!existsSync(path)) return out
|
|
38
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
39
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/)
|
|
40
|
+
if (m) out[m[1]] = m[2].trim()
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Files under `exposed` dirs, at any depth. Dot-prefixed entries are skipped at
|
|
47
|
+
* every level: `.uploads-tmp/` holds in-flight upload partials (files.ts:25) and
|
|
48
|
+
* must never be published as a deliverable.
|
|
49
|
+
* @param {string} accountDir @param {string[]} exposed
|
|
50
|
+
* @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string}[]>}
|
|
51
|
+
*/
|
|
52
|
+
export async function walkExposed(accountDir, exposed) {
|
|
53
|
+
/** @type {{relPath: string, sizeBytes: number, modifiedAt: string}[]} */
|
|
54
|
+
const out = []
|
|
55
|
+
|
|
56
|
+
/** @param {string} rel */
|
|
57
|
+
async function walk(rel) {
|
|
58
|
+
/** @type {import('node:fs').Dirent[]} */ let entries
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(join(accountDir, rel), { withFileTypes: true })
|
|
61
|
+
} catch {
|
|
62
|
+
return // absent or unreadable dir contributes nothing
|
|
63
|
+
}
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
if (e.name.startsWith('.')) continue
|
|
66
|
+
const childRel = `${rel}/${e.name}`
|
|
67
|
+
if (e.isDirectory()) {
|
|
68
|
+
await walk(childRel)
|
|
69
|
+
} else if (e.isFile()) {
|
|
70
|
+
try {
|
|
71
|
+
const st = await stat(join(accountDir, childRel))
|
|
72
|
+
out.push({ relPath: childRel, sizeBytes: st.size, modifiedAt: st.mtime.toISOString() })
|
|
73
|
+
} catch {
|
|
74
|
+
// vanished mid-walk; the next cycle settles it
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const dir of exposed) await walk(dir)
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {{ accountDir: string, accountId: string, dbName: string,
|
|
86
|
+
* client: { query: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]> },
|
|
87
|
+
* log: (line: string) => void, nowIso: string,
|
|
88
|
+
* previousExposed?: string[] }} opts
|
|
89
|
+
* @returns {Promise<{ exposed: string[], rows: number, schemaPresent: boolean }>}
|
|
90
|
+
*/
|
|
91
|
+
export async function pushAccount(opts) {
|
|
92
|
+
const { accountDir, accountId, client, log, nowIso } = opts
|
|
93
|
+
|
|
94
|
+
/** @type {string | null} */ let schemaText = null
|
|
95
|
+
try {
|
|
96
|
+
schemaText = await readFile(join(accountDir, 'SCHEMA.md'), 'utf8')
|
|
97
|
+
} catch {
|
|
98
|
+
schemaText = null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const r = resolveExposedDirs(schemaText)
|
|
102
|
+
log(
|
|
103
|
+
`${TAG} op=resolve account=${accountId} schemaPresent=${r.schemaPresent} allowed=${r.allowed.length} ` +
|
|
104
|
+
`domainBuckets=${r.domainBuckets.join(',') || 'none'} exposed=${r.exposed.join(',') || 'none'}`,
|
|
105
|
+
)
|
|
106
|
+
// The generator gives a plugin-claimed dir to the plugin-owned region only
|
|
107
|
+
// (account-schema-owned-dirs.py:287), so this is normally silent. When it is
|
|
108
|
+
// not, a bucket has vanished from the client's view and nothing else says so.
|
|
109
|
+
for (const c of r.collisions) {
|
|
110
|
+
log(`${TAG} op=collision account=${accountId} dir=${c} claimedBy=plugin-owned`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Emitted only on a delta, so the line stays rare and is always worth
|
|
114
|
+
// reading. A schema regeneration — a vertical change, a plugin claiming a
|
|
115
|
+
// dir — silently changes what the client sees, with no deploy and nothing
|
|
116
|
+
// else to say so. `removed=` on a populated bucket is the client-visible
|
|
117
|
+
// regression. Reported from the push because the push is what applies the
|
|
118
|
+
// change; if it is not running, nothing changes and there is nothing to say.
|
|
119
|
+
if (opts.previousExposed) {
|
|
120
|
+
const added = r.exposed.filter((d) => !opts.previousExposed.includes(d))
|
|
121
|
+
const removed = opts.previousExposed.filter((d) => !r.exposed.includes(d))
|
|
122
|
+
if (added.length || removed.length) {
|
|
123
|
+
log(
|
|
124
|
+
`${TAG} op=exposed-changed account=${accountId} ` +
|
|
125
|
+
`added=${added.join(',') || 'none'} removed=${removed.join(',') || 'none'}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fail closed: no schema, or nothing exposed, means write NOTHING — not a
|
|
131
|
+
// delete. A delete would leave the table identical to a successful publish of
|
|
132
|
+
// an account that genuinely has no deliverables, which is the one state an
|
|
133
|
+
// operator must be able to tell apart.
|
|
134
|
+
if (!r.schemaPresent || r.exposed.length === 0) {
|
|
135
|
+
return { exposed: r.exposed, rows: 0, schemaPresent: r.schemaPresent }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rows = await walkExposed(accountDir, r.exposed)
|
|
139
|
+
|
|
140
|
+
await client.query('DELETE FROM directory WHERE accountId = ?', [accountId])
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
await client.query(
|
|
143
|
+
'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt) VALUES (?, ?, ?, ?, ?)',
|
|
144
|
+
[accountId, row.relPath, row.sizeBytes, row.modifiedAt, nowIso],
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
log(`${TAG} op=replace account=${accountId} deleted=all inserted=${rows.length}`)
|
|
148
|
+
|
|
149
|
+
// Read back rather than trusting the write: "issued the inserts" is not "the
|
|
150
|
+
// rows are there", and a half-applied batch is otherwise invisible.
|
|
151
|
+
const check = await client.query('SELECT COUNT(*) AS n FROM directory WHERE accountId = ?', [accountId])
|
|
152
|
+
const after = Number(check[0]?.n ?? -1)
|
|
153
|
+
log(`${TAG} op=verify account=${accountId} rowsAfter=${after} expected=${rows.length}`)
|
|
154
|
+
|
|
155
|
+
return { exposed: r.exposed, rows: rows.length, schemaPresent: true }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Where the push records what it last successfully wrote, per account.
|
|
160
|
+
*
|
|
161
|
+
* The heartbeat's audit reads this IN-PROCESS to answer "is this index still
|
|
162
|
+
* being refreshed?" — a question no event-driven line can answer, because the
|
|
163
|
+
* failure being detected is the absence of events. It lives under data/ rather
|
|
164
|
+
* than in an account dir so the audit needs nothing but PLATFORM_ROOT, and so
|
|
165
|
+
* it never trips the account write-path guard.
|
|
166
|
+
* @param {string} platformRoot
|
|
167
|
+
*/
|
|
168
|
+
export function stateFilePath(platformRoot) {
|
|
169
|
+
return resolve(platformRoot, '..', 'data', 'portal-index-state.json')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {string} platformRoot @returns {Record<string, {lastPushMs: number, rows: number, exposed: string[]}>} */
|
|
173
|
+
export function readState(platformRoot) {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(readFileSync(stateFilePath(platformRoot), 'utf8'))
|
|
176
|
+
} catch {
|
|
177
|
+
return {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Written ONLY after op=verify, so a stale entry means the push stopped
|
|
183
|
+
* succeeding rather than merely stopped logging. Read-modify-write per account
|
|
184
|
+
* so one account's failure does not erase another's record.
|
|
185
|
+
* @param {string} platformRoot @param {string} accountId
|
|
186
|
+
* @param {{lastPushMs: number, rows: number, exposed: string[]}} entry
|
|
187
|
+
*/
|
|
188
|
+
export function recordSuccess(platformRoot, accountId, entry) {
|
|
189
|
+
const path = stateFilePath(platformRoot)
|
|
190
|
+
const state = readState(platformRoot)
|
|
191
|
+
state[accountId] = entry
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(path, JSON.stringify(state, null, 1))
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Never fatal: the index IS published at this point. But never silent
|
|
196
|
+
// either — an unwritable state file makes every later audit report a
|
|
197
|
+
// healthy account as never-ran, which is a false alarm that would send an
|
|
198
|
+
// operator looking in the wrong place.
|
|
199
|
+
console.error(
|
|
200
|
+
`${TAG} op=state-write-failed account=${accountId} path="${path}" err="${err instanceof Error ? err.message : String(err)}"`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {string} flag */
|
|
206
|
+
function arg(flag) {
|
|
207
|
+
const i = process.argv.indexOf(flag)
|
|
208
|
+
return i === -1 ? undefined : process.argv[i + 1]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Every account carrying a data-portal.json has a portal. The file's presence is
|
|
213
|
+
* the enrolment signal — there is no separate flag to keep in sync with it.
|
|
214
|
+
* @param {string} accountsRoot
|
|
215
|
+
* @returns {Promise<{accountDir: string, accountId: string, dbName: string}[]>}
|
|
216
|
+
*/
|
|
217
|
+
async function portalAccounts(accountsRoot) {
|
|
218
|
+
/** @type {{accountDir: string, accountId: string, dbName: string}[]} */ const out = []
|
|
219
|
+
/** @type {string[]} */ let names
|
|
220
|
+
try {
|
|
221
|
+
names = await readdir(accountsRoot)
|
|
222
|
+
} catch {
|
|
223
|
+
return out
|
|
224
|
+
}
|
|
225
|
+
for (const accountId of names) {
|
|
226
|
+
const accountDir = join(accountsRoot, accountId)
|
|
227
|
+
try {
|
|
228
|
+
const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
|
|
229
|
+
if (typeof cfg?.portalDbName === 'string' && cfg.portalDbName) {
|
|
230
|
+
out.push({ accountDir, accountId, dbName: cfg.portalDbName })
|
|
231
|
+
} else {
|
|
232
|
+
console.error(`${TAG} op=skip account=${accountId} reason=no-portalDbName`)
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// No data-portal.json: this account has no portal. Not an error.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve the Cloudflare credentials the D1 API needs.
|
|
243
|
+
*
|
|
244
|
+
* Two things this must not conflate. `cfAccountId` is the CLOUDFLARE account
|
|
245
|
+
* (the `/accounts/<id>/d1/...` path segment) and comes from the house
|
|
246
|
+
* credential; the Maxy sub-account id is a different value entirely and is what
|
|
247
|
+
* scopes the `directory` rows. Passing one where the other belongs makes every
|
|
248
|
+
* database lookup fail.
|
|
249
|
+
*
|
|
250
|
+
* And the house token is a MINTER — using it directly against D1 returns 10000.
|
|
251
|
+
* `cf-token.sh` mints (or reuses) a D1-scoped token from it and prints the
|
|
252
|
+
* secrets KEY holding the value, never the value itself.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} platformRoot
|
|
255
|
+
* @returns {{ cfAccountId: string, token: string } | { error: string }}
|
|
256
|
+
*/
|
|
257
|
+
function resolveCloudflareCredentials(platformRoot) {
|
|
258
|
+
const houseEnvPath = join(platformRoot, 'config', 'cloudflare-house.env')
|
|
259
|
+
const house = readEnvFile(houseEnvPath)
|
|
260
|
+
if (!house.CLOUDFLARE_API_TOKEN) return { error: 'no-house-cloudflare-credential' }
|
|
261
|
+
// Required rather than inferred: the D1 endpoint is addressed per Cloudflare
|
|
262
|
+
// account, and guessing it would run these statements against the wrong one.
|
|
263
|
+
if (!house.CLOUDFLARE_ACCOUNT_ID) return { error: 'no-house-cloudflare-account-id' }
|
|
264
|
+
|
|
265
|
+
const cfTokenSh = join(platformRoot, 'plugins', 'cloudflare', 'bin', 'cf-token.sh')
|
|
266
|
+
if (!existsSync(cfTokenSh)) return { error: 'cf-token-sh-missing' }
|
|
267
|
+
|
|
268
|
+
/** @type {string} */ let keyName
|
|
269
|
+
try {
|
|
270
|
+
keyName = execFileSync(cfTokenSh, ['d1', houseEnvPath], { encoding: 'utf8' }).trim()
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
273
|
+
return { error: `scoped-token-mint-failed: ${detail}` }
|
|
274
|
+
}
|
|
275
|
+
// Re-read: cf-token.sh persists the minted token into the same file.
|
|
276
|
+
const token = readEnvFile(houseEnvPath)[keyName]
|
|
277
|
+
if (!token) return { error: `scoped-token-absent-after-mint key=${keyName}` }
|
|
278
|
+
|
|
279
|
+
return { cfAccountId: house.CLOUDFLARE_ACCOUNT_ID, token }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function main() {
|
|
283
|
+
const platformRoot = process.env.PLATFORM_ROOT
|
|
284
|
+
if (!platformRoot) {
|
|
285
|
+
console.error(`${TAG} op=usage reason=no-platform-root PLATFORM_ROOT is required`)
|
|
286
|
+
process.exit(1)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const cred = resolveCloudflareCredentials(platformRoot)
|
|
290
|
+
if ('error' in cred) {
|
|
291
|
+
console.error(`${TAG} op=skip reason=${cred.error}`)
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @type {{accountDir: string, accountId: string, dbName: string}[]} */ let targets
|
|
296
|
+
if (process.argv.includes('--all-accounts')) {
|
|
297
|
+
// Same derivation as the platform's own DATA_ROOT (data-path.ts:19), from
|
|
298
|
+
// the PLATFORM_ROOT the heartbeat already passes.
|
|
299
|
+
const accountsRoot = resolve(platformRoot, '..', 'data', 'accounts')
|
|
300
|
+
targets = await portalAccounts(accountsRoot)
|
|
301
|
+
console.error(`${TAG} op=enumerate accountsRoot="${accountsRoot}" accounts=${targets.length}`)
|
|
302
|
+
} else {
|
|
303
|
+
const accountDir = arg('--account-dir')
|
|
304
|
+
const accountId = arg('--account')
|
|
305
|
+
const dbName = arg('--db')
|
|
306
|
+
if (!accountDir || !accountId || !dbName) {
|
|
307
|
+
console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
|
|
308
|
+
process.exit(1)
|
|
309
|
+
}
|
|
310
|
+
targets = [{ accountDir, accountId, dbName }]
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const state = readState(platformRoot)
|
|
314
|
+
let failed = 0
|
|
315
|
+
for (const t of targets) {
|
|
316
|
+
try {
|
|
317
|
+
// cfAccountId addresses the D1 endpoint; t.accountId scopes the rows.
|
|
318
|
+
// Two different identifiers — see resolveCloudflareCredentials.
|
|
319
|
+
const client = createD1Client({
|
|
320
|
+
accountId: cred.cfAccountId,
|
|
321
|
+
token: cred.token,
|
|
322
|
+
dbName: t.dbName,
|
|
323
|
+
})
|
|
324
|
+
const r = await pushAccount({
|
|
325
|
+
...t,
|
|
326
|
+
client,
|
|
327
|
+
log: (l) => console.error(l),
|
|
328
|
+
nowIso: new Date().toISOString(),
|
|
329
|
+
previousExposed: state[t.accountId]?.exposed,
|
|
330
|
+
})
|
|
331
|
+
// Only a schema-present run records success. A fail-closed run published
|
|
332
|
+
// nothing, so recording it would let a broken schema masquerade as a
|
|
333
|
+
// healthy hourly cycle for as long as it stayed broken.
|
|
334
|
+
if (r.schemaPresent) {
|
|
335
|
+
recordSuccess(platformRoot, t.accountId, {
|
|
336
|
+
lastPushMs: Date.now(),
|
|
337
|
+
rows: r.rows,
|
|
338
|
+
exposed: r.exposed,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
console.error(`${TAG} op=done account=${t.accountId} rows=${r.rows}`)
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// One account's failure must not stop the rest, and it must never be
|
|
344
|
+
// swallowed — the message is the only thing that says which account.
|
|
345
|
+
failed++
|
|
346
|
+
console.error(
|
|
347
|
+
`${TAG} op=failed account=${t.accountId} err="${err instanceof Error ? err.message : String(err)}"`,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (failed > 0) process.exit(1)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Only when invoked directly, so the module stays importable by tests.
|
|
355
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
356
|
+
void main()
|
|
357
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Which folders of an account may a portal client see?
|
|
2
|
+
//
|
|
3
|
+
// Not a list in this file. The answer is derived per account from that
|
|
4
|
+
// account's own SCHEMA.md, which is generated by
|
|
5
|
+
// platform/scripts/lib/account-schema-owned-dirs.py: one bucket per top-level
|
|
6
|
+
// entity of the brand's vertical ontology, plus whatever plugins claim, inside
|
|
7
|
+
// a closed allowed-top-level set.
|
|
8
|
+
//
|
|
9
|
+
// Exposed = ontology buckets + `output`, intersected with the allowed set.
|
|
10
|
+
// Ontology buckets hold per-entity artifacts ("File a job's artifacts under its
|
|
11
|
+
// entity folder", the region's own prose); `output` is the delivery convention
|
|
12
|
+
// (doc-source-resolve.ts:14 calls it agent-authored). Operator-data buckets,
|
|
13
|
+
// uploads, plugin workspaces and platform-internal dirs are in neither source
|
|
14
|
+
// and so are never exposed.
|
|
15
|
+
|
|
16
|
+
const ONT_START = '<!-- ontology-buckets:start -->'
|
|
17
|
+
const ONT_END = '<!-- ontology-buckets:end -->'
|
|
18
|
+
const OWNED_START = '<!-- plugin-owned-dirs:start -->'
|
|
19
|
+
const OWNED_END = '<!-- plugin-owned-dirs:end -->'
|
|
20
|
+
|
|
21
|
+
/** The delivery convention, always exposed when the account allows it. */
|
|
22
|
+
export const ALWAYS_EXPOSED = 'output'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The generator writes the ontology region with an ASCII hyphen and the
|
|
26
|
+
* plugin-owned region with an em dash (account-schema-owned-dirs.py:294 vs
|
|
27
|
+
* :281). Matching the wrong one yields zero buckets and reads as "this account
|
|
28
|
+
* has no deliverables", which is why each region has its own pattern.
|
|
29
|
+
*/
|
|
30
|
+
const DOMAIN_LINE = /^- `([^`/]+)\/` - one folder per /
|
|
31
|
+
const OWNED_LINE = /^- `([^`/]+)\/` — /
|
|
32
|
+
|
|
33
|
+
/** @param {string} text @param {string} start @param {string} end */
|
|
34
|
+
function region(text, start, end) {
|
|
35
|
+
const i = text.indexOf(start)
|
|
36
|
+
if (i === -1) return null
|
|
37
|
+
const j = text.indexOf(end, i)
|
|
38
|
+
return text.slice(i, j === -1 ? text.length : j)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {string} text @returns {string[] | null} */
|
|
42
|
+
function allowedBlockOrNull(text) {
|
|
43
|
+
const lines = text.split('\n')
|
|
44
|
+
const start = lines.findIndex((l) => l.trim() === '```allowed-top-level')
|
|
45
|
+
if (start === -1) return null
|
|
46
|
+
for (let j = start + 1; j < lines.length; j++) {
|
|
47
|
+
if (lines[j].trim() === '```') {
|
|
48
|
+
return lines.slice(start + 1, j).map((l) => l.trim()).filter(Boolean)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string | null} schemaText Contents of accounts/<id>/SCHEMA.md, or null when absent.
|
|
56
|
+
* @returns {{ schemaPresent: boolean, allowed: string[], domainBuckets: string[],
|
|
57
|
+
* pluginOwned: string[], collisions: string[], exposed: string[] }}
|
|
58
|
+
*/
|
|
59
|
+
export function resolveExposedDirs(schemaText) {
|
|
60
|
+
const empty = {
|
|
61
|
+
schemaPresent: false,
|
|
62
|
+
allowed: /** @type {string[]} */ ([]),
|
|
63
|
+
domainBuckets: /** @type {string[]} */ ([]),
|
|
64
|
+
pluginOwned: /** @type {string[]} */ ([]),
|
|
65
|
+
collisions: /** @type {string[]} */ ([]),
|
|
66
|
+
exposed: /** @type {string[]} */ ([]),
|
|
67
|
+
}
|
|
68
|
+
// Fail CLOSED. fs-schema-guard.sh:76-78 fails open on a missing schema so an
|
|
69
|
+
// unseeded legacy account is not write-blocked; a read surface facing a
|
|
70
|
+
// client must do the opposite. Do not "fix" this to match the guard.
|
|
71
|
+
if (typeof schemaText !== 'string' || schemaText.length === 0) return empty
|
|
72
|
+
|
|
73
|
+
const allowed = allowedBlockOrNull(schemaText)
|
|
74
|
+
if (allowed === null) return { ...empty, schemaPresent: true }
|
|
75
|
+
|
|
76
|
+
const ont = region(schemaText, ONT_START, ONT_END)
|
|
77
|
+
const owned = region(schemaText, OWNED_START, OWNED_END)
|
|
78
|
+
|
|
79
|
+
/** @param {string | null} text @param {RegExp} re */
|
|
80
|
+
const dirsIn = (text, re) => {
|
|
81
|
+
if (!text) return []
|
|
82
|
+
/** @type {string[]} */ const out = []
|
|
83
|
+
for (const line of text.split('\n')) {
|
|
84
|
+
const m = re.exec(line.trim())
|
|
85
|
+
if (m && !out.includes(m[1])) out.push(m[1])
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const domainBuckets = dirsIn(ont, DOMAIN_LINE)
|
|
91
|
+
const pluginOwned = dirsIn(owned, OWNED_LINE)
|
|
92
|
+
|
|
93
|
+
// The generator omits a plugin-claimed dir from the ontology region
|
|
94
|
+
// (account-schema-owned-dirs.py:287), so this should be empty in practice.
|
|
95
|
+
// When it is not, the bucket silently vanishes from the client's view — the
|
|
96
|
+
// caller logs it rather than guessing which side should win.
|
|
97
|
+
const collisions = domainBuckets.filter((d) => pluginOwned.includes(d))
|
|
98
|
+
|
|
99
|
+
const candidates = [ALWAYS_EXPOSED, ...domainBuckets.filter((d) => !collisions.includes(d))]
|
|
100
|
+
const exposed = [...new Set(candidates)].filter((d) => allowed.includes(d)).sort()
|
|
101
|
+
|
|
102
|
+
return { schemaPresent: true, allowed, domainBuckets, pluginOwned, collisions, exposed }
|
|
103
|
+
}
|
|
@@ -67,7 +67,7 @@ describe('sessions', () => {
|
|
|
67
67
|
it('mints a session resolvable to its owner', async () => {
|
|
68
68
|
const { db } = fakeDb()
|
|
69
69
|
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
70
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice' })
|
|
70
|
+
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '' })
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
it('does not resolve an expired session', async () => {
|
|
@@ -97,7 +97,7 @@ describe('session revocation — the property the design claims', () => {
|
|
|
97
97
|
// by construction, not by remembering to delete anything.
|
|
98
98
|
const { db, peopleRows } = fakeDb()
|
|
99
99
|
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
100
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice' })
|
|
100
|
+
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({ ownerId: 'alice', accountId: '' })
|
|
101
101
|
|
|
102
102
|
peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
|
|
103
103
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { createD1Client } from '../../bin/d1-http.mjs'
|
|
3
|
+
|
|
4
|
+
type Init = { method?: string; body?: string; headers?: Record<string, string> }
|
|
5
|
+
type Call = { url: string; init: Init }
|
|
6
|
+
|
|
7
|
+
function fakeFetch(calls: Call[], rows: unknown[] = []) {
|
|
8
|
+
return async (url: string, init: Init) => {
|
|
9
|
+
calls.push({ url, init })
|
|
10
|
+
if (url.includes('?name=')) {
|
|
11
|
+
return { ok: true, status: 200, json: async () => ({ result: [{ name: 'portal-db', uuid: 'db-uuid' }] }) }
|
|
12
|
+
}
|
|
13
|
+
return { ok: true, status: 200, json: async () => ({ result: [{ results: rows }] }) }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const opts = (fetchFn: unknown) => ({
|
|
18
|
+
accountId: 'acc',
|
|
19
|
+
token: 't',
|
|
20
|
+
dbName: 'portal-db',
|
|
21
|
+
fetchFn: fetchFn as never,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('createD1Client', () => {
|
|
25
|
+
it('resolves the database by exact name then queries by uuid', async () => {
|
|
26
|
+
const calls: Call[] = []
|
|
27
|
+
const c = createD1Client(opts(fakeFetch(calls)))
|
|
28
|
+
await c.query('SELECT 1')
|
|
29
|
+
expect(calls[0].url).toContain('/accounts/acc/d1/database?name=portal-db')
|
|
30
|
+
expect(calls[1].url).toContain('/d1/database/db-uuid/query')
|
|
31
|
+
expect(calls[1].init.headers?.Authorization).toBe('Bearer t')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('sends values as params, never interpolated into sql', async () => {
|
|
35
|
+
const calls: Call[] = []
|
|
36
|
+
const c = createD1Client(opts(fakeFetch(calls)))
|
|
37
|
+
await c.query('INSERT INTO directory (relPath) VALUES (?)', ["o'brien.pdf"])
|
|
38
|
+
const body = JSON.parse(calls[1].init.body ?? '{}')
|
|
39
|
+
expect(body.sql).toBe('INSERT INTO directory (relPath) VALUES (?)')
|
|
40
|
+
expect(body.params).toEqual(["o'brien.pdf"])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns the rows of the first result', async () => {
|
|
44
|
+
const c = createD1Client(opts(fakeFetch([], [{ n: 3 }])))
|
|
45
|
+
expect(await c.query('SELECT COUNT(*) AS n FROM directory')).toEqual([{ n: 3 }])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('caches the name lookup across statements', async () => {
|
|
49
|
+
const calls: Call[] = []
|
|
50
|
+
const c = createD1Client(opts(fakeFetch(calls)))
|
|
51
|
+
await c.query('SELECT 1')
|
|
52
|
+
await c.query('SELECT 2')
|
|
53
|
+
expect(calls.filter((x) => x.url.includes('?name=')).length).toBe(1)
|
|
54
|
+
expect(c.statements).toBe(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws carrying the response body when the api fails', async () => {
|
|
58
|
+
// Never discard an error body: a bare status turns every failure into the
|
|
59
|
+
// same unactionable line.
|
|
60
|
+
const fetchFn = async () => ({
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 403,
|
|
63
|
+
json: async () => ({ errors: [{ message: 'insufficient permissions' }] }),
|
|
64
|
+
})
|
|
65
|
+
const c = createD1Client(opts(fetchFn))
|
|
66
|
+
await expect(c.query('SELECT 1')).rejects.toThrow(/403.*insufficient permissions/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('refuses a database whose name only nearly matches', async () => {
|
|
70
|
+
// `?name=` is a filter whose matching rule is not documented, so taking the
|
|
71
|
+
// first result on trust could run this account's statements against a
|
|
72
|
+
// near-named database.
|
|
73
|
+
const fetchFn = async (url: string) => ({
|
|
74
|
+
ok: true,
|
|
75
|
+
status: 200,
|
|
76
|
+
json: async () =>
|
|
77
|
+
url.includes('?name=') ? { result: [{ name: 'portal-db-old', uuid: 'x' }] } : { result: [] },
|
|
78
|
+
})
|
|
79
|
+
const c = createD1Client(opts(fetchFn))
|
|
80
|
+
await expect(c.query('SELECT 1')).rejects.toThrow(/no D1 database named/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('counts a statement that was issued and then errored', async () => {
|
|
84
|
+
let n = 0
|
|
85
|
+
const fetchFn = async (url: string) => {
|
|
86
|
+
n++
|
|
87
|
+
if (url.includes('?name=')) {
|
|
88
|
+
return { ok: true, status: 200, json: async () => ({ result: [{ name: 'portal-db', uuid: 'u' }] }) }
|
|
89
|
+
}
|
|
90
|
+
return { ok: false, status: 500, json: async () => ({ errors: [{ message: 'boom' }] }) }
|
|
91
|
+
}
|
|
92
|
+
const c = createD1Client(opts(fetchFn))
|
|
93
|
+
await expect(c.query('SELECT 1')).rejects.toThrow()
|
|
94
|
+
expect(c.statements).toBe(1)
|
|
95
|
+
})
|
|
96
|
+
})
|