@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
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Task 2097 — `email-signature-set` implementation plan
|
|
2
|
+
|
|
3
|
+
Design: [`2026-07-28-task-2097-email-signature-set-design.md`](../specs/2026-07-28-task-2097-email-signature-set-design.md).
|
|
4
|
+
|
|
5
|
+
## Registration surfaces — corrected against the tree
|
|
6
|
+
|
|
7
|
+
The task file names `ADMIN_CORE_TOOLS`. It does not exist: `grep -rn 'ADMIN_CORE_TOOLS' platform/` returns
|
|
8
|
+
nothing outside comments. The live surfaces, each confirmed by reading the file, are four:
|
|
9
|
+
|
|
10
|
+
| # | Surface | Evidence |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| 1 | `plugins/email/mcp/src/index.ts` | `eagerTool(server, "email-setup", …)` at line 124 |
|
|
13
|
+
| 2 | `plugins/email/PLUGIN.md` | frontmatter `description:`, `tools:` list, Capabilities, Skills table |
|
|
14
|
+
| 3 | `platform/templates/specialists/agents/personal-assistant.md` | the only template matching `grep -rl email-setup platform/templates/` |
|
|
15
|
+
| 4 | `services/claude-session-manager/src/canonical-tool-names.generated.ts` | header: generated from `PLUGIN.md` `tools:` blocks, `check-canonical-tool-names.mjs` fails the build on drift |
|
|
16
|
+
|
|
17
|
+
Surface 4 is not in the task file and would have broken the build. It is regenerated, never hand-edited:
|
|
18
|
+
`cd platform && npm run gen:canonical-tools`.
|
|
19
|
+
|
|
20
|
+
Three further files match `email-draft-send` and are deliberately **not** touched:
|
|
21
|
+
`plugins/admin/hooks/preference-consult-gate.sh`, `scripts/lib/provision-account-dir.sh`, and the
|
|
22
|
+
`plugins/admin/PLUGIN.md` prose describing them. All three are the outbound-send preference gate,
|
|
23
|
+
matching sends that carry attachments. `email-signature-set` sends nothing, so it is not a gate member.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **`lib/signature.ts`** — add `SIGNATURE_BASENAME`, `signatureDirFor(accountId)`, and
|
|
28
|
+
`saveSignature(accountDir, parts)`. `loadSignature` / `applySignature` unchanged.
|
|
29
|
+
`signatureDirFor` is `accountDirFor(accountId) || process.env.ACCOUNT_DIR || undefined`, which is the
|
|
30
|
+
expression `compose.ts:112` already computes, with an empty-string env normalised to `undefined`
|
|
31
|
+
(both already load as no-signature, so behaviour is unchanged). No import cycle: `attachment-resolve.ts`
|
|
32
|
+
imports only `node:fs` and `node:path`.
|
|
33
|
+
2. **`lib/compose.ts`** — replace the inline `sigDir` expression with `signatureDirFor(accountId)` and the
|
|
34
|
+
literal `"email-signature"` in `assetNote` with `SIGNATURE_BASENAME`. `accountDirFor` stays imported for
|
|
35
|
+
the attachment path at line 75.
|
|
36
|
+
3. **`tools/email-signature-set.ts`** — refuse an all-absent call and an unresolvable directory before any
|
|
37
|
+
write, call `saveSignature`, log, return the resolved paths.
|
|
38
|
+
4. **`index.ts`** — import and `eagerTool` register, placed after the `email-setup` block. Guard with
|
|
39
|
+
`refuseNoAccount`, wrap in try/catch like `email-setup`. The handler's local result variable is named
|
|
40
|
+
`message`, not `text`, so it cannot shadow the `text` parameter.
|
|
41
|
+
5. **`PLUGIN.md`** — `tools:` entry (`riskClass: write_local`, both allowlists false, matching `email-setup`),
|
|
42
|
+
a Config clause in the frontmatter `description:`, a Capabilities bullet, a Skills-table row.
|
|
43
|
+
6. **`personal-assistant.md`** — add `mcp__plugin_email_email__email-signature-set` after `email-setup`.
|
|
44
|
+
7. **Regenerate** `canonical-tool-names.generated.ts`.
|
|
45
|
+
8. **`skills/email-signature/SKILL.md`** — new skill.
|
|
46
|
+
9. **`skills/email-composition/SKILL.md`** — one added rule, see below.
|
|
47
|
+
10. **`__tests__/email-signature-set.test.ts`** — committed, not ephemeral: the plugin already carries 34
|
|
48
|
+
test files and the task file mandates a regression test.
|
|
49
|
+
|
|
50
|
+
## Addition beyond the task file
|
|
51
|
+
|
|
52
|
+
The task specifies that the new `email-signature` skill carries the rule "never hand-build a signature
|
|
53
|
+
inside a draft body". That skill activates when the operator asks to set or change a signature. The
|
|
54
|
+
incident happened while **drafting**, when that skill is not loaded. `grep -in signature` over
|
|
55
|
+
`skills/email-composition/SKILL.md` returns zero hits, so the drafting surface currently says nothing
|
|
56
|
+
about signatures.
|
|
57
|
+
|
|
58
|
+
So the rule is also stated in one sentence in `email-composition`, which is the skill that is loaded while
|
|
59
|
+
drafting. Without it the doctrine lives only in a skill absent at the moment it is needed. Declared in Plan
|
|
60
|
+
Conformity as an addition.
|
|
61
|
+
|
|
62
|
+
## Verification
|
|
63
|
+
|
|
64
|
+
`npm run build` and `npx vitest run` in `plugins/email/mcp`, then `npm run gen:canonical-tools` and the
|
|
65
|
+
`check-canonical-tool-names.mjs` gate in `platform/`. No browser surface, no Pi hook surface.
|
package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Task 2097 — `email-signature-set` design
|
|
2
|
+
|
|
3
|
+
Date 2026-07-28. Lane *Platform / email · signature provisioning*.
|
|
4
|
+
Task file: [`.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md`](../../../../.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md).
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`email-draft` appends an account's signature deterministically, with no model judgement, at
|
|
9
|
+
[`compose.ts:113`](../../../plugins/email/mcp/src/lib/compose.ts). It calls `loadSignature(sigDir)` then
|
|
10
|
+
`applySignature`, where `loadSignature` (Task 1952,
|
|
11
|
+
[`signature.ts:24`](../../../plugins/email/mcp/src/lib/signature.ts)) reads three fixed-convention files at the
|
|
12
|
+
account root: `email-signature.txt`, `email-signature.html`, `email-signature.font`.
|
|
13
|
+
|
|
14
|
+
Nothing in the platform writes those files. Task 1952 shipped the reader with no writer, so the
|
|
15
|
+
path-and-format convention lives only in code and no account can be provisioned. On the G.L. Smith
|
|
16
|
+
account none of the three exist, `applied` was false, no signature was appended, and a drafting
|
|
17
|
+
subagent with no file-read tool reconstructed the HTML from a memory node and pulled a stale logo
|
|
18
|
+
into a client draft.
|
|
19
|
+
|
|
20
|
+
The failure is not a model ignoring a rule. The deterministic path works; it had no files to read.
|
|
21
|
+
|
|
22
|
+
## Outcome
|
|
23
|
+
|
|
24
|
+
An account's signature is installed into the loader's exact path and format through a sanctioned,
|
|
25
|
+
discoverable tool, and every subsequent draft carries it byte-for-byte with no model-authored
|
|
26
|
+
signature markup.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
`lib/signature.ts` becomes the single owner of the signature convention in both directions. Read and
|
|
31
|
+
write cannot drift because one file defines the base path, the three extensions, and the font
|
|
32
|
+
trimming for both.
|
|
33
|
+
|
|
34
|
+
| Unit | Responsibility | Depends on |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `lib/signature.ts` → `signatureDirFor(accountId)` | Resolve the one directory an account's signature lives in | `attachment-resolve.ts` → `accountDirFor`, `process.env.ACCOUNT_DIR` |
|
|
37
|
+
| `lib/signature.ts` → `saveSignature(accountDir, parts)` | Write present parts, remove absent ones, report both | `node:fs` |
|
|
38
|
+
| `lib/signature.ts` → `loadSignature` / `applySignature` | Unchanged (Task 1952) | — |
|
|
39
|
+
| `tools/email-signature-set.ts` | Param refusals, call `saveSignature`, log, format the response | `lib/signature.ts` |
|
|
40
|
+
| `lib/compose.ts` | Swaps its inline dir expression for `signatureDirFor` | `lib/signature.ts` |
|
|
41
|
+
|
|
42
|
+
### Directory resolution
|
|
43
|
+
|
|
44
|
+
`compose.ts:113` currently resolves the read directory inline as
|
|
45
|
+
`accountDirFor(accountId) || process.env.ACCOUNT_DIR`. The setter must write to the identical
|
|
46
|
+
directory in every environment, so that expression moves into `signatureDirFor(accountId)` and both
|
|
47
|
+
sides call it. Substituting the identical expression leaves `compose.ts` behaviour unchanged; the
|
|
48
|
+
point is that "which directory" now has one definition rather than two copies that can diverge.
|
|
49
|
+
|
|
50
|
+
Writing to `accountDirFor(accountId)` alone was rejected: on a spawn where `PLATFORM_ROOT` is absent
|
|
51
|
+
but `ACCOUNT_DIR` is set, the setter would refuse while drafts would have read the file fine.
|
|
52
|
+
|
|
53
|
+
## Behaviour
|
|
54
|
+
|
|
55
|
+
Parameters, all optional strings: `text`, `html`, `font`. The account is the caller's `accountId`,
|
|
56
|
+
the same authority `email-draft` uses. There is no `mailbox` parameter — a signature is account-scoped,
|
|
57
|
+
because `loadSignature` reads one account root and knows nothing about mailboxes.
|
|
58
|
+
|
|
59
|
+
| Input | Effect on `email-signature.<ext>` |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `text` present | `.txt` written byte-for-byte verbatim, no reflow, no re-encoding |
|
|
62
|
+
| `html` present | `.html` written byte-for-byte verbatim |
|
|
63
|
+
| `font` present | `.font` written trimmed, matching `loadSignature`'s trim-on-read |
|
|
64
|
+
| any one absent | that file removed if it exists; absence of the file is not an error |
|
|
65
|
+
| all three absent | refused, nothing on disk is touched |
|
|
66
|
+
| directory unresolvable | refused, nothing on disk is touched |
|
|
67
|
+
|
|
68
|
+
Absent and empty are different inputs. A parameter is absent only when it is not supplied; a supplied
|
|
69
|
+
empty string is present, so it writes an empty file and counts toward the all-three-absent check. That
|
|
70
|
+
follows from writing verbatim: the tool stores what it was given rather than second-guessing it. The
|
|
71
|
+
consequence is that an empty `.txt` makes the next draft report `applied` true while appending
|
|
72
|
+
nothing visible, which is why an empty string is a deliberate act and omission is the way to remove.
|
|
73
|
+
|
|
74
|
+
The removal rule is what makes a text-only call clear a stale `.html` rather than leave it to keep
|
|
75
|
+
appending. The all-three-absent refusal is what stops a mis-shaped call silently wiping a working
|
|
76
|
+
signature; removal stays reachable, but only as a side effect of setting something.
|
|
77
|
+
|
|
78
|
+
The response names the resolved paths written and the resolved paths removed, so the caller sees the
|
|
79
|
+
account root it landed in rather than trusting it.
|
|
80
|
+
|
|
81
|
+
## Failure handling
|
|
82
|
+
|
|
83
|
+
Files are written in the fixed order `.txt`, `.html`, `.font`. A filesystem error surfaces verbatim
|
|
84
|
+
naming the offending path and the paths already written, so a half-applied state is reported rather
|
|
85
|
+
than hidden. There is no transactional rollback: the task does not ask for one, and reaching the
|
|
86
|
+
partial state requires a filesystem fault mid-call.
|
|
87
|
+
|
|
88
|
+
## Observability
|
|
89
|
+
|
|
90
|
+
`[email-signature-set] op=write accountId=<first-8> files=<written> removed=<removed>` on every
|
|
91
|
+
successful write. The task specifies the line through `files=`; `removed=` is added because the task
|
|
92
|
+
mandates removal as a behaviour and mandates that provisioning be auditable, and an unlogged removal
|
|
93
|
+
is not auditable.
|
|
94
|
+
|
|
95
|
+
The existing `[email-draft] op=signature applied=<bool> asset=<path>` line stays the per-draft
|
|
96
|
+
signal. An account that has run the setter shows `applied=true` on its next draft. That pair is the
|
|
97
|
+
end-to-end evidence: a write line naming the files, then a draft line reporting them applied.
|
|
98
|
+
|
|
99
|
+
## Registration
|
|
100
|
+
|
|
101
|
+
`ADMIN_CORE_TOOLS` is named in the task file but does not exist — `grep -rn 'ADMIN_CORE_TOOLS\s*[:=]'`
|
|
102
|
+
over `platform/` returns nothing, and the name survives only in comments. The live surfaces are:
|
|
103
|
+
|
|
104
|
+
1. `mcp/src/index.ts` — `eagerTool(server, name, description, zodShape, handler)`.
|
|
105
|
+
2. `PLUGIN.md` — a `tools:` entry (`riskClass: write_local`, `publicAllowlist: false`,
|
|
106
|
+
`adminAllowlist: false`, matching `email-setup`), a clause in the frontmatter `description:` prose,
|
|
107
|
+
an operator-docs bullet, and a skills-table row for the new skill.
|
|
108
|
+
3. `platform/templates/specialists/agents/personal-assistant.md` — the `tools:` frontmatter grant, the
|
|
109
|
+
canonical per-tool permission surface since Task 453.
|
|
110
|
+
|
|
111
|
+
`riskClass: write_local` is correct because the tool writes account-local files and sends nothing
|
|
112
|
+
outbound, which is the same class `email-setup` carries.
|
|
113
|
+
|
|
114
|
+
## Skill
|
|
115
|
+
|
|
116
|
+
`platform/plugins/email/skills/email-signature/SKILL.md`, activated when the operator asks to set or
|
|
117
|
+
change an email signature. It states the loader convention, and three standing rules:
|
|
118
|
+
|
|
119
|
+
- A signature is installed only through `email-signature-set`. Hand-writing the three files is not a
|
|
120
|
+
documented path.
|
|
121
|
+
- The agent never hand-builds a signature inside a draft body, because `email-draft` applies the
|
|
122
|
+
stored one automatically and a hand-built one both duplicates it and drifts from the asset.
|
|
123
|
+
- The signature content is read from the operator's own source of truth and passed through verbatim.
|
|
124
|
+
Reconstructing markup from memory or from stripped graph text is what put the stale logo in a
|
|
125
|
+
client draft.
|
|
126
|
+
|
|
127
|
+
Written as outcomes and constraints, not a step recipe, per `maxy-code/CLAUDE.md`.
|
|
128
|
+
|
|
129
|
+
## Testing
|
|
130
|
+
|
|
131
|
+
Added to the plugin's existing vitest surface at `mcp/src/__tests__/`, not substituted for it.
|
|
132
|
+
|
|
133
|
+
- Round trip: set all three, `loadSignature` returns the exact values, `applySignature` reports
|
|
134
|
+
`applied` true.
|
|
135
|
+
- Verbatim: text and html land byte-identical, trailing newlines and all; font lands trimmed.
|
|
136
|
+
- Text-only call writes `.txt` and removes a pre-existing `.html` and `.font`, so a later draft
|
|
137
|
+
carries no stale HTML.
|
|
138
|
+
- All-three-absent call is refused and leaves pre-existing files untouched.
|
|
139
|
+
- Unresolvable directory is refused.
|
|
140
|
+
- A draft composed after a successful set carries body and html exactly equal to `applySignature`
|
|
141
|
+
output, with no model-authored signature text.
|
|
142
|
+
- `signatureDirFor` prefers `accountDirFor` and falls back to `ACCOUNT_DIR`.
|
|
143
|
+
- A supplied empty string writes an empty file and does not count as absent.
|
|
144
|
+
- Existing `signature.test.ts` stays green.
|
|
145
|
+
|
|
146
|
+
## Out of scope
|
|
147
|
+
|
|
148
|
+
- Making `applied=false` a fail-closed refusal on client-facing drafts. Operator scoped this out on
|
|
149
|
+
2026-07-28 as separate hardening; deliberately not filed.
|
|
150
|
+
- The one-off G.L. Smith provisioning and the stale memory-node deletion. That is a runtime action on
|
|
151
|
+
the live install, performed through the tool this task ships.
|
|
152
|
+
- Any change to `loadSignature` / `applySignature` behaviour (Task 1952). `signatureDirFor` moves an
|
|
153
|
+
expression out of `compose.ts`; neither function's behaviour changes.
|
|
154
|
+
- The drafts list/verify capability, which is
|
|
155
|
+
[Task 2098](../../../../.tasks/pending/2098-email-drafts-list-verify-tool-so-draft-presence-is-confirmable.md).
|
|
@@ -9,6 +9,12 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-28 (0.1.520)
|
|
13
|
+
|
|
14
|
+
- The admin now lands on the Dashboard, with a Public pages card linking the booking site and file-drop portal.
|
|
15
|
+
- The scheduling read surface now returns meetings alongside events, so a note-only meeting update explains why instead of saying not found.
|
|
16
|
+
- New email tools: a signature setter to populate the deterministic signature loader, and a drafts list to confirm a draft is really there.
|
|
17
|
+
|
|
12
18
|
## 2026-07-28 (0.1.519)
|
|
13
19
|
|
|
14
20
|
- The admin webchat now delivers the assistant's answer as the turn's final text, and the separate reply tool has been retired for admin sessions (public visitors still get it).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// portal-assemble.mjs - fill a data portal's wrangler.toml from the shared
|
|
3
|
+
// house store identity and the account's own config.
|
|
4
|
+
//
|
|
5
|
+
// This exists because hand-filling those values is what broke a portal (Task
|
|
6
|
+
// 2099). The skill used to say "fill __D1_DATABASE_NAME__, __D1_DATABASE_ID__
|
|
7
|
+
// and __R2_BUCKET_NAME__, leave no placeholder", and two sessions resolving the
|
|
8
|
+
// same placeholder independently chose different names: a bucket was created as
|
|
9
|
+
// <acct>-portal while the config named <acct>-data-portal, and cross-account
|
|
10
|
+
// list isolation meant neither session could see the other's resource. The
|
|
11
|
+
// platform rule is to take the model out of a decision that has one right
|
|
12
|
+
// answer, so the values now come from one file and are substituted here.
|
|
13
|
+
//
|
|
14
|
+
// Every refusal below is fail-closed and names what is wrong. A portal that
|
|
15
|
+
// deploys with a blank binding is worse than one that does not deploy: it
|
|
16
|
+
// serves, and every upload lands nowhere.
|
|
17
|
+
//
|
|
18
|
+
// Pure core (assembleTree) is separated from the CLI so the refusals are
|
|
19
|
+
// testable without a filesystem.
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { basename, join, resolve } from 'node:path'
|
|
23
|
+
|
|
24
|
+
/** The three keys the shared store is identified by, in cloudflare-house.env. */
|
|
25
|
+
export const SHARED_KEYS = ['DATA_PORTAL_DB_NAME', 'DATA_PORTAL_DB_ID', 'DATA_PORTAL_BUCKET']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse KEY=value lines without polluting process.env. Same discipline as
|
|
29
|
+
* readHouseCredential in lib/storage-broker, and the same file is being read.
|
|
30
|
+
* @param {string} raw
|
|
31
|
+
* @returns {Record<string,string>}
|
|
32
|
+
*/
|
|
33
|
+
export function parseEnvFile(raw) {
|
|
34
|
+
/** @type {Record<string,string>} */
|
|
35
|
+
const env = {}
|
|
36
|
+
for (const line of raw.split('\n')) {
|
|
37
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
|
|
38
|
+
if (m) env[m[1]] = m[2].trim()
|
|
39
|
+
}
|
|
40
|
+
return env
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Substitute every placeholder in the template wrangler.toml.
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* toml: string,
|
|
48
|
+
* shared: Record<string,string>,
|
|
49
|
+
* cfg: { accountId?: string, projectName?: string, storageModel?: string },
|
|
50
|
+
* installOrigin: string,
|
|
51
|
+
* expectedAccountId?: string,
|
|
52
|
+
* }} opts
|
|
53
|
+
* @returns {string} the assembled wrangler.toml
|
|
54
|
+
*/
|
|
55
|
+
export function assembleTree({ toml, shared, cfg, installOrigin, expectedAccountId }) {
|
|
56
|
+
// The storage model is checked FIRST, because a dedicated account is not a
|
|
57
|
+
// missing-value problem to report but a portal this template cannot serve at
|
|
58
|
+
// all: its database has no accountId column, and every query this template
|
|
59
|
+
// emits binds one. Absent means dedicated, so a pre-2099 config file that
|
|
60
|
+
// never heard of the field is refused rather than silently treated as shared.
|
|
61
|
+
const model = cfg?.storageModel ?? 'dedicated'
|
|
62
|
+
if (model !== 'shared') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`portal-assemble: account ${cfg?.accountId ?? '(unnamed)'} is on the "${model}" storage ` +
|
|
65
|
+
`model. This template targets the shared house store and binds accountId in every query, ` +
|
|
66
|
+
`which a dedicated database does not have. Pre-2099 portals stay on their own template; ` +
|
|
67
|
+
`migrating one is deliberate work, not a redeploy.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const key of SHARED_KEYS) {
|
|
72
|
+
if (!shared?.[key]) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`portal-assemble: ${key} is missing or empty in the house config. The shared store must ` +
|
|
75
|
+
`exist and be recorded before any portal can bind it; assembling without it would ` +
|
|
76
|
+
`deploy a portal whose binding points at nothing.`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!cfg?.accountId) {
|
|
81
|
+
throw new Error('portal-assemble: accountId is missing from data-portal.json')
|
|
82
|
+
}
|
|
83
|
+
// PORTAL_ACCOUNT_ID becomes the tenant scope for every query and every object
|
|
84
|
+
// key the deployed worker issues, and it is baked in permanently at this
|
|
85
|
+
// point. The house-side loops do NOT read it: they scope by the account
|
|
86
|
+
// DIRECTORY name. So the two must be the same string, or the worker writes
|
|
87
|
+
// rows and objects under one tenant while the pull, the index push and the
|
|
88
|
+
// audit look under another. Nothing downstream would report that: the reads
|
|
89
|
+
// simply return nothing and the client's files never arrive.
|
|
90
|
+
if (expectedAccountId && cfg.accountId !== expectedAccountId) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`portal-assemble: data-portal.json says accountId "${cfg.accountId}" but the account ` +
|
|
93
|
+
`directory is "${expectedAccountId}". The house-side loops scope by the directory, so ` +
|
|
94
|
+
`baking the other value would strand every upload. Fix the config, not this check.`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
// The same charset the deployed worker's authorizeKey enforces on both key
|
|
98
|
+
// segments. Baking a value it will reject means every upload 403s after
|
|
99
|
+
// deploy rather than here.
|
|
100
|
+
if (!/^[a-z0-9-]+$/.test(cfg.accountId)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`portal-assemble: accountId "${cfg.accountId}" is outside [a-z0-9-], which the portal's own ` +
|
|
103
|
+
`key gate rejects, so every upload would be denied after deploy.`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (!cfg?.projectName) {
|
|
107
|
+
throw new Error('portal-assemble: projectName is missing from data-portal.json')
|
|
108
|
+
}
|
|
109
|
+
if (!installOrigin) {
|
|
110
|
+
throw new Error('portal-assemble: install origin is required (the public host, not the admin host)')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const out = toml
|
|
114
|
+
.replaceAll('__PROJECT_NAME__', cfg.projectName)
|
|
115
|
+
.replaceAll('__D1_DATABASE_NAME__', shared.DATA_PORTAL_DB_NAME)
|
|
116
|
+
.replaceAll('__D1_DATABASE_ID__', shared.DATA_PORTAL_DB_ID)
|
|
117
|
+
.replaceAll('__R2_BUCKET_NAME__', shared.DATA_PORTAL_BUCKET)
|
|
118
|
+
.replaceAll('__PORTAL_ACCOUNT_ID__', cfg.accountId)
|
|
119
|
+
.replaceAll('__PORTAL_INSTALL_ORIGIN__', installOrigin)
|
|
120
|
+
|
|
121
|
+
// The post-condition, verified rather than assumed. A placeholder that
|
|
122
|
+
// survives substitution is a value nobody supplied, and wrangler would
|
|
123
|
+
// happily deploy the literal string.
|
|
124
|
+
const left = out.match(/__[A-Z0-9_]+__/g)
|
|
125
|
+
if (left) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`portal-assemble: placeholder(s) left unfilled: ${[...new Set(left)].join(', ')}`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {string} msg */
|
|
134
|
+
function die(msg) {
|
|
135
|
+
console.error(`[portal-assemble] error: ${msg}`)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {string} flag */
|
|
140
|
+
function arg(flag) {
|
|
141
|
+
const i = process.argv.indexOf(flag)
|
|
142
|
+
return i === -1 ? undefined : process.argv[i + 1]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Run only as a CLI, so the pure core above stays importable by the tests.
|
|
146
|
+
if (process.argv[1] && process.argv[1].endsWith('portal-assemble.mjs')) {
|
|
147
|
+
const accountDir = arg('--account-dir')
|
|
148
|
+
const platformRoot = arg('--platform-root')
|
|
149
|
+
const installOrigin = arg('--install-origin')
|
|
150
|
+
const templateToml = arg('--template')
|
|
151
|
+
if (!accountDir) die('--account-dir <dir> is required')
|
|
152
|
+
if (!platformRoot) die('--platform-root <dir> is required')
|
|
153
|
+
if (!installOrigin) die('--install-origin <https://host> is required')
|
|
154
|
+
if (!templateToml) die('--template <path to template wrangler.toml> is required')
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const shared = parseEnvFile(
|
|
158
|
+
readFileSync(join(/** @type {string} */ (platformRoot), 'config', 'cloudflare-house.env'), 'utf8'),
|
|
159
|
+
)
|
|
160
|
+
const cfg = JSON.parse(
|
|
161
|
+
readFileSync(join(/** @type {string} */ (accountDir), 'data-portal.json'), 'utf8'),
|
|
162
|
+
)
|
|
163
|
+
const toml = readFileSync(/** @type {string} */ (templateToml), 'utf8')
|
|
164
|
+
const out = assembleTree({
|
|
165
|
+
toml,
|
|
166
|
+
shared,
|
|
167
|
+
cfg,
|
|
168
|
+
installOrigin: /** @type {string} */ (installOrigin),
|
|
169
|
+
// The directory is the id every house-side loop scopes by, so it is the
|
|
170
|
+
// one the baked-in value has to match.
|
|
171
|
+
expectedAccountId: basename(resolve(/** @type {string} */ (accountDir))),
|
|
172
|
+
})
|
|
173
|
+
const dest = join(
|
|
174
|
+
/** @type {string} */ (accountDir),
|
|
175
|
+
'sites',
|
|
176
|
+
cfg.projectName,
|
|
177
|
+
'wrangler.toml',
|
|
178
|
+
)
|
|
179
|
+
writeFileSync(dest, out)
|
|
180
|
+
console.log(
|
|
181
|
+
`[portal-assemble] op=assemble account=${cfg.accountId} project=${cfg.projectName} ` +
|
|
182
|
+
`db=${shared.DATA_PORTAL_DB_NAME} bucket=${shared.DATA_PORTAL_BUCKET} result=ok dest=${dest}`,
|
|
183
|
+
)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
die(/** @type {Error} */ (err).message)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -71,6 +71,23 @@ if (!/^[A-Za-z0-9-]+$/.test(/** @type {string} */ (accountId))) {
|
|
|
71
71
|
die('--account must match ^[A-Za-z0-9-]+$ — an account id, not a path')
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Which people table this row is going into (Task 2099), because the two have
|
|
75
|
+
// different keys and SQLite requires an ON CONFLICT target to match a real
|
|
76
|
+
// unique index.
|
|
77
|
+
//
|
|
78
|
+
// shared PRIMARY KEY (accountId, ownerId) the one house store
|
|
79
|
+
// dedicated ownerId TEXT PRIMARY KEY a pre-2099 portal's own database
|
|
80
|
+
//
|
|
81
|
+
// Getting this wrong cannot corrupt anything: the conflict target simply does
|
|
82
|
+
// not match an index and SQLite refuses the statement outright with "ON CONFLICT
|
|
83
|
+
// clause does not match any PRIMARY KEY or UNIQUE constraint". It fails at the
|
|
84
|
+
// database, before any row moves, which is why a default is safe here. It
|
|
85
|
+
// defaults to the go-forward model; a grandfathered portal must say so.
|
|
86
|
+
const storageModel = arg('--storage-model') ?? 'shared'
|
|
87
|
+
if (storageModel !== 'shared' && storageModel !== 'dedicated') {
|
|
88
|
+
die('--storage-model must be "shared" or "dedicated" (see the account\'s data-portal.json)')
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
// Optional per-person folder allowlist: comma-separated top-level names that
|
|
75
92
|
// narrow what this person sees below the account's exposed set. Absent means
|
|
76
93
|
// the full set. Each name is validated to the dir-segment charset, so a grant
|
|
@@ -116,12 +133,36 @@ const createdAt = new Date().toISOString()
|
|
|
116
133
|
// `createdAt` is deliberately absent from the DO UPDATE: it records when the
|
|
117
134
|
// person was enrolled, and rotating a passcode is not creating a person.
|
|
118
135
|
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
136
|
+
// On the shared store the conflict target is (accountId, ownerId), not ownerId
|
|
137
|
+
// alone (Task 2099). That table is shared by every portal, so two clients may
|
|
138
|
+
// each enrol a person of the same name: conflicting on ownerId alone would treat
|
|
139
|
+
// the second client's enrolment as a rotation of the first client's person and
|
|
140
|
+
// overwrite their passcode, their grant, and the ownership of their files.
|
|
141
|
+
//
|
|
142
|
+
// On a grandfathered database the key really is ownerId alone, and naming the
|
|
143
|
+
// pair there does not degrade gracefully: SQLite refuses the whole statement,
|
|
144
|
+
// which would silently take passcode rotation away from those portals. Rotation
|
|
145
|
+
// is the one remedy for a leaked passcode, so it must keep working on every
|
|
146
|
+
// portal, not just the new ones.
|
|
147
|
+
//
|
|
148
|
+
// On the shared path `accountId` is NOT in the DO UPDATE: it is half the key
|
|
149
|
+
// that decides which row was matched, so it cannot also be a field the match
|
|
150
|
+
// rewrites. A consequence worth stating plainly, because it used to be possible:
|
|
151
|
+
// re-running enrolment with a different --account no longer MOVES a person, it
|
|
152
|
+
// enrols a separate person in that other portal. `--account` is the portal's own
|
|
153
|
+
// account, and a person always reads that portal's folders (Task 2104).
|
|
154
|
+
//
|
|
155
|
+
// The dedicated path keeps `accountId` in the DO UPDATE exactly as before, so a
|
|
156
|
+
// grandfathered portal's behaviour is unchanged in every respect.
|
|
157
|
+
//
|
|
158
|
+
// `folders` IS in the DO UPDATE on both: re-enrolment replaces the grant, so a
|
|
159
|
+
// narrower or wider set on re-run takes effect rather than sticking.
|
|
160
|
+
const conflict =
|
|
161
|
+
storageModel === 'shared'
|
|
162
|
+
? 'ON CONFLICT(accountId, ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, folders=excluded.folders'
|
|
163
|
+
: 'ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId, folders=excluded.folders'
|
|
123
164
|
console.log(
|
|
124
|
-
`INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}')
|
|
165
|
+
`INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}') ${conflict};`,
|
|
125
166
|
)
|
|
126
167
|
|
|
127
168
|
// stderr: everything for the human. Never stdout — see the note at the top.
|
|
@@ -12,11 +12,16 @@ const SALT = '000102030405060708090a0b0c0d0e0f'
|
|
|
12
12
|
const RIGHT = generatePasscode()
|
|
13
13
|
const WRONG = generatePasscode()
|
|
14
14
|
const IP = '203.0.113.7'
|
|
15
|
+
// This portal's own account, baked into its wrangler vars at assemble time. One
|
|
16
|
+
// shared store backs every portal (Task 2099), so this value is what separates
|
|
17
|
+
// these rows from another client's and every query below binds it.
|
|
18
|
+
const ACCOUNT = 'acct-a'
|
|
15
19
|
|
|
16
20
|
async function envWith(people: Array<{ ownerId: string; passcode: string }>, attemptCount = 0) {
|
|
17
21
|
const rows = await Promise.all(
|
|
18
22
|
people.map(async (p) => ({
|
|
19
23
|
ownerId: p.ownerId,
|
|
24
|
+
accountId: ACCOUNT,
|
|
20
25
|
salt: SALT,
|
|
21
26
|
hash: await hashPasscode(p.passcode, SALT),
|
|
22
27
|
})),
|
|
@@ -45,7 +50,11 @@ async function envWith(people: Array<{ ownerId: string; passcode: string }>, att
|
|
|
45
50
|
async first<T>() {
|
|
46
51
|
if (/FROM auth_attempts/i.test(query)) return { count: attemptCount + 1 } as T
|
|
47
52
|
if (/FROM people/i.test(query)) {
|
|
48
|
-
|
|
53
|
+
// Keyed on the pair, because the people table is shared: bound[0]
|
|
54
|
+
// is the ownerId, bound[1] this portal's account. An ownerId-only
|
|
55
|
+
// fake would let a same-named person in another tenant answer.
|
|
56
|
+
return (rows.find((r) => r.ownerId === bound[0] && r.accountId === bound[1]) ??
|
|
57
|
+
null) as T | null
|
|
49
58
|
}
|
|
50
59
|
return null as T | null
|
|
51
60
|
},
|
|
@@ -54,6 +63,7 @@ async function envWith(people: Array<{ ownerId: string; passcode: string }>, att
|
|
|
54
63
|
},
|
|
55
64
|
},
|
|
56
65
|
BUCKET: {} as never,
|
|
66
|
+
PORTAL_ACCOUNT_ID: ACCOUNT,
|
|
57
67
|
} as unknown as PortalEnv
|
|
58
68
|
return { env, sessions, cleared, scopes }
|
|
59
69
|
}
|
|
@@ -174,12 +184,14 @@ describe('processAuth', () => {
|
|
|
174
184
|
expect(hashesForUnknown).toBe(hashesForKnown)
|
|
175
185
|
})
|
|
176
186
|
|
|
177
|
-
it('scopes the rate limiter on owner AND ip, so one caller cannot lock out another', async () => {
|
|
187
|
+
it('scopes the rate limiter on account, owner AND ip, so one caller cannot lock out another', async () => {
|
|
178
188
|
// Keyed on the owner alone the counter is a weapon: six unauthenticated
|
|
179
|
-
// requests naming a person lock that person out of their own portal.
|
|
189
|
+
// requests naming a person lock that person out of their own portal. The
|
|
190
|
+
// account leads the key because auth_attempts is shared by every portal, so
|
|
191
|
+
// without it two clients who both enrol an "alice" share one lockout.
|
|
180
192
|
const { env, scopes } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
|
|
181
193
|
await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
|
|
182
|
-
expect(scopes[0]).toBe(
|
|
194
|
+
expect(scopes[0]).toBe(`${ACCOUNT}|alice|${IP}`)
|
|
183
195
|
expect(String(scopes[0])).toContain(IP)
|
|
184
196
|
})
|
|
185
197
|
|
|
@@ -187,7 +199,7 @@ describe('processAuth', () => {
|
|
|
187
199
|
const { env, cleared } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
|
|
188
200
|
await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
|
|
189
201
|
expect(cleared.length).toBe(1)
|
|
190
|
-
expect(cleared[0][0]).toBe(
|
|
202
|
+
expect(cleared[0][0]).toBe(`${ACCOUNT}|alice|${IP}`)
|
|
191
203
|
})
|
|
192
204
|
|
|
193
205
|
it('does not clear the counter on a failed sign-in', async () => {
|
|
@@ -203,8 +215,8 @@ describe('processAuth', () => {
|
|
|
203
215
|
const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
|
|
204
216
|
await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
|
|
205
217
|
const expected = (await hashPasscode(RIGHT, SALT)).slice(0, 16)
|
|
206
|
-
// bind order: sessionId, ownerId, pcCheck, expiresAt, createdAt
|
|
207
|
-
expect(sessions[0][
|
|
218
|
+
// bind order: sessionId, ownerId, accountId, pcCheck, expiresAt, createdAt
|
|
219
|
+
expect(sessions[0][3]).toBe(expected)
|
|
208
220
|
})
|
|
209
221
|
|
|
210
222
|
it('quotes the owner in log lines, so a crafted ownerId cannot forge one', async () => {
|