@rubytech/create-sitedesk-code 0.1.519 → 0.1.521
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-28-task-2097-email-signature-set.md +65 -0
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +20 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +46 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +56 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +111 -4
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +15 -3
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
- package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
- package/payload/platform/scripts/check-architecture-skill-no-drift.mjs +20 -6
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +17 -5
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +30 -6
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +53 -21
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +16 -0
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +41 -2
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-TIIWMF5G.js → chunk-AZLT4LVO.js} +40 -7
- package/payload/server/{manager-B6OFOXSW.js → manager-CQBSUJXR.js} +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/AdminLoginScreens-Dar_MMJm.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-Dar_MMJm.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Dar_MMJm.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-CpaI2F_p.js +2 -0
- package/payload/server/public/assets/AdminShell-CpaI2F_p.js.br +0 -0
- package/payload/server/public/assets/AdminShell-CpaI2F_p.js.gz +0 -0
- package/payload/server/public/assets/{activity-DOsfKzT3.js → activity-B1rt4GIy.js} +1 -1
- package/payload/server/public/assets/activity-B1rt4GIy.js.br +0 -0
- package/payload/server/public/assets/activity-B1rt4GIy.js.gz +0 -0
- package/payload/server/public/assets/admin-DE3lLNje.js +1 -0
- package/payload/server/public/assets/admin-DE3lLNje.js.br +0 -0
- package/payload/server/public/assets/admin-DE3lLNje.js.gz +0 -0
- package/payload/server/public/assets/{agents-DqDnZTH_.js → agents-BXq8woVf.js} +1 -1
- package/payload/server/public/assets/agents-BXq8woVf.js.br +0 -0
- package/payload/server/public/assets/agents-BXq8woVf.js.gz +0 -0
- package/payload/server/public/assets/{browser-FL17Dfp0.js → browser-1QSCPl3X.js} +1 -1
- package/payload/server/public/assets/browser-1QSCPl3X.js.br +0 -0
- package/payload/server/public/assets/browser-1QSCPl3X.js.gz +0 -0
- package/payload/server/public/assets/{calendar-CLfLvsnV.js → calendar-D009qWVV.js} +1 -1
- package/payload/server/public/assets/calendar-D009qWVV.js.br +0 -0
- package/payload/server/public/assets/calendar-D009qWVV.js.gz +0 -0
- package/payload/server/public/assets/chat-CUg1fTqJ.js +1 -0
- package/payload/server/public/assets/chat-CUg1fTqJ.js.br +1 -0
- package/payload/server/public/assets/chat-CUg1fTqJ.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-C2JEy7uU.js +1 -0
- package/payload/server/public/assets/chevron-left-C2JEy7uU.js.br +0 -0
- package/payload/server/public/assets/chevron-right-DWa047Ys.js +1 -0
- package/payload/server/public/assets/chevron-right-DWa047Ys.js.br +0 -0
- package/payload/server/public/assets/clock-C3OqfCnn.js +1 -0
- package/payload/server/public/assets/clock-C3OqfCnn.js.br +0 -0
- package/payload/server/public/assets/clock-C3OqfCnn.js.gz +0 -0
- package/payload/server/public/assets/data-Bu_yCScC.js +1 -0
- package/payload/server/public/assets/data-Bu_yCScC.js.br +0 -0
- package/payload/server/public/assets/data-Bu_yCScC.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DNm0AzUz.js → file-text-DwGAfCsd.js} +1 -1
- package/payload/server/public/assets/file-text-DwGAfCsd.js.br +0 -0
- package/payload/server/public/assets/file-text-DwGAfCsd.js.gz +0 -0
- package/payload/server/public/assets/{graph-fAvmlVzh.js → graph-D-DGlfhW.js} +2 -2
- package/payload/server/public/assets/graph-D-DGlfhW.js.br +0 -0
- package/payload/server/public/assets/graph-D-DGlfhW.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-CwdkCxfs.js → graph-labels-BbX3fXzl.js} +1 -1
- package/payload/server/public/assets/graph-labels-BbX3fXzl.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BbX3fXzl.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-B6tAPhC2.js → maximize-2-C_-KapxH.js} +1 -1
- package/payload/server/public/assets/maximize-2-C_-KapxH.js.br +0 -0
- package/payload/server/public/assets/maximize-2-C_-KapxH.js.gz +0 -0
- package/payload/server/public/assets/{operator-CTKvBVjk.js → operator-CFrZVcv6.js} +1 -1
- package/payload/server/public/assets/operator-CFrZVcv6.js.br +0 -0
- package/payload/server/public/assets/operator-CFrZVcv6.js.gz +0 -0
- package/payload/server/public/assets/{page-O5HkHQw4.js → page-CHt62f1_.js} +1 -1
- package/payload/server/public/assets/page-CHt62f1_.js.br +0 -0
- package/payload/server/public/assets/page-CHt62f1_.js.gz +0 -0
- package/payload/server/public/assets/page-E74MYB18.js +32 -0
- package/payload/server/public/assets/page-E74MYB18.js.br +0 -0
- package/payload/server/public/assets/page-E74MYB18.js.gz +0 -0
- package/payload/server/public/assets/{public-BF1rnVPa.js → public-CI6K1sNW.js} +1 -1
- package/payload/server/public/assets/public-CI6K1sNW.js.br +0 -0
- package/payload/server/public/assets/public-CI6K1sNW.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-B8TqfCy4.js → rotate-ccw-PWu_QGrD.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-PWu_QGrD.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-PWu_QGrD.js.gz +0 -0
- package/payload/server/public/assets/routines-Cp3_XS7S.js +1 -0
- package/payload/server/public/assets/routines-Cp3_XS7S.js.br +0 -0
- package/payload/server/public/assets/routines-Cp3_XS7S.js.gz +0 -0
- package/payload/server/public/assets/skills-g8HvunPA.js +1 -0
- package/payload/server/public/assets/skills-g8HvunPA.js.br +0 -0
- package/payload/server/public/assets/skills-g8HvunPA.js.gz +0 -0
- package/payload/server/public/assets/{tasks-DePyOrRk.js → tasks-CMC_jDVm.js} +1 -1
- package/payload/server/public/assets/tasks-CMC_jDVm.js.br +0 -0
- package/payload/server/public/assets/tasks-CMC_jDVm.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-D9gtnG01.js → time-entry-format-BXWuQ6Sw.js} +1 -1
- package/payload/server/public/assets/time-entry-format-BXWuQ6Sw.js.br +2 -0
- package/payload/server/public/assets/time-entry-format-BXWuQ6Sw.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-DAtm2mLL.js → triangle-alert-BNshRC13.js} +1 -1
- package/payload/server/public/assets/triangle-alert-BNshRC13.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-BNshRC13.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-uMJT-p6i.js → useCopyFeedback-CwTZ9DqH.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-CwTZ9DqH.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-CwTZ9DqH.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js +14 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-BFrPC-s7.js → useVoiceRecorder-BGqV5R6u.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-BGqV5R6u.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BGqV5R6u.js.gz +0 -0
- package/payload/server/public/assets/{wrench-CYgaN7K9.js → wrench-DQ045Pyk.js} +1 -1
- package/payload/server/public/assets/wrench-DQ045Pyk.js.br +0 -0
- package/payload/server/public/assets/wrench-DQ045Pyk.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +687 -400
- package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js +0 -1
- package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js +0 -2
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.gz +0 -0
- package/payload/server/public/assets/activity-DOsfKzT3.js.br +0 -0
- package/payload/server/public/assets/activity-DOsfKzT3.js.gz +0 -0
- package/payload/server/public/assets/admin-COgD1z5a.js +0 -1
- package/payload/server/public/assets/admin-COgD1z5a.js.br +0 -0
- package/payload/server/public/assets/admin-COgD1z5a.js.gz +0 -0
- package/payload/server/public/assets/agents-DqDnZTH_.js.br +0 -0
- package/payload/server/public/assets/agents-DqDnZTH_.js.gz +0 -0
- package/payload/server/public/assets/browser-FL17Dfp0.js.br +0 -0
- package/payload/server/public/assets/browser-FL17Dfp0.js.gz +0 -0
- package/payload/server/public/assets/calendar-CLfLvsnV.js.br +0 -0
- package/payload/server/public/assets/calendar-CLfLvsnV.js.gz +0 -0
- package/payload/server/public/assets/chat-DYq4FxMG.js +0 -1
- package/payload/server/public/assets/chat-DYq4FxMG.js.br +0 -0
- package/payload/server/public/assets/chat-DYq4FxMG.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DWt5cdUo.js +0 -1
- package/payload/server/public/assets/chevron-right-B9suVrYy.js +0 -1
- package/payload/server/public/assets/chevron-right-B9suVrYy.js.br +0 -0
- package/payload/server/public/assets/clock-DW6qVXtv.js +0 -1
- package/payload/server/public/assets/clock-DW6qVXtv.js.br +0 -0
- package/payload/server/public/assets/clock-DW6qVXtv.js.gz +0 -0
- package/payload/server/public/assets/data-C8rbOgds.js +0 -1
- package/payload/server/public/assets/data-C8rbOgds.js.br +0 -0
- package/payload/server/public/assets/data-C8rbOgds.js.gz +0 -0
- package/payload/server/public/assets/file-text-DNm0AzUz.js.br +0 -0
- package/payload/server/public/assets/file-text-DNm0AzUz.js.gz +0 -0
- package/payload/server/public/assets/graph-fAvmlVzh.js.br +0 -0
- package/payload/server/public/assets/graph-fAvmlVzh.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-CwdkCxfs.js.br +0 -0
- package/payload/server/public/assets/graph-labels-CwdkCxfs.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-B6tAPhC2.js.br +0 -0
- package/payload/server/public/assets/maximize-2-B6tAPhC2.js.gz +0 -0
- package/payload/server/public/assets/operator-CTKvBVjk.js.br +0 -0
- package/payload/server/public/assets/operator-CTKvBVjk.js.gz +0 -0
- package/payload/server/public/assets/page-BWoNW3sa.js +0 -32
- package/payload/server/public/assets/page-BWoNW3sa.js.br +0 -0
- package/payload/server/public/assets/page-BWoNW3sa.js.gz +0 -0
- package/payload/server/public/assets/page-O5HkHQw4.js.br +0 -0
- package/payload/server/public/assets/page-O5HkHQw4.js.gz +0 -0
- package/payload/server/public/assets/public-BF1rnVPa.js.br +0 -0
- package/payload/server/public/assets/public-BF1rnVPa.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.gz +0 -0
- package/payload/server/public/assets/routines-4R7BLZyH.js +0 -1
- package/payload/server/public/assets/routines-4R7BLZyH.js.br +0 -0
- package/payload/server/public/assets/routines-4R7BLZyH.js.gz +0 -0
- package/payload/server/public/assets/skills-CQ9Qs2tg.js +0 -1
- package/payload/server/public/assets/skills-CQ9Qs2tg.js.br +0 -0
- package/payload/server/public/assets/skills-CQ9Qs2tg.js.gz +0 -0
- package/payload/server/public/assets/tasks-DePyOrRk.js.br +0 -0
- package/payload/server/public/assets/tasks-DePyOrRk.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-D9gtnG01.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-D9gtnG01.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js +0 -14
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.gz +0 -0
- package/payload/server/public/assets/wrench-CYgaN7K9.js.br +0 -1
- package/payload/server/public/assets/wrench-CYgaN7K9.js.gz +0 -0
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
-- D1 schema for the data portal (data-portal skill).
|
|
2
2
|
--
|
|
3
|
-
--
|
|
4
|
-
-- what they have dropped (manifest),
|
|
5
|
-
-- (auth_attempts)
|
|
3
|
+
-- Six tables: who may authenticate (people), who currently is (sessions),
|
|
4
|
+
-- what they have dropped (manifest), how hard they have been trying
|
|
5
|
+
-- (auth_attempts), and what the device has published (directory,
|
|
6
|
+
-- directory_state). Everything is IF NOT EXISTS so re-applying is safe.
|
|
7
|
+
--
|
|
8
|
+
-- This schema is applied to the ONE shared, house-owned database that backs
|
|
9
|
+
-- every portal (Task 2099). `accountId` is therefore the tenant discriminator,
|
|
10
|
+
-- not a hint: it appears on every table carrying client data, the deployed
|
|
11
|
+
-- worker binds its own account into every query from the wrangler var
|
|
12
|
+
-- PORTAL_ACCOUNT_ID, and nothing else separates one client's rows from
|
|
13
|
+
-- another's. `auth_attempts` is the one exception and needs no column, because
|
|
14
|
+
-- the tenant rides inside its `scope` string (see _lib/ratelimit.ts).
|
|
15
|
+
--
|
|
16
|
+
-- A portal provisioned before 2099 has its OWN database with the older shape
|
|
17
|
+
-- and is never migrated in place. IF NOT EXISTS adds no column to an existing
|
|
18
|
+
-- table, so applying this file to such a database silently leaves it as it was.
|
|
19
|
+
-- That is exactly why the shared store must be a freshly created database, and
|
|
20
|
+
-- why those portals stay on their own template until migrated deliberately.
|
|
6
21
|
|
|
7
22
|
-- One row per enrolled person. No plaintext passcode is ever stored: the
|
|
8
23
|
-- enrolment script generates the passcode, prints it to the operator once, and
|
|
@@ -32,14 +47,20 @@
|
|
|
32
47
|
-- to a `people` table that predates it, and enrolment's INSERT names `folders`,
|
|
33
48
|
-- so it fails loudly on such a table. Add it by hand in that case:
|
|
34
49
|
-- ALTER TABLE people ADD COLUMN folders TEXT NOT NULL DEFAULT '';
|
|
50
|
+
-- The key is (accountId, ownerId), not ownerId alone. On the shared store two
|
|
51
|
+
-- clients may each enrol a person called `rob`, and a bare ownerId key would
|
|
52
|
+
-- reject the second enrolment outright. Worse, the enrolment upsert would treat
|
|
53
|
+
-- it as a rotation and overwrite the first client's person: their passcode,
|
|
54
|
+
-- their folder grant, and the owner of their files.
|
|
35
55
|
CREATE TABLE IF NOT EXISTS people (
|
|
36
|
-
ownerId TEXT
|
|
56
|
+
ownerId TEXT NOT NULL,
|
|
37
57
|
name TEXT NOT NULL,
|
|
38
58
|
salt TEXT NOT NULL,
|
|
39
59
|
hash TEXT NOT NULL,
|
|
40
60
|
createdAt TEXT NOT NULL,
|
|
41
61
|
accountId TEXT NOT NULL DEFAULT '',
|
|
42
|
-
folders TEXT NOT NULL DEFAULT ''
|
|
62
|
+
folders TEXT NOT NULL DEFAULT '',
|
|
63
|
+
PRIMARY KEY (accountId, ownerId)
|
|
43
64
|
);
|
|
44
65
|
|
|
45
66
|
-- `pcCheck` is a short digest of the person's passcode hash at mint time, and
|
|
@@ -48,9 +69,16 @@ CREATE TABLE IF NOT EXISTS people (
|
|
|
48
69
|
-- every session minted against the old one stops resolving immediately, with
|
|
49
70
|
-- no cleanup step for an operator to remember and no 12-hour window to wait
|
|
50
71
|
-- out. A leaked passcode is exactly the case where a procedural remedy fails.
|
|
72
|
+
--
|
|
73
|
+
-- `accountId` records which portal minted the session. `sessionId` is a global
|
|
74
|
+
-- UUID and is safe as the key on its own, but the resolve joins `people` and
|
|
75
|
+
-- that join must match on both columns: keyed on ownerId alone it can pair a
|
|
76
|
+
-- session with a same-named person belonging to a different client, and hand
|
|
77
|
+
-- back that client's folder grant.
|
|
51
78
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
52
79
|
sessionId TEXT PRIMARY KEY,
|
|
53
80
|
ownerId TEXT NOT NULL,
|
|
81
|
+
accountId TEXT NOT NULL DEFAULT '',
|
|
54
82
|
pcCheck TEXT NOT NULL,
|
|
55
83
|
expiresAt INTEGER NOT NULL,
|
|
56
84
|
createdAt TEXT NOT NULL
|
|
@@ -92,22 +120,34 @@ CREATE INDEX IF NOT EXISTS sessions_owner ON sessions (ownerId);
|
|
|
92
120
|
-- ALTER TABLE manifest ADD COLUMN devicePath TEXT NOT NULL DEFAULT '';
|
|
93
121
|
-- ALTER TABLE manifest ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
|
|
94
122
|
-- Their absence is loud: upload names targetPath in its INSERT.
|
|
123
|
+
-- `accountId` is the tenant the row belongs to, and `objectKey` is unique only
|
|
124
|
+
-- within it. A bare `objectKey UNIQUE` is not merely too broad on a shared
|
|
125
|
+
-- store, it is exploitable: the upload upsert conflicts on that key, so one
|
|
126
|
+
-- client uploading at another client's key would take over the existing row,
|
|
127
|
+
-- reset its ingested flag, and redirect the file. The pull loop also filters on
|
|
128
|
+
-- this column, which is what stops one account's sweep collecting another's
|
|
129
|
+
-- pending uploads into its own folder tree.
|
|
95
130
|
CREATE TABLE IF NOT EXISTS manifest (
|
|
96
131
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
132
|
fileId TEXT UNIQUE NOT NULL,
|
|
133
|
+
accountId TEXT NOT NULL DEFAULT '',
|
|
98
134
|
ownerId TEXT NOT NULL,
|
|
99
135
|
filename TEXT NOT NULL,
|
|
100
|
-
objectKey TEXT
|
|
136
|
+
objectKey TEXT NOT NULL,
|
|
101
137
|
size INTEGER NOT NULL,
|
|
102
138
|
uploadedAt TEXT NOT NULL,
|
|
103
139
|
ingested INTEGER NOT NULL DEFAULT 0,
|
|
104
140
|
targetPath TEXT NOT NULL DEFAULT '',
|
|
105
141
|
devicePath TEXT NOT NULL DEFAULT '',
|
|
106
|
-
deleted INTEGER NOT NULL DEFAULT 0
|
|
142
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
143
|
+
UNIQUE (accountId, objectKey)
|
|
107
144
|
);
|
|
108
145
|
|
|
109
146
|
CREATE INDEX IF NOT EXISTS manifest_uningested ON manifest (ingested);
|
|
110
147
|
CREATE INDEX IF NOT EXISTS manifest_owner ON manifest (ownerId);
|
|
148
|
+
-- The pull and the audit both filter by account on the shared store, so the
|
|
149
|
+
-- discriminator carries its own index rather than riding the owner one.
|
|
150
|
+
CREATE INDEX IF NOT EXISTS manifest_account ON manifest (accountId);
|
|
111
151
|
-- The pull selects `ingested = 0 OR deleted = 1`, so the withdrawal half needs
|
|
112
152
|
-- its own index or a withdrawal costs a full scan every sixty seconds.
|
|
113
153
|
CREATE INDEX IF NOT EXISTS manifest_deleted ON manifest (deleted);
|
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
// The isolation rule.
|
|
2
2
|
//
|
|
3
|
-
// Every file op authorizes owner-vs-key here BEFORE any R2 call. The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Every file op authorizes tenant-and-owner-vs-key here BEFORE any R2 call. The
|
|
4
|
+
// key prefix is a consequence of this check, never the check itself: a caller
|
|
5
|
+
// could otherwise reach another person's files by naming them, and prefix
|
|
6
|
+
// convention alone would not stop it.
|
|
7
|
+
//
|
|
8
|
+
// There are TWO segments to check, not one (Task 2099). Every portal binds the
|
|
9
|
+
// same shared bucket, so the account segment is the only thing separating one
|
|
10
|
+
// client's objects from another's. Checking the owner segment alone would leave
|
|
11
|
+
// `<other-account>/alice/x.pdf` authorized for this portal's alice, and the
|
|
12
|
+
// delete, download and multipart-complete paths all take the key straight from
|
|
13
|
+
// the client, so this is verified on every call rather than assumed from how
|
|
14
|
+
// the key was built.
|
|
7
15
|
|
|
8
16
|
const SEP = '/'
|
|
9
17
|
|
|
10
|
-
// The whole gate rests on
|
|
11
|
-
//
|
|
18
|
+
// The whole gate rests on ids that cannot contain the separator. If two people
|
|
19
|
+
// were ever enrolled as 'alice' and 'alice/2024', then
|
|
12
20
|
// 'alice/2024/invoice.pdf' starts with 'alice/' and alice would read the other
|
|
13
|
-
// person's files
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
21
|
+
// person's files; the same argument applies to the account segment. Enrolment
|
|
22
|
+
// and assembly are agents writing values, so this invariant cannot live only in
|
|
23
|
+
// the skill's prose. It is enforced here, where the decision is actually made,
|
|
24
|
+
// and it fails closed.
|
|
25
|
+
const SAFE_ID = /^[a-z0-9-]+$/
|
|
17
26
|
|
|
18
|
-
export function ownerPrefix(ownerId: string): string {
|
|
19
|
-
return `${ownerId}${SEP}`
|
|
27
|
+
export function ownerPrefix(accountId: string, ownerId: string): string {
|
|
28
|
+
return `${accountId}${SEP}${ownerId}${SEP}`
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
export function authorizeKey(ownerId: string, key: string): boolean {
|
|
23
|
-
if (!ownerId || !key) return false
|
|
24
|
-
if (!
|
|
31
|
+
export function authorizeKey(accountId: string, ownerId: string, key: string): boolean {
|
|
32
|
+
if (!accountId || !ownerId || !key) return false
|
|
33
|
+
if (!SAFE_ID.test(accountId) || !SAFE_ID.test(ownerId)) return false
|
|
25
34
|
// Reject traversal and absolute forms outright rather than normalising them:
|
|
26
35
|
// a normaliser is one more thing that can be wrong, and no legitimate key
|
|
27
36
|
// contains these.
|
|
28
37
|
if (key.includes('..') || key.startsWith(SEP)) return false
|
|
29
|
-
// startsWith
|
|
30
|
-
//
|
|
31
|
-
return key.startsWith(ownerPrefix(ownerId))
|
|
38
|
+
// startsWith on the bare ids would authorize 'acct-a-evil/...' for 'acct-a'
|
|
39
|
+
// and 'alice-evil/...' for 'alice'. Both separators are part of the rule.
|
|
40
|
+
return key.startsWith(ownerPrefix(accountId, ownerId))
|
|
32
41
|
}
|
|
@@ -19,9 +19,16 @@ export const RATE_MAX_ATTEMPTS = 5
|
|
|
19
19
|
* unauthenticated requests naming a person lock that person out of their own
|
|
20
20
|
* portal for the window, from anywhere, repeatably. Keyed on owner+IP, an
|
|
21
21
|
* attacker throttles only themselves. The cost is that a distributed guesser
|
|
22
|
-
* gets RATE_MAX_ATTEMPTS per address, which the passcodes' entropy carries.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
* gets RATE_MAX_ATTEMPTS per address, which the passcodes' entropy carries.
|
|
23
|
+
*
|
|
24
|
+
* The account leads the key because the `auth_attempts` table is shared by
|
|
25
|
+
* every portal (Task 2099). Two clients who each enrol a person of the same
|
|
26
|
+
* name would otherwise share one lockout counter, so traffic against one
|
|
27
|
+
* client's portal could lock a different client's person out of theirs. This
|
|
28
|
+
* is why the table itself needs no accountId column: the tenant is already
|
|
29
|
+
* inside `scope`, and the unique index is on (scope, windowStart). */
|
|
30
|
+
export function rateScope(accountId: string, ownerId: string, clientIp: string): string {
|
|
31
|
+
return `${accountId}|${ownerId}|${clientIp}`
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** Clear a scope's counter. Called on a successful sign-in, so a person doing
|
|
@@ -31,21 +31,31 @@ export function passcodeCheck(hash: string): string {
|
|
|
31
31
|
export async function mintSession(
|
|
32
32
|
db: D1Database,
|
|
33
33
|
ownerId: string,
|
|
34
|
+
accountId: string,
|
|
34
35
|
sessionId: string,
|
|
35
36
|
nowMs: number,
|
|
36
37
|
pcCheck: string,
|
|
37
38
|
): Promise<string> {
|
|
38
39
|
await db
|
|
39
40
|
.prepare(
|
|
40
|
-
'INSERT INTO sessions (sessionId, ownerId, pcCheck, expiresAt, createdAt) VALUES (?, ?, ?, ?, ?)',
|
|
41
|
+
'INSERT INTO sessions (sessionId, ownerId, accountId, pcCheck, expiresAt, createdAt) VALUES (?, ?, ?, ?, ?, ?)',
|
|
42
|
+
)
|
|
43
|
+
.bind(
|
|
44
|
+
sessionId,
|
|
45
|
+
ownerId,
|
|
46
|
+
accountId,
|
|
47
|
+
pcCheck,
|
|
48
|
+
nowMs + SESSION_TTL_MS,
|
|
49
|
+
new Date(nowMs).toISOString(),
|
|
41
50
|
)
|
|
42
|
-
.bind(sessionId, ownerId, pcCheck, nowMs + SESSION_TTL_MS, new Date(nowMs).toISOString())
|
|
43
51
|
.run()
|
|
44
52
|
// Opportunistic GC: expired rows are dead weight and nothing else prunes
|
|
45
|
-
// them. Scoped to this owner so it stays a small, indexed delete
|
|
53
|
+
// them. Scoped to this owner so it stays a small, indexed delete, and to this
|
|
54
|
+
// account because the store is shared: two portals may hold the same owner
|
|
55
|
+
// id, and an owner-only delete would prune the other portal's rows.
|
|
46
56
|
await db
|
|
47
|
-
.prepare('DELETE FROM sessions WHERE ownerId = ? AND expiresAt <= ?')
|
|
48
|
-
.bind(ownerId, nowMs)
|
|
57
|
+
.prepare('DELETE FROM sessions WHERE ownerId = ? AND accountId = ? AND expiresAt <= ?')
|
|
58
|
+
.bind(ownerId, accountId, nowMs)
|
|
49
59
|
.run()
|
|
50
60
|
return sessionId
|
|
51
61
|
}
|
|
@@ -53,6 +63,7 @@ export async function mintSession(
|
|
|
53
63
|
export async function resolveSession(
|
|
54
64
|
db: D1Database,
|
|
55
65
|
sessionId: string,
|
|
66
|
+
accountId: string,
|
|
56
67
|
nowMs: number,
|
|
57
68
|
): Promise<{ ownerId: string; accountId: string; folders: string[] } | null> {
|
|
58
69
|
if (!sessionId) return null
|
|
@@ -60,18 +71,23 @@ export async function resolveSession(
|
|
|
60
71
|
.prepare(
|
|
61
72
|
`SELECT s.ownerId AS ownerId, p.accountId AS accountId, p.folders AS folders
|
|
62
73
|
FROM sessions s
|
|
63
|
-
JOIN people p ON p.ownerId = s.ownerId
|
|
74
|
+
JOIN people p ON p.ownerId = s.ownerId AND p.accountId = s.accountId
|
|
64
75
|
WHERE s.sessionId = ?
|
|
76
|
+
AND s.accountId = ?
|
|
65
77
|
AND s.expiresAt > ?
|
|
66
78
|
AND s.pcCheck = substr(p.hash, 1, 16)`,
|
|
67
79
|
)
|
|
68
|
-
.bind(sessionId, nowMs)
|
|
80
|
+
.bind(sessionId, accountId, nowMs)
|
|
69
81
|
.first<{ ownerId: string; accountId: string; folders: string }>()
|
|
70
82
|
if (!row || typeof row.ownerId !== 'string') return null
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
83
|
+
// The join matches on BOTH columns and the lookup is already scoped to this
|
|
84
|
+
// portal's own account, which is what stops a same-named person in another
|
|
85
|
+
// tenant resolving here. A row that still disagrees is a contradiction, and
|
|
86
|
+
// on a shared store the safe reading of a contradiction is nothing at all.
|
|
87
|
+
// This also fails closed on a legacy row carrying no accountId, rather than
|
|
88
|
+
// treating an absent tenant as a match.
|
|
89
|
+
const resolved = typeof row.accountId === 'string' ? row.accountId : ''
|
|
90
|
+
if (!resolved || resolved !== accountId) return null
|
|
75
91
|
// `folders` is a comma-separated allowlist of top-level names. Absent or empty
|
|
76
92
|
// means no grant, which the two client surfaces read as no filter (full set).
|
|
77
93
|
const folders =
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts
CHANGED
|
@@ -76,16 +76,25 @@ export async function checkTarget(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* `ownerId/targetPath/filename`, collapsing to
|
|
80
|
-
* untargeted.
|
|
79
|
+
* `accountId/ownerId/targetPath/filename`, collapsing to
|
|
80
|
+
* `accountId/ownerId/filename` when untargeted.
|
|
81
81
|
*
|
|
82
|
-
* The
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* The account leads the key because the bucket is shared by every portal (Task
|
|
83
|
+
* 2099): without it, two clients who each enrol a person of the same name would
|
|
84
|
+
* write to the same object path.
|
|
85
|
+
*
|
|
86
|
+
* The target is IN the key because `objectKey` is unique per account. With a
|
|
87
|
+
* flat key the same filename dropped into two folders would collide: the upsert
|
|
88
|
+
* would overwrite the first object, reset its ingested flag, and the first file
|
|
89
|
+
* would never reach the device at all.
|
|
86
90
|
*/
|
|
87
|
-
export function uploadKey(
|
|
91
|
+
export function uploadKey(
|
|
92
|
+
accountId: string,
|
|
93
|
+
ownerId: string,
|
|
94
|
+
targetPath: string,
|
|
95
|
+
filename: string,
|
|
96
|
+
): string {
|
|
88
97
|
return targetPath
|
|
89
|
-
? `${ownerPrefix(ownerId)}${targetPath}/${filename}`
|
|
90
|
-
: `${ownerPrefix(ownerId)}${filename}`
|
|
98
|
+
? `${ownerPrefix(accountId, ownerId)}${targetPath}/${filename}`
|
|
99
|
+
: `${ownerPrefix(accountId, ownerId)}${filename}`
|
|
91
100
|
}
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts
CHANGED
|
@@ -57,6 +57,22 @@ export interface R2Bucket {
|
|
|
57
57
|
export interface PortalEnv {
|
|
58
58
|
DB: D1Database
|
|
59
59
|
BUCKET: R2Bucket
|
|
60
|
+
/**
|
|
61
|
+
* This portal's own account id, stamped into wrangler.toml [vars] at assemble
|
|
62
|
+
* time by bin/portal-assemble.mjs.
|
|
63
|
+
*
|
|
64
|
+
* It is the tenant scope for every query and every object key. DB and BUCKET
|
|
65
|
+
* above are the ONE shared store that backs every portal (Task 2099), so this
|
|
66
|
+
* value is the only thing separating this client's rows and objects from
|
|
67
|
+
* another client's.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately NOT taken from the session or from `people.accountId`: the
|
|
70
|
+
* people join is by ownerId, two clients may each enrol a person of the same
|
|
71
|
+
* name, and a tenant scope derived from the request would then resolve to
|
|
72
|
+
* whichever row happened to match. It is baked in at deploy time precisely so
|
|
73
|
+
* it cannot be influenced by a request.
|
|
74
|
+
*/
|
|
75
|
+
PORTAL_ACCOUNT_ID: string
|
|
60
76
|
/**
|
|
61
77
|
* Shared with the install; signs download links for device-held files. A
|
|
62
78
|
* Pages SECRET, never a var in wrangler.toml, and never sent to the client.
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts
CHANGED
|
@@ -53,15 +53,20 @@ export async function processAuth(
|
|
|
53
53
|
// owner: keyed on the owner alone, the counter is a weapon — six requests
|
|
54
54
|
// naming a person lock that person out of their own portal for the window,
|
|
55
55
|
// from anywhere, unauthenticated.
|
|
56
|
-
const scope = rateScope(ownerId, clientIp)
|
|
56
|
+
const scope = rateScope(env.PORTAL_ACCOUNT_ID, ownerId, clientIp)
|
|
57
57
|
const rate = await checkAndRecordAttempt(env.DB, scope, nowMs)
|
|
58
58
|
if (!rate.allowed) {
|
|
59
59
|
log(`[data-portal] op=auth owner=${q(ownerId)} result=rate-limited attempts=${rate.count}`)
|
|
60
60
|
return { status: 429, payload: { ok: false, error: 'too many attempts' } }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Scoped to this portal's own account: the people table is shared by every
|
|
64
|
+
// portal, so an ownerId-only lookup would find a same-named person belonging
|
|
65
|
+
// to another client and check the passcode against THEIR hash.
|
|
66
|
+
const person = await env.DB.prepare(
|
|
67
|
+
'SELECT ownerId, salt, hash FROM people WHERE ownerId = ? AND accountId = ?',
|
|
68
|
+
)
|
|
69
|
+
.bind(ownerId, env.PORTAL_ACCOUNT_ID)
|
|
65
70
|
.first<{ ownerId: string; salt: string; hash: string }>()
|
|
66
71
|
|
|
67
72
|
if (!person) {
|
|
@@ -82,7 +87,14 @@ export async function processAuth(
|
|
|
82
87
|
await clearAttempts(env.DB, scope)
|
|
83
88
|
|
|
84
89
|
const sessionId = newId()
|
|
85
|
-
await mintSession(
|
|
90
|
+
await mintSession(
|
|
91
|
+
env.DB,
|
|
92
|
+
ownerId,
|
|
93
|
+
env.PORTAL_ACCOUNT_ID,
|
|
94
|
+
sessionId,
|
|
95
|
+
nowMs,
|
|
96
|
+
passcodeCheck(person.hash),
|
|
97
|
+
)
|
|
86
98
|
log(`[data-portal] op=auth owner=${q(ownerId)} result=ok`)
|
|
87
99
|
return { status: 200, payload: { ok: true }, sessionId }
|
|
88
100
|
}
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts
CHANGED
|
@@ -11,21 +11,27 @@ export async function processDelete(
|
|
|
11
11
|
now: () => number,
|
|
12
12
|
): Promise<Handler> {
|
|
13
13
|
const startedMs = now()
|
|
14
|
-
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
14
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
|
|
15
15
|
if (!session) {
|
|
16
16
|
log('[data-portal] op=delete owner=none result=denied')
|
|
17
17
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// The key arrives from the client, so the account segment is checked here and
|
|
20
|
+
// never inferred: the bucket is shared, and without it a caller could name
|
|
21
|
+
// another tenant's object outright.
|
|
22
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, session.ownerId, key)) {
|
|
20
23
|
log(`[data-portal] op=delete owner=${q(session.ownerId)} result=denied reason=not-owner`)
|
|
21
24
|
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
22
25
|
}
|
|
23
26
|
const owner = q(session.ownerId)
|
|
24
27
|
|
|
25
28
|
// What the client is asking to remove decides HOW it is removed, so the row
|
|
26
|
-
// is read before anything is written (Task 1910).
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// is read before anything is written (Task 1910). Every manifest statement
|
|
30
|
+
// below carries the account: objectKey is unique per account, not globally.
|
|
31
|
+
const row = await env.DB.prepare(
|
|
32
|
+
'SELECT devicePath FROM manifest WHERE objectKey = ? AND accountId = ?',
|
|
33
|
+
)
|
|
34
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
29
35
|
.first<{ devicePath: string }>()
|
|
30
36
|
if (!row) {
|
|
31
37
|
log(`[data-portal] op=delete owner=${owner} key=${q(key)} result=absent`)
|
|
@@ -43,7 +49,11 @@ export async function processDelete(
|
|
|
43
49
|
// minute and saying otherwise would be a lie the next refresh exposes.
|
|
44
50
|
if (row.devicePath) {
|
|
45
51
|
try {
|
|
46
|
-
await env.DB.prepare(
|
|
52
|
+
await env.DB.prepare(
|
|
53
|
+
'UPDATE manifest SET deleted = 1 WHERE objectKey = ? AND accountId = ?',
|
|
54
|
+
)
|
|
55
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
56
|
+
.run()
|
|
47
57
|
} catch (err) {
|
|
48
58
|
log(
|
|
49
59
|
`[data-portal] op=withdraw owner=${owner} key=${q(key)} ` +
|
|
@@ -53,8 +63,10 @@ export async function processDelete(
|
|
|
53
63
|
}
|
|
54
64
|
// Re-read rather than assume the UPDATE landed: an unmarked row is a
|
|
55
65
|
// withdrawal the pull will never see, and nothing else would report it.
|
|
56
|
-
const marked = await env.DB.prepare(
|
|
57
|
-
|
|
66
|
+
const marked = await env.DB.prepare(
|
|
67
|
+
'SELECT deleted FROM manifest WHERE objectKey = ? AND accountId = ?',
|
|
68
|
+
)
|
|
69
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
58
70
|
.first<{ deleted: number }>()
|
|
59
71
|
const ok = marked?.deleted === 1
|
|
60
72
|
log(
|
|
@@ -84,7 +96,9 @@ export async function processDelete(
|
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
try {
|
|
87
|
-
await env.DB.prepare('DELETE FROM manifest WHERE objectKey = ?')
|
|
99
|
+
await env.DB.prepare('DELETE FROM manifest WHERE objectKey = ? AND accountId = ?')
|
|
100
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
101
|
+
.run()
|
|
88
102
|
log(`[data-portal] op=manifest-delete owner=${owner} key=${q(key)} result=ok`)
|
|
89
103
|
} catch (err) {
|
|
90
104
|
log(
|
|
@@ -96,8 +110,9 @@ export async function processDelete(
|
|
|
96
110
|
// The verified post-condition: both sides re-read, not "we called delete".
|
|
97
111
|
const objectPresent = (await env.BUCKET.head(key)) !== null
|
|
98
112
|
const rowPresent =
|
|
99
|
-
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?')
|
|
100
|
-
|
|
113
|
+
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ? AND accountId = ?')
|
|
114
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
115
|
+
.first()) !== null
|
|
101
116
|
// Derived, never hardcoded: a failed delete printing result=ok next to
|
|
102
117
|
// objectPresent=true is a self-contradicting line.
|
|
103
118
|
const result = !objectPresent && !rowPresent ? 'ok' : 'incomplete'
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts
CHANGED
|
@@ -18,21 +18,25 @@ export async function processDownload(
|
|
|
18
18
|
log: Logger,
|
|
19
19
|
nowMs: number,
|
|
20
20
|
): Promise<DownloadResult> {
|
|
21
|
-
const session = await resolveSession(env.DB, sessionId, nowMs)
|
|
21
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, nowMs)
|
|
22
22
|
if (!session) {
|
|
23
23
|
log('[data-portal] op=download owner=none result=denied')
|
|
24
24
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
25
25
|
}
|
|
26
|
-
// Authorize BEFORE any R2 call, so a denied caller cannot cause a fetch.
|
|
27
|
-
|
|
26
|
+
// Authorize BEFORE any R2 call, so a denied caller cannot cause a fetch. The
|
|
27
|
+
// key comes from the client and the bucket is shared, so the account segment
|
|
28
|
+
// is part of what is checked, never inferred from how the key was built.
|
|
29
|
+
if (!authorizeKey(env.PORTAL_ACCOUNT_ID, session.ownerId, key)) {
|
|
28
30
|
log(`[data-portal] op=download owner=${q(session.ownerId)} result=denied reason=not-owner`)
|
|
29
31
|
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
30
32
|
}
|
|
31
33
|
// Where the bytes are decides how they are served (Task 1910). A moved row's
|
|
32
34
|
// R2 object has been deleted, so reading it here would 404 the client's own
|
|
33
35
|
// upload the moment the device collected it.
|
|
34
|
-
const row = await env.DB.prepare(
|
|
35
|
-
|
|
36
|
+
const row = await env.DB.prepare(
|
|
37
|
+
'SELECT devicePath FROM manifest WHERE objectKey = ? AND accountId = ?',
|
|
38
|
+
)
|
|
39
|
+
.bind(key, env.PORTAL_ACCOUNT_ID)
|
|
36
40
|
.first<{ devicePath: string }>()
|
|
37
41
|
if (!row) {
|
|
38
42
|
log(`[data-portal] op=download owner=${q(session.ownerId)} result=absent`)
|
|
@@ -53,7 +57,7 @@ export async function processDownload(
|
|
|
53
57
|
const url = await buildOwnUploadUrl(
|
|
54
58
|
origin,
|
|
55
59
|
secret,
|
|
56
|
-
|
|
60
|
+
env.PORTAL_ACCOUNT_ID,
|
|
57
61
|
session.ownerId,
|
|
58
62
|
row.devicePath,
|
|
59
63
|
expiresAt,
|
|
@@ -106,12 +110,12 @@ export async function processIndexedDownload(
|
|
|
106
110
|
nowMs: number,
|
|
107
111
|
fetchFn: HeadFetch,
|
|
108
112
|
): Promise<IndexedDownloadResult> {
|
|
109
|
-
const session = await resolveSession(env.DB, sessionId, nowMs)
|
|
113
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, nowMs)
|
|
110
114
|
if (!session) {
|
|
111
115
|
log('[data-portal] op=fetch-through owner=none result=denied')
|
|
112
116
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
113
117
|
}
|
|
114
|
-
if (!
|
|
118
|
+
if (!env.PORTAL_ACCOUNT_ID) {
|
|
115
119
|
log(`[data-portal] op=fetch-through owner=${q(session.ownerId)} result=no-account`)
|
|
116
120
|
return { status: 404, payload: { ok: false, error: 'not found' } }
|
|
117
121
|
}
|
|
@@ -135,7 +139,7 @@ export async function processIndexedDownload(
|
|
|
135
139
|
ON s.accountId = d.accountId AND s.currentGeneration = d.generation
|
|
136
140
|
WHERE d.accountId = ? AND d.relPath = ?`,
|
|
137
141
|
)
|
|
138
|
-
.bind(
|
|
142
|
+
.bind(env.PORTAL_ACCOUNT_ID, relPath)
|
|
139
143
|
.first<{ relPath: string; isDir: number }>()
|
|
140
144
|
if (!row) {
|
|
141
145
|
log(
|
|
@@ -165,7 +169,7 @@ export async function processIndexedDownload(
|
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
const expiresAt = nowMs + SIGNED_TTL_MS
|
|
168
|
-
const url = await buildSignedUrl(origin, secret,
|
|
172
|
+
const url = await buildSignedUrl(origin, secret, env.PORTAL_ACCOUNT_ID, relPath, expiresAt)
|
|
169
173
|
|
|
170
174
|
const started = Date.now()
|
|
171
175
|
try {
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts
CHANGED
|
@@ -114,31 +114,35 @@ export async function processFiles(
|
|
|
114
114
|
nowMs: number,
|
|
115
115
|
prefix = '',
|
|
116
116
|
): Promise<Handler> {
|
|
117
|
-
const session = await resolveSession(env.DB, sessionId, nowMs)
|
|
117
|
+
const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, nowMs)
|
|
118
118
|
if (!session) {
|
|
119
119
|
log('[data-portal] op=list owner=none result=denied')
|
|
120
120
|
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
121
121
|
}
|
|
122
|
-
// Scoped by
|
|
122
|
+
// Scoped by account AND owner, not by key prefix: the owner is the authority
|
|
123
|
+
// for which files, the account for whose. The manifest is shared by every
|
|
124
|
+
// portal, so an owner-only filter would list a same-named person's uploads
|
|
125
|
+
// from another client outright.
|
|
123
126
|
const rows = await env.DB.prepare(
|
|
124
127
|
'SELECT fileId, filename, objectKey, size, uploadedAt, ingested, targetPath, devicePath, deleted ' +
|
|
125
|
-
'FROM manifest WHERE ownerId = ? ORDER BY uploadedAt DESC',
|
|
128
|
+
'FROM manifest WHERE accountId = ? AND ownerId = ? ORDER BY uploadedAt DESC',
|
|
126
129
|
)
|
|
127
|
-
.bind(session.ownerId)
|
|
130
|
+
.bind(env.PORTAL_ACCOUNT_ID, session.ownerId)
|
|
128
131
|
.all<Record<string, unknown>>()
|
|
129
132
|
const allFiles = rows.results ?? []
|
|
130
133
|
|
|
131
134
|
// The account's published folder index, scoped by accountId and read through
|
|
132
|
-
// the generation pointer so a push in flight is never half-visible.
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
+
// the generation pointer so a push in flight is never half-visible. Bound to
|
|
136
|
+
// the portal's own account rather than the session's: resolveSession already
|
|
137
|
+
// requires the two to agree, and binding the baked-in value keeps the tenant
|
|
138
|
+
// scope independent of anything a request can influence.
|
|
135
139
|
let entries: DirEntry[] = []
|
|
136
140
|
const grant = session.folders
|
|
137
141
|
// The tops the client can actually browse. Built from the SAME grant-filtered
|
|
138
142
|
// rows the tree is built from, so the uploads panel and the tree can never
|
|
139
143
|
// disagree about what is reachable.
|
|
140
144
|
const visibleTops = new Set<string>()
|
|
141
|
-
if (
|
|
145
|
+
if (env.PORTAL_ACCOUNT_ID) {
|
|
142
146
|
const dir = await env.DB.prepare(
|
|
143
147
|
`SELECT d.relPath AS relPath, d.sizeBytes AS sizeBytes, d.modifiedAt AS modifiedAt,
|
|
144
148
|
d.isDir AS isDir
|
|
@@ -147,7 +151,7 @@ export async function processFiles(
|
|
|
147
151
|
ON s.accountId = d.accountId AND s.currentGeneration = d.generation
|
|
148
152
|
WHERE d.accountId = ?`,
|
|
149
153
|
)
|
|
150
|
-
.bind(
|
|
154
|
+
.bind(env.PORTAL_ACCOUNT_ID)
|
|
151
155
|
.all<DirRow>()
|
|
152
156
|
// The grant narrows below the account's exposed set: keep only rows the
|
|
153
157
|
// grant admits. An empty grant admits everything, which is the full set and
|