@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
|
@@ -37,6 +37,10 @@ function harness(opts: { folders?: string[] } = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const env = {
|
|
40
|
+
// This portal's own account, baked into wrangler at assemble time. Every
|
|
41
|
+
// object key and every query binds it, so the fake session row below has to
|
|
42
|
+
// carry the same value or resolveSession refuses.
|
|
43
|
+
PORTAL_ACCOUNT_ID: 'acct',
|
|
40
44
|
BUCKET: {
|
|
41
45
|
async createMultipartUpload(key: string) {
|
|
42
46
|
const id = `r2-${++seq}`
|
|
@@ -69,7 +73,10 @@ function harness(opts: { folders?: string[] } = {}) {
|
|
|
69
73
|
return null
|
|
70
74
|
},
|
|
71
75
|
async run() {
|
|
72
|
-
|
|
76
|
+
// The manifest upsert binds (fileId, accountId, ownerId,
|
|
77
|
+
// filename, objectKey, size, ...), so the key is arg 4 and the
|
|
78
|
+
// size arg 5.
|
|
79
|
+
rows.set(String(args[4]), { fileId: args[0], size: args[5] })
|
|
73
80
|
},
|
|
74
81
|
}
|
|
75
82
|
},
|
|
@@ -105,7 +112,7 @@ describe('multipart create', () => {
|
|
|
105
112
|
)
|
|
106
113
|
expect(res.status).toBe(200)
|
|
107
114
|
expect(res.payload.r2UploadId).toBe('r2-1')
|
|
108
|
-
expect(res.payload.key).toBe('alice/big.zip')
|
|
115
|
+
expect(res.payload.key).toBe('acct/alice/big.zip')
|
|
109
116
|
})
|
|
110
117
|
|
|
111
118
|
it('applies the same target rule as a single-shot upload', async () => {
|
|
@@ -122,7 +129,7 @@ describe('multipart create', () => {
|
|
|
122
129
|
const res = await processMultipartCreate(
|
|
123
130
|
's', 'big.zip', 'jobs', 1024, h.env, h.log, h.newId, () => 1,
|
|
124
131
|
)
|
|
125
|
-
expect(res.payload.key).toBe('alice/jobs/big.zip')
|
|
132
|
+
expect(res.payload.key).toBe('acct/alice/jobs/big.zip')
|
|
126
133
|
})
|
|
127
134
|
})
|
|
128
135
|
|
|
@@ -131,17 +138,18 @@ describe('multipart part', () => {
|
|
|
131
138
|
const h = harness()
|
|
132
139
|
await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
|
|
133
140
|
const res = await processMultipartPart(
|
|
134
|
-
's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(MAX_PART_BYTES + 1), h.env, h.log,
|
|
141
|
+
's', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(MAX_PART_BYTES + 1), h.env, h.log,
|
|
135
142
|
)
|
|
136
143
|
expect(res.status).toBe(413)
|
|
137
144
|
})
|
|
138
145
|
|
|
139
146
|
it('refuses a part whose key is not the caller own', async () => {
|
|
140
147
|
// The key arrives on EVERY part request, so it is re-authorized every time.
|
|
141
|
-
// A session that may write alice/ must not be able to append to
|
|
148
|
+
// A session that may write acct/alice/ must not be able to append to
|
|
149
|
+
// acct/bob/, even though both live in this portal's own account.
|
|
142
150
|
const h = harness()
|
|
143
151
|
const res = await processMultipartPart(
|
|
144
|
-
's', 'bob/big.zip', 'r2-1', 1, new Uint8Array(4), h.env, h.log,
|
|
152
|
+
's', 'acct/bob/big.zip', 'r2-1', 1, new Uint8Array(4), h.env, h.log,
|
|
145
153
|
)
|
|
146
154
|
expect(res.status).toBe(403)
|
|
147
155
|
})
|
|
@@ -150,7 +158,7 @@ describe('multipart part', () => {
|
|
|
150
158
|
const h = harness()
|
|
151
159
|
await processMultipartCreate('s', 'big.zip', '', 200 * 1024 * 1024, h.env, h.log, h.newId, () => 1)
|
|
152
160
|
const res = await processMultipartPart(
|
|
153
|
-
's', 'alice/big.zip', 'r2-1', 1, new Uint8Array(8), h.env, h.log,
|
|
161
|
+
's', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(8), h.env, h.log,
|
|
154
162
|
)
|
|
155
163
|
expect(res.status).toBe(200)
|
|
156
164
|
expect(res.payload.etag).toBe('etag-1')
|
|
@@ -161,15 +169,15 @@ describe('multipart complete', () => {
|
|
|
161
169
|
it('assembles the object and writes one manifest row', async () => {
|
|
162
170
|
const h = harness()
|
|
163
171
|
await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
|
|
164
|
-
await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(60), h.env, h.log)
|
|
165
|
-
await processMultipartPart('s', 'alice/big.zip', 'r2-1', 2, new Uint8Array(40), h.env, h.log)
|
|
172
|
+
await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(60), h.env, h.log)
|
|
173
|
+
await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 2, new Uint8Array(40), h.env, h.log)
|
|
166
174
|
const res = await processMultipartComplete(
|
|
167
|
-
's', 'alice/big.zip', 'r2-1', '',
|
|
175
|
+
's', 'acct/alice/big.zip', 'r2-1', '',
|
|
168
176
|
[{ partNumber: 1, etag: 'etag-1' }, { partNumber: 2, etag: 'etag-2' }],
|
|
169
177
|
h.env, h.log, h.newId, () => 1,
|
|
170
178
|
)
|
|
171
179
|
expect(res.status).toBe(200)
|
|
172
|
-
expect(h.objects.get('alice/big.zip')).toBe(100)
|
|
180
|
+
expect(h.objects.get('acct/alice/big.zip')).toBe(100)
|
|
173
181
|
expect(h.rows.size).toBe(1)
|
|
174
182
|
})
|
|
175
183
|
|
|
@@ -179,7 +187,7 @@ describe('multipart complete', () => {
|
|
|
179
187
|
const h = harness()
|
|
180
188
|
await processMultipartCreate('s', 'big.zip', '', 100, h.env, h.log, h.newId, () => 1)
|
|
181
189
|
const res = await processMultipartComplete(
|
|
182
|
-
's', 'alice/big.zip', 'r2-1', '',
|
|
190
|
+
's', 'acct/alice/big.zip', 'r2-1', '',
|
|
183
191
|
[{ partNumber: 1, etag: 'e', size: MAX_TOTAL_BYTES }, { partNumber: 2, etag: 'e', size: 1 }],
|
|
184
192
|
h.env, h.log, h.newId, () => 1,
|
|
185
193
|
)
|
|
@@ -191,7 +199,7 @@ describe('multipart complete', () => {
|
|
|
191
199
|
it('refuses a complete for a key that is not the caller own', async () => {
|
|
192
200
|
const h = harness()
|
|
193
201
|
const res = await processMultipartComplete(
|
|
194
|
-
's', 'bob/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'e' }], h.env, h.log, h.newId, () => 1,
|
|
202
|
+
's', 'acct/bob/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'e' }], h.env, h.log, h.newId, () => 1,
|
|
195
203
|
)
|
|
196
204
|
expect(res.status).toBe(403)
|
|
197
205
|
})
|
|
@@ -199,9 +207,9 @@ describe('multipart complete', () => {
|
|
|
199
207
|
it('logs op=complete with a re-read post-condition', async () => {
|
|
200
208
|
const h = harness()
|
|
201
209
|
await processMultipartCreate('s', 'big.zip', '', 10, h.env, h.log, h.newId, () => 1)
|
|
202
|
-
await processMultipartPart('s', 'alice/big.zip', 'r2-1', 1, new Uint8Array(10), h.env, h.log)
|
|
210
|
+
await processMultipartPart('s', 'acct/alice/big.zip', 'r2-1', 1, new Uint8Array(10), h.env, h.log)
|
|
203
211
|
await processMultipartComplete(
|
|
204
|
-
's', 'alice/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'etag-1' }],
|
|
212
|
+
's', 'acct/alice/big.zip', 'r2-1', '', [{ partNumber: 1, etag: 'etag-1' }],
|
|
205
213
|
h.env, h.log, h.newId, () => 1,
|
|
206
214
|
)
|
|
207
215
|
expect(
|
|
@@ -3,9 +3,11 @@ import { processUpload } from '../../skills/data-portal/template/functions/api/u
|
|
|
3
3
|
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The manifest is a Map keyed on objectKey, so the schema's
|
|
7
|
-
* is real rather than assumed.
|
|
8
|
-
*
|
|
6
|
+
* The manifest is a Map keyed on objectKey, so the schema's
|
|
7
|
+
* `UNIQUE (accountId, objectKey)` is real rather than assumed. Every key now
|
|
8
|
+
* leads with the account segment, so on this single-account harness keying on
|
|
9
|
+
* objectKey alone is that same conflict key. Task 1704's lesson: a fake that
|
|
10
|
+
* accepts what the store rejects passes a test the production path fails.
|
|
9
11
|
*/
|
|
10
12
|
function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
|
|
11
13
|
const folders = opts.folders ?? ['jobs']
|
|
@@ -14,6 +16,9 @@ function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
|
|
|
14
16
|
const lines: string[] = []
|
|
15
17
|
|
|
16
18
|
const env = {
|
|
19
|
+
// This portal's own account, baked into wrangler.toml at assemble time. It
|
|
20
|
+
// is the tenant scope for every query and the leading segment of every key.
|
|
21
|
+
PORTAL_ACCOUNT_ID: 'acct',
|
|
17
22
|
BUCKET: {
|
|
18
23
|
async put(key: string, body: Uint8Array) {
|
|
19
24
|
objects.set(key, body)
|
|
@@ -45,17 +50,20 @@ function harness(opts: { folders?: string[]; grant?: string[] } = {}) {
|
|
|
45
50
|
return null
|
|
46
51
|
},
|
|
47
52
|
async run() {
|
|
48
|
-
// The upsert: conflict on objectKey replaces in
|
|
49
|
-
// what the real
|
|
50
|
-
|
|
53
|
+
// The upsert: conflict on (accountId, objectKey) replaces in
|
|
54
|
+
// place, which is what the real
|
|
55
|
+
// ON CONFLICT (accountId, objectKey) does. accountId is bind 1,
|
|
56
|
+
// so every column after it sits one index later than it used to.
|
|
57
|
+
const key = String(args[4])
|
|
51
58
|
rows.set(key, {
|
|
52
59
|
fileId: args[0],
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
accountId: args[1],
|
|
61
|
+
ownerId: args[2],
|
|
62
|
+
filename: args[3],
|
|
55
63
|
objectKey: key,
|
|
56
|
-
size: args[
|
|
64
|
+
size: args[5],
|
|
57
65
|
ingested: 0,
|
|
58
|
-
targetPath: args[
|
|
66
|
+
targetPath: args[8],
|
|
59
67
|
})
|
|
60
68
|
},
|
|
61
69
|
}
|
|
@@ -83,8 +91,8 @@ describe('targeted upload', () => {
|
|
|
83
91
|
's', 'a.pdf', 'jobs', new Uint8Array([1, 2]), h.env, h.log, h.newId, () => 1000,
|
|
84
92
|
)
|
|
85
93
|
expect(res.status).toBe(200)
|
|
86
|
-
expect([...h.objects.keys()]).toEqual(['alice/jobs/a.pdf'])
|
|
87
|
-
expect(h.rows.get('alice/jobs/a.pdf')!.targetPath).toBe('jobs')
|
|
94
|
+
expect([...h.objects.keys()]).toEqual(['acct/alice/jobs/a.pdf'])
|
|
95
|
+
expect(h.rows.get('acct/alice/jobs/a.pdf')!.targetPath).toBe('jobs')
|
|
88
96
|
})
|
|
89
97
|
|
|
90
98
|
it('stores at the owner root when untargeted', async () => {
|
|
@@ -93,7 +101,7 @@ describe('targeted upload', () => {
|
|
|
93
101
|
's', 'a.pdf', '', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000,
|
|
94
102
|
)
|
|
95
103
|
expect(res.status).toBe(200)
|
|
96
|
-
expect([...h.objects.keys()]).toEqual(['alice/a.pdf'])
|
|
104
|
+
expect([...h.objects.keys()]).toEqual(['acct/alice/a.pdf'])
|
|
97
105
|
})
|
|
98
106
|
|
|
99
107
|
it('keeps the same filename in two folders as two distinct objects', async () => {
|
|
@@ -102,7 +110,7 @@ describe('targeted upload', () => {
|
|
|
102
110
|
const h = harness({ folders: ['jobs', 'output'] })
|
|
103
111
|
await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
|
|
104
112
|
await processUpload('s', 'a.pdf', 'output', new Uint8Array([2]), h.env, h.log, h.newId, () => 1000)
|
|
105
|
-
expect([...h.objects.keys()].sort()).toEqual(['alice/jobs/a.pdf', 'alice/output/a.pdf'])
|
|
113
|
+
expect([...h.objects.keys()].sort()).toEqual(['acct/alice/jobs/a.pdf', 'acct/alice/output/a.pdf'])
|
|
106
114
|
expect(h.rows.size).toBe(2)
|
|
107
115
|
})
|
|
108
116
|
|
|
@@ -111,7 +119,7 @@ describe('targeted upload', () => {
|
|
|
111
119
|
await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([1]), h.env, h.log, h.newId, () => 1000)
|
|
112
120
|
await processUpload('s', 'a.pdf', 'jobs', new Uint8Array([2, 2]), h.env, h.log, h.newId, () => 2000)
|
|
113
121
|
expect(h.rows.size).toBe(1)
|
|
114
|
-
expect(h.rows.get('alice/jobs/a.pdf')!.size).toBe(2)
|
|
122
|
+
expect(h.rows.get('acct/alice/jobs/a.pdf')!.size).toBe(2)
|
|
115
123
|
})
|
|
116
124
|
|
|
117
125
|
it('refuses an out-of-grant target and stores nothing', async () => {
|
|
@@ -20,6 +20,10 @@ function envWithGenerations(
|
|
|
20
20
|
manifest: Record<string, unknown>[] = [],
|
|
21
21
|
) {
|
|
22
22
|
return {
|
|
23
|
+
// The portal's own account, baked in at assemble time. On the shared store
|
|
24
|
+
// (Task 2099) it is the tenant scope every query binds, so the fake below
|
|
25
|
+
// honours it the same way it honours the generation pointer.
|
|
26
|
+
PORTAL_ACCOUNT_ID: 'acct',
|
|
23
27
|
DB: {
|
|
24
28
|
prepare(sql: string) {
|
|
25
29
|
return {
|
|
@@ -27,6 +31,10 @@ function envWithGenerations(
|
|
|
27
31
|
return {
|
|
28
32
|
async first() {
|
|
29
33
|
if (sql.includes('FROM sessions')) {
|
|
34
|
+
// Bound (sessionId, accountId, nowMs). A session belonging to
|
|
35
|
+
// another portal's account resolves to nothing here, and the
|
|
36
|
+
// joined people row carries the account the caller asked for.
|
|
37
|
+
if (bound[1] !== 'acct') return null
|
|
30
38
|
return { ownerId: 'alice', accountId: 'acct', folders: '' }
|
|
31
39
|
}
|
|
32
40
|
return null
|
|
@@ -44,13 +44,30 @@ describe('data-portal SKILL.md', () => {
|
|
|
44
44
|
expect(skill()).toMatch(/plugin-read/)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it('records the
|
|
48
|
-
// The load-bearing
|
|
49
|
-
//
|
|
50
|
-
//
|
|
47
|
+
it('records the shared store and single-session provisioning (Task 2099)', () => {
|
|
48
|
+
// The load-bearing decision, and the reverse of what this test used to
|
|
49
|
+
// pin. Provisioning was two sessions so the client account would own its
|
|
50
|
+
// own bucket. That is gone: every portal binds ONE shared house-owned
|
|
51
|
+
// database and bucket, created once, and a per-client store is what put
|
|
52
|
+
// the account on Cloudflare's ten-database cap.
|
|
51
53
|
const text = skill()
|
|
52
|
-
expect(text).toMatch(/
|
|
53
|
-
expect(text).toMatch(/
|
|
54
|
+
expect(text).toMatch(/one session|single house session|provisioning is one session/i)
|
|
55
|
+
expect(text).toMatch(/DATA_PORTAL_DB_NAME/)
|
|
56
|
+
expect(text).toMatch(/DATA_PORTAL_BUCKET/)
|
|
57
|
+
// The per-client creates must not come back by copy-paste.
|
|
58
|
+
expect(text).not.toMatch(/storage-r2-bucket-create/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('pins the tenant discriminator, since one store now holds every client', () => {
|
|
62
|
+
const text = skill()
|
|
63
|
+
expect(text).toMatch(/PORTAL_ACCOUNT_ID/)
|
|
64
|
+
expect(text).toMatch(/storageModel/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('forbids hand-filling the assembled config', () => {
|
|
68
|
+
// Two sessions resolving the same placeholder independently is what
|
|
69
|
+
// shipped a portal whose bucket name and config disagreed.
|
|
70
|
+
expect(skill()).toMatch(/portal-assemble\.mjs/)
|
|
54
71
|
})
|
|
55
72
|
|
|
56
73
|
it('names the passcode discipline', () => {
|
|
@@ -3,13 +3,18 @@ import { processUpload } from '../../skills/data-portal/template/functions/api/u
|
|
|
3
3
|
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
4
4
|
|
|
5
5
|
// The fake must model the constraints the schema actually declares, not just
|
|
6
|
-
// the happy path. `
|
|
7
|
-
// rather than a second row; a fake that accepts
|
|
8
|
-
// "exactly one manifest row" assertion pass while the
|
|
6
|
+
// the happy path. The table-level `UNIQUE (accountId, objectKey)` is what makes
|
|
7
|
+
// a re-upload an upsert rather than a second row; a fake that accepts
|
|
8
|
+
// duplicates would let the "exactly one manifest row" assertion pass while the
|
|
9
|
+
// real D1 threw. The store is shared by every portal, so the uniqueness is per
|
|
10
|
+
// account and never on objectKey alone.
|
|
9
11
|
function makeEnv() {
|
|
10
12
|
const manifestRows: unknown[][] = []
|
|
11
13
|
const objects = new Map<string, Uint8Array>()
|
|
12
14
|
const env = {
|
|
15
|
+
// This portal's own account, baked into wrangler.toml [vars] at assemble
|
|
16
|
+
// time. It is the tenant scope for every query and every object key.
|
|
17
|
+
PORTAL_ACCOUNT_ID: 'acct-a',
|
|
13
18
|
DB: {
|
|
14
19
|
prepare(query: string) {
|
|
15
20
|
let bound: unknown[] = []
|
|
@@ -20,11 +25,14 @@ function makeEnv() {
|
|
|
20
25
|
},
|
|
21
26
|
async run() {
|
|
22
27
|
if (/INSERT INTO manifest/i.test(query)) {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
28
|
+
const accountId = bound[1] as string
|
|
29
|
+
const objectKey = bound[4] as string
|
|
30
|
+
const existing = manifestRows.findIndex(
|
|
31
|
+
(r) => r[1] === accountId && r[4] === objectKey,
|
|
32
|
+
)
|
|
25
33
|
if (existing >= 0) {
|
|
26
|
-
if (!/ON CONFLICT\s*\(\s*objectKey\s*\)\s*DO UPDATE/i.test(query)) {
|
|
27
|
-
throw new Error('UNIQUE constraint failed: manifest.objectKey')
|
|
34
|
+
if (!/ON CONFLICT\s*\(\s*accountId\s*,\s*objectKey\s*\)\s*DO UPDATE/i.test(query)) {
|
|
35
|
+
throw new Error('UNIQUE constraint failed: manifest.accountId, manifest.objectKey')
|
|
28
36
|
}
|
|
29
37
|
manifestRows[existing] = bound
|
|
30
38
|
} else {
|
|
@@ -38,11 +46,19 @@ function makeEnv() {
|
|
|
38
46
|
},
|
|
39
47
|
async first<T>() {
|
|
40
48
|
if (/FROM sessions/i.test(query)) {
|
|
41
|
-
|
|
49
|
+
// Bound (sessionId, accountId, nowMs). The joined people row
|
|
50
|
+
// carries its own account, and resolveSession returns null unless
|
|
51
|
+
// that account equals the one it was asked about.
|
|
52
|
+
return (bound[0] === 'sess-a' && bound[1] === 'acct-a'
|
|
53
|
+
? { ownerId: 'alice', accountId: 'acct-a' }
|
|
54
|
+
: null) as T | null
|
|
42
55
|
}
|
|
43
56
|
if (/FROM manifest/i.test(query)) {
|
|
57
|
+
// Bound (objectKey, accountId).
|
|
44
58
|
const key = bound[0] as string
|
|
45
|
-
return (objects.has(key)
|
|
59
|
+
return (objects.has(key) && bound[1] === 'acct-a'
|
|
60
|
+
? { fileId: 'x' }
|
|
61
|
+
: null) as T | null
|
|
46
62
|
}
|
|
47
63
|
return null as T | null
|
|
48
64
|
},
|
|
@@ -72,7 +88,7 @@ function makeEnv() {
|
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
describe('processUpload', () => {
|
|
75
|
-
it('writes the object under the owner prefix and exactly one manifest row', async () => {
|
|
91
|
+
it('writes the object under the account and owner prefix and exactly one manifest row', async () => {
|
|
76
92
|
const { env, manifestRows, objects } = makeEnv()
|
|
77
93
|
const lines: string[] = []
|
|
78
94
|
const res = await processUpload(
|
|
@@ -85,7 +101,7 @@ describe('processUpload', () => {
|
|
|
85
101
|
() => 1_000_000,
|
|
86
102
|
)
|
|
87
103
|
expect(res.status).toBe(200)
|
|
88
|
-
expect([...objects.keys()]).toEqual(['alice/invoice.pdf'])
|
|
104
|
+
expect([...objects.keys()]).toEqual(['acct-a/alice/invoice.pdf'])
|
|
89
105
|
expect(manifestRows.length).toBe(1)
|
|
90
106
|
})
|
|
91
107
|
|
|
@@ -100,8 +116,8 @@ describe('processUpload', () => {
|
|
|
100
116
|
() => 'up-1',
|
|
101
117
|
() => 1_000_000,
|
|
102
118
|
)
|
|
103
|
-
// Column order: fileId, ownerId, filename, objectKey, size, uploadedAt, ingested
|
|
104
|
-
expect(manifestRows[0][
|
|
119
|
+
// Column order: fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested
|
|
120
|
+
expect(manifestRows[0][7]).toBe(0)
|
|
105
121
|
})
|
|
106
122
|
|
|
107
123
|
it('emits the full lifeline correlated by uploadId, ending in a verified post-condition', async () => {
|
|
@@ -139,7 +155,7 @@ describe('processUpload', () => {
|
|
|
139
155
|
expect(manifestRows.length).toBe(0)
|
|
140
156
|
})
|
|
141
157
|
|
|
142
|
-
it('rejects a filename that escapes the owner prefix, and writes nothing', async () => {
|
|
158
|
+
it('rejects a filename that escapes the account and owner prefix, and writes nothing', async () => {
|
|
143
159
|
const { env, objects, manifestRows } = makeEnv()
|
|
144
160
|
const res = await processUpload(
|
|
145
161
|
'sess-a',
|
|
@@ -230,16 +246,16 @@ describe('processUpload', () => {
|
|
|
230
246
|
)
|
|
231
247
|
expect(second.status).toBe(200)
|
|
232
248
|
expect(manifestRows.length).toBe(1)
|
|
233
|
-
expect(objects.get('alice/invoice.pdf')?.length).toBe(3)
|
|
249
|
+
expect(objects.get('acct-a/alice/invoice.pdf')?.length).toBe(3)
|
|
234
250
|
})
|
|
235
251
|
|
|
236
252
|
it('updates the row to describe the new object, not the old one', async () => {
|
|
237
253
|
const { env, manifestRows } = makeEnv()
|
|
238
254
|
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
239
255
|
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
240
|
-
// Column order: fileId, ownerId, filename, objectKey, size, uploadedAt, ingested
|
|
241
|
-
expect(manifestRows[0][
|
|
242
|
-
expect(manifestRows[0][
|
|
256
|
+
// Column order: fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested
|
|
257
|
+
expect(manifestRows[0][5]).toBe(3)
|
|
258
|
+
expect(manifestRows[0][6]).toBe(new Date(2_000).toISOString())
|
|
243
259
|
})
|
|
244
260
|
|
|
245
261
|
it('re-arms ingestion by resetting ingested to 0', async () => {
|
|
@@ -248,9 +264,9 @@ describe('processUpload', () => {
|
|
|
248
264
|
// ingested, and nothing would ever say so.
|
|
249
265
|
const { env, manifestRows } = makeEnv()
|
|
250
266
|
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([1]), env, () => {}, () => 'up-1', () => 1_000)
|
|
251
|
-
manifestRows[0][
|
|
267
|
+
manifestRows[0][7] = 1 // the device-side sweep marks it ingested
|
|
252
268
|
await processUpload('sess-a', 'invoice.pdf', '', new Uint8Array([9, 9, 9]), env, () => {}, () => 'up-2', () => 2_000)
|
|
253
|
-
expect(manifestRows[0][
|
|
269
|
+
expect(manifestRows[0][7]).toBe(0)
|
|
254
270
|
})
|
|
255
271
|
})
|
|
256
272
|
})
|
|
@@ -25,34 +25,81 @@ Uploads must keep succeeding while the device is offline, which is the same reas
|
|
|
25
25
|
went to Pages. The install's magic-link gate cannot be reused: it gates `/api/chat` on the install
|
|
26
26
|
and its sessions live in memory, so it does not cross origins.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## One shared store, and provisioning is one session
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
and
|
|
30
|
+
Every portal binds the **same** house-owned D1 database and the **same** house-owned R2 bucket. They
|
|
31
|
+
are created once, for the whole install, and reused by every client thereafter. Do **not** create a
|
|
32
|
+
database or a bucket per client: that is what put the account on Cloudflare's ten-database cap and
|
|
33
|
+
blocked the eleventh portal outright.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
2. **In the admin session**, do everything else: the D1 database, the assembly, recording the Pages
|
|
36
|
-
project to the client account with `storage-pages-adopt`, and then the deploy.
|
|
35
|
+
Their identity lives in three keys in `platform/config/cloudflare-house.env`, written once when the
|
|
36
|
+
store is created and never retyped:
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
```
|
|
39
|
+
DATA_PORTAL_DB_NAME=<the one shared database>
|
|
40
|
+
DATA_PORTAL_DB_ID=<its id>
|
|
41
|
+
DATA_PORTAL_BUCKET=<the one shared bucket>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That file is house-only and is preserved across upgrades. A new file under `platform/config/` would
|
|
45
|
+
not be: the installer wipes that directory and restores only this one.
|
|
46
|
+
|
|
47
|
+
Because the store already exists, **provisioning is a single house session**. There is no client-side
|
|
48
|
+
step. What is still per client: the Pages project and its custom domain, the account's own
|
|
49
|
+
`PORTAL_FETCH_SECRET`, enrolment of the people, and the assembled tree.
|
|
50
|
+
|
|
51
|
+
Isolation is no longer one store per client. It is an `accountId` column on every row and an
|
|
52
|
+
`<accountId>/` prefix on every object key, with the portal's own account baked into its worker as
|
|
53
|
+
`PORTAL_ACCOUNT_ID` at assemble time. The worker binds that value into every query and every key
|
|
54
|
+
check, so it is the one thing separating this client's rows and objects from another's.
|
|
55
|
+
|
|
56
|
+
`data-portal.json` must carry `"storageModel": "shared"`. An absent value means a pre-2099 portal on
|
|
57
|
+
its own dedicated database.
|
|
58
|
+
|
|
59
|
+
The house-side loops do not take that field at its word. They compare the account's `portalDbName`
|
|
60
|
+
and `bucketName` against the shared names in `cloudflare-house.env` and use what the config actually
|
|
61
|
+
NAMES, logging `op=storage-model result=overridden` when the two disagree. The label is hand-written
|
|
62
|
+
in a hand-maintained file, and trusting it is fail-open in the one direction that leaks: a config
|
|
63
|
+
naming the shared database whose label was dropped or mis-cased would otherwise be read store-wide,
|
|
64
|
+
and the uploads pull would carry another client's files into this account's tree. Write the field
|
|
65
|
+
correctly anyway; it is what the assemble step reads, and it is the fallback before cutover. Those keep their own store **and their own deployed worker**: this
|
|
66
|
+
template binds `accountId` in every query and their database has no such column, so a dedicated
|
|
67
|
+
account is refused at assemble rather than redeployed. Migrating one is deliberate work.
|
|
68
|
+
|
|
69
|
+
The old two-session split is gone, and so is the reason given for it. It existed so the client
|
|
70
|
+
account would own the bucket, on the grounds that its ingestion sweep would otherwise be denied
|
|
71
|
+
`cross-account` against its own files. That was not so: the sweep (`portal-uploads-pull`) runs
|
|
72
|
+
house-level through the house credential and is not owner-gated. The split's real effect was that
|
|
73
|
+
two sessions filled the same store name independently and disagreed, which is the bug this design
|
|
74
|
+
removes.
|
|
42
75
|
|
|
43
76
|
## Enrol the people first
|
|
44
77
|
|
|
45
78
|
Collect from the operator the portal's custom domain and the list of people, then write
|
|
46
|
-
`<accountDir>/data-portal.json` carrying `portalDomain`, `
|
|
47
|
-
`
|
|
79
|
+
`<accountDir>/data-portal.json` carrying `portalDomain`, `accountId`, `projectName`,
|
|
80
|
+
`"storageModel": "shared"`, `portalDbName`, `bucketName`, and a `people` array of
|
|
81
|
+
`{ name, ownerId, prefix }`.
|
|
82
|
+
|
|
83
|
+
`portalDbName` and `bucketName` are the SHARED store's names, identical in every account's file and
|
|
84
|
+
copied from `cloudflare-house.env`. They stay in this file because the house-side loops (the uploads
|
|
85
|
+
pull, the index push, the audit) enumerate accounts by it and read the store names from it, which is
|
|
86
|
+
what lets shared and pre-2099 dedicated portals run side by side with no other branching.
|
|
48
87
|
|
|
49
88
|
For each person:
|
|
50
89
|
|
|
51
90
|
```bash
|
|
52
91
|
node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
|
|
53
|
-
--account <accountId> [--folders <a,b,c>]
|
|
92
|
+
--account <accountId> [--storage-model shared|dedicated] [--folders <a,b,c>] \
|
|
93
|
+
| npx wrangler d1 execute <portalDbName> --remote --file=-
|
|
54
94
|
```
|
|
55
95
|
|
|
96
|
+
`--storage-model` picks the upsert's conflict target and defaults to `shared`. It must match the
|
|
97
|
+
account's `data-portal.json`, because the two `people` tables have different keys: the shared one is
|
|
98
|
+
keyed `(accountId, ownerId)` so two clients can each enrol a person of the same name, and a
|
|
99
|
+
pre-2099 one is keyed on `ownerId` alone. Naming the wrong pair does not degrade, it fails the whole
|
|
100
|
+
statement, so a grandfathered portal must pass `--storage-model dedicated` or its passcode rotation
|
|
101
|
+
stops working. That failure is loud and happens at the database before any row moves.
|
|
102
|
+
|
|
56
103
|
The script mints the passcode and prints it **once to stderr**, so it reaches the operator's terminal
|
|
57
104
|
and never the pipe; stdout carries the `INSERT` alone. Do not hand-roll this: the passcode's length
|
|
58
105
|
and alphabet are pinned in `template/functions/api/_lib/passcode.mjs`, the script is what applies
|
|
@@ -93,9 +140,22 @@ files. The gate enforces it too, but do not enrol a name it will reject.
|
|
|
93
140
|
|
|
94
141
|
## Assemble the canonical tree
|
|
95
142
|
|
|
96
|
-
Copy `template/` to `<accountDir>/sites/<project
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
Copy `template/` to `<accountDir>/sites/<project>/`, then fill `wrangler.toml` with the script. Do
|
|
144
|
+
**not** fill it by hand:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
node platform/plugins/cloudflare/bin/portal-assemble.mjs \
|
|
148
|
+
--account-dir <accountDir> --platform-root platform \
|
|
149
|
+
--install-origin https://<public-tunnel-hostname> \
|
|
150
|
+
--template platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
It reads the shared store's identity from `cloudflare-house.env` and the account's own id from
|
|
154
|
+
`data-portal.json`, substitutes every placeholder including `__PORTAL_ACCOUNT_ID__`, and fails closed
|
|
155
|
+
with the offending name when a shared key is missing, when the account is on the dedicated model, or
|
|
156
|
+
when any placeholder survives. Hand-filling these values is exactly what shipped a portal whose
|
|
157
|
+
bucket name and config disagreed, so the one right answer comes from one file and is substituted
|
|
158
|
+
mechanically.
|
|
99
159
|
|
|
100
160
|
`sites/<project>/` is the only location the broker will publish: `resolveSiteDir` resolves
|
|
101
161
|
`<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat: `index.html`, `portal.css`, `portal.js`,
|
|
@@ -107,10 +167,11 @@ config does not declare publishes the site one level too deep, which serves 404
|
|
|
107
167
|
The deploy serves everything in that tree except a fixed ignore list — `functions/`, `_worker.js`,
|
|
108
168
|
`_redirects`, `_headers`, `_routes.json`, `node_modules`, `.git`, `.DS_Store`, `.wrangler` — and
|
|
109
169
|
`pages deploy` reads no `.assetsignore`, so anything else you leave there is public. Nothing else
|
|
110
|
-
belongs in it. `schema.sql` therefore lives beside `template/` in the skill, not inside it.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`IF NOT EXISTS` throughout, so
|
|
170
|
+
belongs in it. `schema.sql` therefore lives beside `template/` in the skill, not inside it. It is
|
|
171
|
+
applied ONCE, to the one shared database named by `DATA_PORTAL_DB_NAME`, when that store is first
|
|
172
|
+
created. A new client portal needs no schema step at all: the tables are already there, and its rows
|
|
173
|
+
are separated from every other client's by `accountId`. The file is `IF NOT EXISTS` throughout, so
|
|
174
|
+
re-applying is safe.
|
|
114
175
|
|
|
115
176
|
Safe, but not a migration: `IF NOT EXISTS` adds no column to a table that already exists. A portal
|
|
116
177
|
whose `directory` table predates the `isDir` column keeps failing every index push, because the push
|
|
@@ -356,4 +417,6 @@ event at request time.
|
|
|
356
417
|
## Tool discipline
|
|
357
418
|
|
|
358
419
|
Never write a token into the tree, never commit one, never echo one. The bucket name and database
|
|
359
|
-
name come from `bin/
|
|
420
|
+
name come from `platform/config/cloudflare-house.env` by way of `bin/portal-assemble.mjs`, never
|
|
421
|
+
hand-built and never retyped. (`bin/cf-store-name.sh` still names per-account stores for booking and
|
|
422
|
+
other skills; the portal no longer has one.)
|