@rubytech/create-sitedesk-code 0.1.519 → 0.1.520
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/whats-new/SKILL.md +6 -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/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +20 -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 +42 -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 +76 -0
- 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 +14 -2
- 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/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/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-Bpo_wgxm.js → AdminShell-Djg6ndne.js} +1 -1
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
- package/payload/server/public/assets/{activity-DOsfKzT3.js → activity-sqbyX0pX.js} +1 -1
- package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
- package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
- package/payload/server/public/assets/{agents-DqDnZTH_.js → agents-BFYM5EDA.js} +1 -1
- package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
- package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
- package/payload/server/public/assets/{browser-FL17Dfp0.js → browser-DP5GZe4G.js} +1 -1
- package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
- package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
- package/payload/server/public/assets/{calendar-CLfLvsnV.js → calendar-BKaemLDG.js} +1 -1
- package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
- package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
- package/payload/server/public/assets/data-CfjttTvR.js +1 -0
- package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
- package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DNm0AzUz.js → file-text-BKK7R-k_.js} +1 -1
- package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
- package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
- package/payload/server/public/assets/{graph-fAvmlVzh.js → graph-FuxYySjK.js} +1 -1
- package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
- package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-CwdkCxfs.js → graph-labels-Cip6fmKK.js} +1 -1
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-B6tAPhC2.js → maximize-2-D79AhVDo.js} +1 -1
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
- package/payload/server/public/assets/{operator-CTKvBVjk.js → operator-BPAfgFhP.js} +1 -1
- package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
- package/payload/server/public/assets/{page-BWoNW3sa.js → page--78CbdFs.js} +1 -1
- package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
- package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
- package/payload/server/public/assets/{page-O5HkHQw4.js → page-BqEyS0W4.js} +1 -1
- package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
- package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
- package/payload/server/public/assets/{public-BF1rnVPa.js → public-D1xs3FzD.js} +1 -1
- package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
- package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-B8TqfCy4.js → rotate-ccw-CcU_XD8B.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
- package/payload/server/public/assets/{routines-4R7BLZyH.js → routines-CA7D0myr.js} +1 -1
- package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
- package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
- package/payload/server/public/assets/{skills-CQ9Qs2tg.js → skills-CfVPPSxO.js} +1 -1
- package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
- package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
- package/payload/server/public/assets/{tasks-DePyOrRk.js → tasks-DgieyZqz.js} +1 -1
- package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-D9gtnG01.js → time-entry-format-IFqinQJj.js} +1 -1
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-DAtm2mLL.js → triangle-alert-C5vH__sq.js} +1 -1
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-uMJT-p6i.js → useCopyFeedback-BeL2gPl6.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-iCzsV96x.css → useSubAccountSwitcher-DLqUQzOl.css} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-BFrPC-s7.js → useVoiceRecorder-Cpw3aOSt.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
- package/payload/server/public/assets/{wrench-CYgaN7K9.js → wrench-BBEup3hd.js} +1 -1
- package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
- package/payload/server/public/assets/wrench-BBEup3hd.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 +454 -352
- 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.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.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.br +0 -0
- package/payload/server/public/assets/routines-4R7BLZyH.js.gz +0 -0
- 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-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
- /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js → useSubAccountSwitcher-CH9qm2Ad.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js.br → useSubAccountSwitcher-CH9qm2Ad.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-BufCPDDH.js.gz → useSubAccountSwitcher-CH9qm2Ad.js.gz} +0 -0
|
@@ -12,12 +12,27 @@ import {
|
|
|
12
12
|
} from '../../skills/data-portal/template/functions/api/_lib/authorize'
|
|
13
13
|
import type { D1Database } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
15
|
+
// The two tenants every cross-account case below is written against. The store
|
|
16
|
+
// is shared, so these are rows in ONE database, not two databases.
|
|
17
|
+
const ACC = 'acct-a'
|
|
18
|
+
const OTHER = 'acct-b'
|
|
19
|
+
|
|
20
|
+
// Models the real resolveSession query: the session row is scoped by account,
|
|
21
|
+
// and it JOINs people on BOTH ownerId and accountId, requiring the session's
|
|
22
|
+
// pcCheck to still match substr(people.hash, 1, 16). A fake that joined on
|
|
23
|
+
// ownerId alone would let the revocation AND the cross-tenant tests below pass
|
|
24
|
+
// vacuously.
|
|
18
25
|
function fakeDb(
|
|
19
|
-
seed: Array<{
|
|
20
|
-
|
|
26
|
+
seed: Array<{
|
|
27
|
+
sessionId: string
|
|
28
|
+
ownerId: string
|
|
29
|
+
accountId: string
|
|
30
|
+
pcCheck: string
|
|
31
|
+
expiresAt: number
|
|
32
|
+
}> = [],
|
|
33
|
+
people: Array<{ ownerId: string; accountId: string; hash: string }> = [
|
|
34
|
+
{ ownerId: 'alice', accountId: ACC, hash: 'HASH-v1-padded00' },
|
|
35
|
+
],
|
|
21
36
|
) {
|
|
22
37
|
const rows = [...seed]
|
|
23
38
|
const peopleRows = [...people]
|
|
@@ -31,13 +46,25 @@ function fakeDb(
|
|
|
31
46
|
},
|
|
32
47
|
async run() {
|
|
33
48
|
if (/INSERT INTO sessions/i.test(query)) {
|
|
34
|
-
const [sessionId, ownerId, pcCheck, expiresAt] = bound as [
|
|
35
|
-
|
|
49
|
+
const [sessionId, ownerId, accountId, pcCheck, expiresAt] = bound as [
|
|
50
|
+
string,
|
|
51
|
+
string,
|
|
52
|
+
string,
|
|
53
|
+
string,
|
|
54
|
+
number,
|
|
55
|
+
]
|
|
56
|
+
rows.push({ sessionId, ownerId, accountId, pcCheck, expiresAt })
|
|
36
57
|
}
|
|
37
58
|
if (/DELETE FROM sessions/i.test(query)) {
|
|
38
|
-
const [ownerId, nowMs] = bound as [string, number]
|
|
59
|
+
const [ownerId, accountId, nowMs] = bound as [string, string, number]
|
|
39
60
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
40
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
rows[i].ownerId === ownerId &&
|
|
63
|
+
rows[i].accountId === accountId &&
|
|
64
|
+
rows[i].expiresAt <= nowMs
|
|
65
|
+
) {
|
|
66
|
+
rows.splice(i, 1)
|
|
67
|
+
}
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
return { meta: { changes: 1 } }
|
|
@@ -46,13 +73,17 @@ function fakeDb(
|
|
|
46
73
|
return { results: [] }
|
|
47
74
|
},
|
|
48
75
|
async first<T>() {
|
|
49
|
-
const [sessionId, nowMs] = bound as [string, number]
|
|
50
|
-
const hit = rows.find(
|
|
76
|
+
const [sessionId, accountId, nowMs] = bound as [string, string, number]
|
|
77
|
+
const hit = rows.find(
|
|
78
|
+
(r) => r.sessionId === sessionId && r.accountId === accountId && r.expiresAt > nowMs,
|
|
79
|
+
)
|
|
51
80
|
if (!hit) return null as T | null
|
|
52
|
-
const person = peopleRows.find(
|
|
81
|
+
const person = peopleRows.find(
|
|
82
|
+
(p) => p.ownerId === hit.ownerId && p.accountId === hit.accountId,
|
|
83
|
+
)
|
|
53
84
|
if (!person) return null as T | null // unenrolled -> the JOIN drops it
|
|
54
85
|
if (person.hash.slice(0, 16) !== hit.pcCheck) return null as T | null // rotated
|
|
55
|
-
return { ownerId: hit.ownerId } as T
|
|
86
|
+
return { ownerId: hit.ownerId, accountId: person.accountId } as T
|
|
56
87
|
},
|
|
57
88
|
}
|
|
58
89
|
return stmt
|
|
@@ -66,28 +97,82 @@ const PC = 'HASH-v1-padded00'.slice(0, 16)
|
|
|
66
97
|
describe('sessions', () => {
|
|
67
98
|
it('mints a session resolvable to its owner', async () => {
|
|
68
99
|
const { db } = fakeDb()
|
|
69
|
-
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
70
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({
|
|
100
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
101
|
+
expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toEqual({
|
|
102
|
+
ownerId: 'alice',
|
|
103
|
+
accountId: ACC,
|
|
104
|
+
folders: [],
|
|
105
|
+
})
|
|
71
106
|
})
|
|
72
107
|
|
|
73
108
|
it('does not resolve an expired session', async () => {
|
|
74
109
|
const { db } = fakeDb()
|
|
75
|
-
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
76
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000 + SESSION_TTL_MS + 1)).toBeNull()
|
|
110
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
111
|
+
expect(await resolveSession(db, 'sess-1', ACC, 1_000_000 + SESSION_TTL_MS + 1)).toBeNull()
|
|
77
112
|
})
|
|
78
113
|
|
|
79
114
|
it('does not resolve an unknown session', async () => {
|
|
80
115
|
const { db } = fakeDb()
|
|
81
|
-
expect(await resolveSession(db, 'nope', 1_000_000)).toBeNull()
|
|
116
|
+
expect(await resolveSession(db, 'nope', ACC, 1_000_000)).toBeNull()
|
|
82
117
|
})
|
|
83
118
|
|
|
84
119
|
it('prunes that owner expired rows on mint, so sessions do not grow forever', async () => {
|
|
85
120
|
const { db, rows } = fakeDb([
|
|
86
|
-
{ sessionId: 'old', ownerId: 'alice', pcCheck: PC, expiresAt: 1 },
|
|
121
|
+
{ sessionId: 'old', ownerId: 'alice', accountId: ACC, pcCheck: PC, expiresAt: 1 },
|
|
87
122
|
])
|
|
88
|
-
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
123
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
89
124
|
expect(rows.map((r) => r.sessionId)).toEqual(['sess-1'])
|
|
90
125
|
})
|
|
126
|
+
|
|
127
|
+
it('prunes only its own tenant expired rows', async () => {
|
|
128
|
+
// The GC deletes by owner, and on a shared store two tenants may hold the
|
|
129
|
+
// same owner id. Unscoped, one portal's routine mint would silently delete
|
|
130
|
+
// another portal's session rows.
|
|
131
|
+
const { db, rows } = fakeDb([
|
|
132
|
+
{ sessionId: 'mine', ownerId: 'alice', accountId: ACC, pcCheck: PC, expiresAt: 1 },
|
|
133
|
+
{ sessionId: 'theirs', ownerId: 'alice', accountId: OTHER, pcCheck: PC, expiresAt: 1 },
|
|
134
|
+
])
|
|
135
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
136
|
+
expect(rows.map((r) => r.sessionId).sort()).toEqual(['sess-1', 'theirs'])
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('cross-tenant sessions — the shared-store property', () => {
|
|
141
|
+
// Two clients each enrol someone called 'alice'. Nothing but accountId tells
|
|
142
|
+
// them apart, so every one of these would have been a real leak under the
|
|
143
|
+
// per-owner join the pre-2099 template used.
|
|
144
|
+
function twoTenants() {
|
|
145
|
+
return fakeDb(
|
|
146
|
+
[],
|
|
147
|
+
[
|
|
148
|
+
{ ownerId: 'alice', accountId: ACC, hash: 'HASH-v1-padded00' },
|
|
149
|
+
{ ownerId: 'alice', accountId: OTHER, hash: 'OTHER-v1-pad0000' },
|
|
150
|
+
],
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
it('does not resolve another tenant session id', async () => {
|
|
155
|
+
const { db } = twoTenants()
|
|
156
|
+
await mintSession(db, 'alice', OTHER, 'sess-other', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
|
|
157
|
+
expect(await resolveSession(db, 'sess-other', ACC, 1_000_000)).toBeNull()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('resolves each tenant own session to its own account', async () => {
|
|
161
|
+
const { db } = twoTenants()
|
|
162
|
+
await mintSession(db, 'alice', ACC, 'sess-a', 1_000_000, PC)
|
|
163
|
+
await mintSession(db, 'alice', OTHER, 'sess-b', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
|
|
164
|
+
expect((await resolveSession(db, 'sess-a', ACC, 1_000_000))?.accountId).toBe(ACC)
|
|
165
|
+
expect((await resolveSession(db, 'sess-b', OTHER, 1_000_000))?.accountId).toBe(OTHER)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('does not let one tenant passcode rotation revoke the other sessions', async () => {
|
|
169
|
+
const { db, peopleRows } = twoTenants()
|
|
170
|
+
await mintSession(db, 'alice', ACC, 'sess-a', 1_000_000, PC)
|
|
171
|
+
await mintSession(db, 'alice', OTHER, 'sess-b', 1_000_000, 'OTHER-v1-pad0000'.slice(0, 16))
|
|
172
|
+
peopleRows[1].hash = 'OTHER-v2-rotated' // the other client rotates their alice
|
|
173
|
+
expect(await resolveSession(db, 'sess-a', ACC, 1_000_000)).not.toBeNull()
|
|
174
|
+
expect(await resolveSession(db, 'sess-b', OTHER, 1_000_000)).toBeNull()
|
|
175
|
+
})
|
|
91
176
|
})
|
|
92
177
|
|
|
93
178
|
describe('session revocation — the property the design claims', () => {
|
|
@@ -96,19 +181,23 @@ describe('session revocation — the property the design claims', () => {
|
|
|
96
181
|
// people.hash, so pcCheck no longer matches and the live session dies —
|
|
97
182
|
// by construction, not by remembering to delete anything.
|
|
98
183
|
const { db, peopleRows } = fakeDb()
|
|
99
|
-
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
100
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toEqual({
|
|
184
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
185
|
+
expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toEqual({
|
|
186
|
+
ownerId: 'alice',
|
|
187
|
+
accountId: ACC,
|
|
188
|
+
folders: [],
|
|
189
|
+
})
|
|
101
190
|
|
|
102
191
|
peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
|
|
103
192
|
|
|
104
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toBeNull()
|
|
193
|
+
expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toBeNull()
|
|
105
194
|
})
|
|
106
195
|
|
|
107
196
|
it('stops resolving once the person is unenrolled', async () => {
|
|
108
197
|
const { db, peopleRows } = fakeDb()
|
|
109
|
-
await mintSession(db, 'alice', 'sess-1', 1_000_000, PC)
|
|
198
|
+
await mintSession(db, 'alice', ACC, 'sess-1', 1_000_000, PC)
|
|
110
199
|
peopleRows.length = 0
|
|
111
|
-
expect(await resolveSession(db, 'sess-1', 1_000_000)).toBeNull()
|
|
200
|
+
expect(await resolveSession(db, 'sess-1', ACC, 1_000_000)).toBeNull()
|
|
112
201
|
})
|
|
113
202
|
})
|
|
114
203
|
|
|
@@ -144,34 +233,34 @@ describe('readSessionCookie', () => {
|
|
|
144
233
|
})
|
|
145
234
|
|
|
146
235
|
describe('authorizeKey — the isolation rule', () => {
|
|
147
|
-
it('allows a key under the owner prefix', () => {
|
|
148
|
-
expect(authorizeKey('alice',
|
|
236
|
+
it('allows a key under the account and owner prefix', () => {
|
|
237
|
+
expect(authorizeKey(ACC, 'alice', `${ACC}/alice/invoice.pdf`)).toBe(true)
|
|
149
238
|
})
|
|
150
239
|
|
|
151
240
|
it('denies another owner prefix', () => {
|
|
152
|
-
expect(authorizeKey('alice',
|
|
241
|
+
expect(authorizeKey(ACC, 'alice', `${ACC}/bob/invoice.pdf`)).toBe(false)
|
|
153
242
|
})
|
|
154
243
|
|
|
155
244
|
it('denies a traversal escape', () => {
|
|
156
|
-
expect(authorizeKey('alice',
|
|
245
|
+
expect(authorizeKey(ACC, 'alice', `${ACC}/alice/../bob/invoice.pdf`)).toBe(false)
|
|
157
246
|
})
|
|
158
247
|
|
|
159
248
|
it('denies a prefix that merely starts with the owner id', () => {
|
|
160
249
|
// 'alice' must not authorize 'alice-evil/...': a startsWith check on the
|
|
161
250
|
// bare id would pass this. The separator is part of the rule.
|
|
162
|
-
expect(authorizeKey('alice',
|
|
251
|
+
expect(authorizeKey(ACC, 'alice', `${ACC}/alice-evil/invoice.pdf`)).toBe(false)
|
|
163
252
|
})
|
|
164
253
|
|
|
165
254
|
it('denies a bare key with no prefix', () => {
|
|
166
|
-
expect(authorizeKey('alice', 'invoice.pdf')).toBe(false)
|
|
255
|
+
expect(authorizeKey(ACC, 'alice', 'invoice.pdf')).toBe(false)
|
|
167
256
|
})
|
|
168
257
|
|
|
169
258
|
it('denies an absolute-looking key', () => {
|
|
170
|
-
expect(authorizeKey('alice',
|
|
259
|
+
expect(authorizeKey(ACC, 'alice', `/${ACC}/alice/invoice.pdf`)).toBe(false)
|
|
171
260
|
})
|
|
172
261
|
|
|
173
262
|
it('denies an empty key', () => {
|
|
174
|
-
expect(authorizeKey('alice', '')).toBe(false)
|
|
263
|
+
expect(authorizeKey(ACC, 'alice', '')).toBe(false)
|
|
175
264
|
})
|
|
176
265
|
|
|
177
266
|
// authorizeKey's correctness silently depends on ownerId containing no
|
|
@@ -180,22 +269,49 @@ describe('authorizeKey — the isolation rule', () => {
|
|
|
180
269
|
// person's files. Enrolment is an agent writing rows, so the invariant
|
|
181
270
|
// cannot rest on the skill's prose — enforce it at the gate.
|
|
182
271
|
it('denies when the ownerId itself contains a separator', () => {
|
|
183
|
-
expect(authorizeKey('alice/2024',
|
|
272
|
+
expect(authorizeKey(ACC, 'alice/2024', `${ACC}/alice/2024/invoice.pdf`)).toBe(false)
|
|
184
273
|
})
|
|
185
274
|
|
|
186
275
|
it('denies an ownerId outside [a-z0-9-], failing closed', () => {
|
|
187
276
|
for (const bad of ['alice/2024', 'Alice', 'alice..', 'alice bob', '../alice', 'alice%2f']) {
|
|
188
|
-
expect(authorizeKey(bad, `${bad}/invoice.pdf`)).toBe(false)
|
|
277
|
+
expect(authorizeKey(ACC, bad, `${ACC}/${bad}/invoice.pdf`)).toBe(false)
|
|
189
278
|
}
|
|
190
279
|
})
|
|
191
280
|
|
|
192
281
|
it('still allows a well-formed ownerId', () => {
|
|
193
|
-
expect(authorizeKey('alice-smith-2',
|
|
282
|
+
expect(authorizeKey(ACC, 'alice-smith-2', `${ACC}/alice-smith-2/invoice.pdf`)).toBe(true)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('authorizeKey — the tenant half of the rule', () => {
|
|
287
|
+
// The store is shared, so the account segment is the only thing separating
|
|
288
|
+
// one client's objects from another's. Every key on the delete, download and
|
|
289
|
+
// multipart paths arrives from the client, so this is checked, never assumed.
|
|
290
|
+
it('denies another tenant prefix even with the right owner', () => {
|
|
291
|
+
expect(authorizeKey(ACC, 'alice', `${OTHER}/alice/invoice.pdf`)).toBe(false)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('denies a bare owner prefix that omits the account', () => {
|
|
295
|
+
expect(authorizeKey(ACC, 'alice', 'alice/invoice.pdf')).toBe(false)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('denies a tenant that merely starts with the account id', () => {
|
|
299
|
+
expect(authorizeKey(ACC, 'alice', `${ACC}-evil/alice/invoice.pdf`)).toBe(false)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('denies an accountId outside [a-z0-9-], failing closed', () => {
|
|
303
|
+
for (const bad of ['acct/a', 'Acct', 'acct..', 'acct a', '../acct']) {
|
|
304
|
+
expect(authorizeKey(bad, 'alice', `${bad}/alice/invoice.pdf`)).toBe(false)
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('denies an empty accountId rather than degrading to an owner-only rule', () => {
|
|
309
|
+
expect(authorizeKey('', 'alice', 'alice/invoice.pdf')).toBe(false)
|
|
194
310
|
})
|
|
195
311
|
})
|
|
196
312
|
|
|
197
313
|
describe('ownerPrefix', () => {
|
|
198
|
-
it('is the owner id
|
|
199
|
-
expect(ownerPrefix('alice')).toBe('alice/')
|
|
314
|
+
it('is the account and the owner id, each with a separator', () => {
|
|
315
|
+
expect(ownerPrefix(ACC, 'alice')).toBe('acct-a/alice/')
|
|
200
316
|
})
|
|
201
317
|
})
|
|
@@ -4,18 +4,36 @@ import { processDownload } from '../../skills/data-portal/template/functions/api
|
|
|
4
4
|
import { processDelete } from '../../skills/data-portal/template/functions/api/delete'
|
|
5
5
|
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
6
6
|
|
|
7
|
-
// alice's session is 'sess-a'; bob owns
|
|
7
|
+
// The portal is account 'acct-a'; alice's session is 'sess-a'; bob owns
|
|
8
|
+
// 'acct-a/bob/secret.pdf'. The store is shared by every portal (Task 2099), so
|
|
9
|
+
// the account is the first key segment and a column on every row, and the fake
|
|
10
|
+
// below filters on it exactly as the real queries do.
|
|
8
11
|
function makeEnv() {
|
|
9
12
|
const manifest = [
|
|
10
|
-
{
|
|
11
|
-
|
|
13
|
+
{
|
|
14
|
+
fileId: 'f1',
|
|
15
|
+
accountId: 'acct-a',
|
|
16
|
+
ownerId: 'alice',
|
|
17
|
+
filename: 'a.pdf',
|
|
18
|
+
objectKey: 'acct-a/alice/a.pdf',
|
|
19
|
+
size: 10,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
fileId: 'f2',
|
|
23
|
+
accountId: 'acct-a',
|
|
24
|
+
ownerId: 'bob',
|
|
25
|
+
filename: 'secret.pdf',
|
|
26
|
+
objectKey: 'acct-a/bob/secret.pdf',
|
|
27
|
+
size: 20,
|
|
28
|
+
},
|
|
12
29
|
]
|
|
13
30
|
const objects = new Map<string, string>([
|
|
14
|
-
['alice/a.pdf', 'A'],
|
|
15
|
-
['bob/secret.pdf', 'B'],
|
|
31
|
+
['acct-a/alice/a.pdf', 'A'],
|
|
32
|
+
['acct-a/bob/secret.pdf', 'B'],
|
|
16
33
|
])
|
|
17
34
|
const deleted: string[] = []
|
|
18
35
|
const env = {
|
|
36
|
+
PORTAL_ACCOUNT_ID: 'acct-a',
|
|
19
37
|
DB: {
|
|
20
38
|
prepare(query: string) {
|
|
21
39
|
let bound: unknown[] = []
|
|
@@ -25,23 +43,48 @@ function makeEnv() {
|
|
|
25
43
|
return stmt
|
|
26
44
|
},
|
|
27
45
|
async run() {
|
|
46
|
+
// Every single-row manifest statement binds (objectKey, accountId):
|
|
47
|
+
// objectKey is unique per account, not globally.
|
|
28
48
|
if (/DELETE FROM manifest/i.test(query)) {
|
|
29
|
-
const i = manifest.findIndex(
|
|
49
|
+
const i = manifest.findIndex(
|
|
50
|
+
(m) => m.objectKey === bound[0] && m.accountId === bound[1],
|
|
51
|
+
)
|
|
30
52
|
if (i >= 0) manifest.splice(i, 1)
|
|
31
53
|
}
|
|
32
54
|
return { meta: { changes: 1 } }
|
|
33
55
|
},
|
|
34
56
|
async all<T>() {
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
// The account's published folder index is empty in these fixtures:
|
|
58
|
+
// this file covers upload rows and key isolation, not the tree.
|
|
59
|
+
if (/FROM directory/i.test(query)) {
|
|
60
|
+
return { results: [] as T[] }
|
|
61
|
+
}
|
|
62
|
+
// The manifest list binds (accountId, ownerId): the owner says
|
|
63
|
+
// which files, the account whose.
|
|
64
|
+
const [account, owner] = bound as string[]
|
|
65
|
+
return {
|
|
66
|
+
results: manifest.filter(
|
|
67
|
+
(m) => m.accountId === account && m.ownerId === owner,
|
|
68
|
+
) as T[],
|
|
69
|
+
}
|
|
37
70
|
},
|
|
38
71
|
async first<T>() {
|
|
39
72
|
if (/FROM sessions/i.test(query)) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
// Bound (sessionId, accountId, nowMs). The join is on BOTH the
|
|
74
|
+
// session's account and the people row's, so a session resolves
|
|
75
|
+
// only against its own portal's account and the resolved
|
|
76
|
+
// people.accountId has to come back matching.
|
|
77
|
+
const map: Record<string, { ownerId: string; accountId: string }> = {
|
|
78
|
+
'sess-a': { ownerId: 'alice', accountId: 'acct-a' },
|
|
79
|
+
'sess-b': { ownerId: 'bob', accountId: 'acct-a' },
|
|
80
|
+
}
|
|
81
|
+
const row = map[bound[0] as string]
|
|
82
|
+
return (row && row.accountId === bound[1]
|
|
83
|
+
? { ownerId: row.ownerId, accountId: row.accountId, folders: '' }
|
|
84
|
+
: null) as T | null
|
|
43
85
|
}
|
|
44
|
-
return (manifest.find((m) => m.objectKey === bound[0]
|
|
86
|
+
return (manifest.find((m) => m.objectKey === bound[0] && m.accountId === bound[1]) ??
|
|
87
|
+
null) as T | null
|
|
45
88
|
},
|
|
46
89
|
}
|
|
47
90
|
return stmt
|
|
@@ -74,7 +117,7 @@ describe('processFiles', () => {
|
|
|
74
117
|
const res = await processFiles('sess-a', env, () => {}, 1_000_000)
|
|
75
118
|
expect(res.status).toBe(200)
|
|
76
119
|
const files = res.payload.files as Array<{ objectKey: string }>
|
|
77
|
-
expect(files.map((f) => f.objectKey)).toEqual(['alice/a.pdf'])
|
|
120
|
+
expect(files.map((f) => f.objectKey)).toEqual(['acct-a/alice/a.pdf'])
|
|
78
121
|
})
|
|
79
122
|
|
|
80
123
|
it("never includes another owner's file", async () => {
|
|
@@ -93,56 +136,72 @@ describe('processFiles', () => {
|
|
|
93
136
|
describe('processDownload — isolation', () => {
|
|
94
137
|
it("allows the owner's own key", async () => {
|
|
95
138
|
const { env } = makeEnv()
|
|
96
|
-
expect(
|
|
97
|
-
|
|
98
|
-
)
|
|
139
|
+
expect(
|
|
140
|
+
(await processDownload('sess-a', 'acct-a/alice/a.pdf', env, () => {}, 1_000_000)).status,
|
|
141
|
+
).toBe(200)
|
|
99
142
|
})
|
|
100
143
|
|
|
101
144
|
it("denies person A's session on person B's key", async () => {
|
|
102
145
|
const { env } = makeEnv()
|
|
103
|
-
const res = await processDownload('sess-a', 'bob/secret.pdf', env, () => {}, 1_000_000)
|
|
146
|
+
const res = await processDownload('sess-a', 'acct-a/bob/secret.pdf', env, () => {}, 1_000_000)
|
|
104
147
|
expect(res.status).toBe(403)
|
|
105
148
|
})
|
|
106
149
|
|
|
107
150
|
it('denies a traversal escape', async () => {
|
|
108
151
|
const { env } = makeEnv()
|
|
109
|
-
const res = await processDownload(
|
|
152
|
+
const res = await processDownload(
|
|
153
|
+
'sess-a',
|
|
154
|
+
'acct-a/alice/../bob/secret.pdf',
|
|
155
|
+
env,
|
|
156
|
+
() => {},
|
|
157
|
+
1_000_000,
|
|
158
|
+
)
|
|
110
159
|
expect(res.status).toBe(403)
|
|
111
160
|
})
|
|
112
161
|
|
|
113
162
|
it('denies an unauthenticated caller', async () => {
|
|
114
163
|
const { env } = makeEnv()
|
|
115
|
-
expect((await processDownload('', 'alice/a.pdf', env, () => {}, 1_000_000)).status).toBe(
|
|
164
|
+
expect((await processDownload('', 'acct-a/alice/a.pdf', env, () => {}, 1_000_000)).status).toBe(
|
|
165
|
+
401,
|
|
166
|
+
)
|
|
116
167
|
})
|
|
117
168
|
})
|
|
118
169
|
|
|
119
170
|
describe('processDelete — isolation and both-sides', () => {
|
|
120
171
|
it('removes object and manifest row together', async () => {
|
|
121
172
|
const { env, deleted, manifest } = makeEnv()
|
|
122
|
-
const res = await processDelete('sess-a', 'alice/a.pdf', env, () => {}, () => 1_000_000)
|
|
173
|
+
const res = await processDelete('sess-a', 'acct-a/alice/a.pdf', env, () => {}, () => 1_000_000)
|
|
123
174
|
expect(res.status).toBe(200)
|
|
124
|
-
expect(deleted).toEqual(['alice/a.pdf'])
|
|
125
|
-
expect(manifest.find((m) => m.objectKey === 'alice/a.pdf')).toBeUndefined()
|
|
175
|
+
expect(deleted).toEqual(['acct-a/alice/a.pdf'])
|
|
176
|
+
expect(manifest.find((m) => m.objectKey === 'acct-a/alice/a.pdf')).toBeUndefined()
|
|
126
177
|
})
|
|
127
178
|
|
|
128
179
|
it("denies person A's session on person B's key, and deletes nothing", async () => {
|
|
129
180
|
const { env, deleted, manifest } = makeEnv()
|
|
130
|
-
const res = await processDelete(
|
|
181
|
+
const res = await processDelete(
|
|
182
|
+
'sess-a',
|
|
183
|
+
'acct-a/bob/secret.pdf',
|
|
184
|
+
env,
|
|
185
|
+
() => {},
|
|
186
|
+
() => 1_000_000,
|
|
187
|
+
)
|
|
131
188
|
expect(res.status).toBe(403)
|
|
132
189
|
expect(deleted).toEqual([])
|
|
133
|
-
expect(manifest.find((m) => m.objectKey === 'bob/secret.pdf')).toBeDefined()
|
|
190
|
+
expect(manifest.find((m) => m.objectKey === 'acct-a/bob/secret.pdf')).toBeDefined()
|
|
134
191
|
})
|
|
135
192
|
|
|
136
193
|
it('denies an unauthenticated caller', async () => {
|
|
137
194
|
const { env, deleted } = makeEnv()
|
|
138
|
-
expect(
|
|
195
|
+
expect(
|
|
196
|
+
(await processDelete('', 'acct-a/alice/a.pdf', env, () => {}, () => 1_000_000)).status,
|
|
197
|
+
).toBe(401)
|
|
139
198
|
expect(deleted).toEqual([])
|
|
140
199
|
})
|
|
141
200
|
|
|
142
201
|
it('emits a per-step lifeline, not one bare line', async () => {
|
|
143
202
|
const { env } = makeEnv()
|
|
144
203
|
const lines: string[] = []
|
|
145
|
-
await processDelete('sess-a', 'alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
|
|
204
|
+
await processDelete('sess-a', 'acct-a/alice/a.pdf', env, (l) => lines.push(l), () => 1_000_000)
|
|
146
205
|
const joined = lines.join('\n')
|
|
147
206
|
expect(joined).toContain('op=r2-delete')
|
|
148
207
|
expect(joined).toContain('op=manifest-delete')
|
|
@@ -157,12 +216,18 @@ describe('processDelete — isolation and both-sides', () => {
|
|
|
157
216
|
throw new Error('r2 unavailable')
|
|
158
217
|
}
|
|
159
218
|
const lines: string[] = []
|
|
160
|
-
const res = await processDelete(
|
|
219
|
+
const res = await processDelete(
|
|
220
|
+
'sess-a',
|
|
221
|
+
'acct-a/alice/a.pdf',
|
|
222
|
+
env,
|
|
223
|
+
(l) => lines.push(l),
|
|
224
|
+
() => 1_000_000,
|
|
225
|
+
)
|
|
161
226
|
expect(res.status).toBe(502)
|
|
162
227
|
expect(lines.join('\n')).toContain('op=r2-delete')
|
|
163
228
|
expect(lines.join('\n')).toContain('result=failed')
|
|
164
229
|
// The row must survive an object that was never deleted.
|
|
165
|
-
expect(manifest.find((m) => m.objectKey === 'alice/a.pdf')).toBeDefined()
|
|
230
|
+
expect(manifest.find((m) => m.objectKey === 'acct-a/alice/a.pdf')).toBeDefined()
|
|
166
231
|
})
|
|
167
232
|
|
|
168
233
|
it('derives result from the post-condition rather than hardcoding ok', async () => {
|
|
@@ -171,7 +236,13 @@ describe('processDelete — isolation and both-sides', () => {
|
|
|
171
236
|
const { env } = makeEnv()
|
|
172
237
|
;(env.BUCKET as unknown as { delete: () => Promise<void> }).delete = async () => {}
|
|
173
238
|
const lines: string[] = []
|
|
174
|
-
const res = await processDelete(
|
|
239
|
+
const res = await processDelete(
|
|
240
|
+
'sess-a',
|
|
241
|
+
'acct-a/alice/a.pdf',
|
|
242
|
+
env,
|
|
243
|
+
(l) => lines.push(l),
|
|
244
|
+
() => 1_000_000,
|
|
245
|
+
)
|
|
175
246
|
expect(res.status).toBe(500)
|
|
176
247
|
expect(lines.join('\n')).toMatch(/op=complete .*result=incomplete objectPresent=true/)
|
|
177
248
|
})
|