@maestro-js/agent-skills 1.0.0-alpha.2 → 1.0.0-alpha.21
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/dist/bin.js +19 -0
- package/package.json +8 -7
- package/{dist → skills}/choosing-an-npm-package/SKILL.md +5 -4
- package/skills/iconify/SKILL.md +82 -0
- package/skills/iconify/scripts/iconify.js +111 -0
- package/skills/iso-fns/SKILL.md +137 -0
- package/skills/iso-fns/references/api_reference.md +435 -0
- package/skills/maestro-architecture/SKILL.md +552 -0
- package/skills/maestro-examples/SKILL.md +140 -0
- package/skills/maestro-examples/references/cadence/cadence-web/.auth/user.json +15 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/app.css +112 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/auth-layout.tsx +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/back-link.tsx +24 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button-link.tsx +77 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/chip.tsx +161 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/container.tsx +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/date-range.tsx +141 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/date-time-picker.tsx +199 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/empty-state.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/form-page.tsx +41 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/form.tsx +106 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/interaction-form.tsx +87 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/menu.tsx +189 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/optional-link.tsx +18 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/page-header.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/rich-text-editor.tsx +352 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/headless-templated-row-table.ts +553 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/index.tsx +44 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/server-table.ts +146 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-actions.tsx +79 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-container.tsx +176 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-container.type-test.ts +158 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/date-range-filter.tsx +230 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/select-filter.tsx +214 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/sort.tsx +88 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-primitives.tsx +229 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-toolbar.tsx +303 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-types.ts +64 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-client-table.ts +160 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-client-table.type-test.ts +220 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-server-table.ts +195 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-server-table.type-test.ts +177 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/tag-editor.tsx +53 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/tag-field.tsx +253 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/toast.tsx +182 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/top-nav.tsx +175 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/entry.server.tsx +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/custom-error-status-code-middleware.server.ts +31 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/request-context.server.ts +40 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/icons/types.ts +36 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/root.tsx +208 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/_index.tsx +5 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/activity.tsx +124 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/archive.tsx +187 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.forgot-password.tsx +108 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.login.tsx +132 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.logout.tsx +17 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.register.tsx +172 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.reset-password.tsx +137 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.$id.edit.tsx +89 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.$id.tsx +521 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.new.tsx +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.tsx +183 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/settings.tsx +158 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/users.tsx +263 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/compose-event-handlers.ts +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/format-date.ts +10 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/overridable-component-helper.ts +31 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/sanitize-html.ts +16 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-hydrated.ts +12 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-query-state.ts +629 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/cadence/cadence-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/cadence/cadence-web/package.json +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tailwind.config.ts +7 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/activity/filter-activity.spec.ts +93 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/activity/recent-activity.spec.ts +70 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/archive/archive-view.spec.ts +62 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/login-invalid.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/login-valid.spec.ts +45 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/logout.spec.ts +46 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/password-reset.spec.ts +105 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/register.spec.ts +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth.setup.ts +48 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/authorization/member-ownership.spec.ts +104 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/authorization/viewer-read-only.spec.ts +103 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/constants.ts +11 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/archive-restore.spec.ts +77 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/create-contact.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/delete-contact.spec.ts +53 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/edit-contact.spec.ts +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/filter-by-last-contacted.spec.ts +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/filter-by-tag.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/global-search.spec.ts +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/sort-contacts.spec.ts +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/tags.spec.ts +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/view-detail.spec.ts +58 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/delete-interaction.spec.ts +59 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/edit-interaction.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/log-email.spec.ts +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/log-note.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/rich-text-formatting.spec.ts +97 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/organization/delete.spec.ts +70 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/organization/edit-name.spec.ts +51 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/change-role.spec.ts +62 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/invite-user.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/remove-user.spec.ts +58 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tsconfig.json +22 -0
- package/skills/maestro-examples/references/cadence/cadence-web/vite.config.ts +37 -0
- package/skills/maestro-examples/references/cadence/config/agent-container.ts +4 -0
- package/skills/maestro-examples/references/cadence/config/cadence-web.ts +12 -0
- package/skills/maestro-examples/references/cadence/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/cadence/config/queue-processor.ts +11 -0
- package/skills/maestro-examples/references/cadence/config/services.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000001_organizations-and-users.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000002_contacts.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000003_interactions.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000004_remember-me-tokens.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000005_password-reset-tokens.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000006_queue-and-recurring-jobs.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000001_organizations-and-users-down.sql +2 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000001_organizations-and-users-up.sql +24 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000002_contacts-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000002_contacts-up.sql +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000003_interactions-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000003_interactions-up.sql +15 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000004_remember-me-tokens-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000004_remember-me-tokens-up.sql +12 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000005_password-reset-tokens-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000005_password-reset-tokens-up.sql +11 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000006_queue-and-recurring-jobs-down.sql +2 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000006_queue-and-recurring-jobs-up.sql +21 -0
- package/skills/maestro-examples/references/cadence/package.json +17 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/package.json +17 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/contact/index.ts +57 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/index.ts +4 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/interaction/index.ts +45 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/organization/index.ts +32 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/user/index.ts +69 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/cadence/packages/services/package.json +43 -0
- package/skills/maestro-examples/references/cadence/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/auth-db-provider.ts +202 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/auth-use-cases.ts +155 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/index.ts +17 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/profile-db-provider.ts +17 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-db-provider.test.ts +120 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-db-provider.ts +145 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-use-cases.ts +196 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/index.ts +29 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/index.ts +23 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/index.ts +25 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-db-provider.test.ts +128 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-db-provider.ts +98 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-use-cases.ts +171 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/maestro.ts +218 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/index.ts +8 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-db-provider.test.ts +53 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-db-provider.ts +60 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-use-cases.ts +88 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/permissions.ts +28 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/contact-seed.ts +30 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/interaction-seed.ts +29 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/organization-seed.ts +25 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/user-seed.ts +27 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/index.ts +27 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-db-provider.test.ts +100 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-db-provider.ts +69 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-use-cases.ts +162 -0
- package/skills/maestro-examples/references/cadence/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/cadence/scripts/foreign-key-delete-behavior.ts +106 -0
- package/skills/maestro-examples/references/cadence/specs/app.md +72 -0
- package/skills/maestro-examples/references/cadence/specs/data-models.md +80 -0
- package/skills/maestro-examples/references/cadence/specs/multi-tenant-security.md +239 -0
- package/skills/maestro-examples/references/cadence/specs/rich-text-editor.md +108 -0
- package/skills/maestro-examples/references/cadence/specs/table.md +151 -0
- package/skills/maestro-examples/references/cadence/specs/use-cases.md +62 -0
- package/skills/maestro-examples/references/cadence/tsconfig.json +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/app.css +152 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/alert-dialog.tsx +153 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/avatar.tsx +57 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button-link.tsx +77 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/channel-card.tsx +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/checkbox-group.tsx +175 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/chip.tsx +161 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/container.tsx +85 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/date-picker.tsx +214 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/date-time-picker.tsx +199 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/dialog.tsx +114 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/enum-chip.tsx +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/file-input.tsx +248 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/form.tsx +106 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/headless-file-input.tsx +365 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/pdf-dist.client.ts +9 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/image-grid.tsx +96 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/image-input.tsx +268 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/like-button.tsx +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/menu.tsx +189 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/pdf-input.tsx +252 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/pdf.tsx +89 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/post-card.tsx +147 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/post-composer.tsx +166 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/radio-group.tsx +115 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/tag-field.tsx +253 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/text-area.tsx +151 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/toast.tsx +179 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/entry.server.tsx +12 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/csrf-hidden-input.tsx +6 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/custom-error-status-code-middleware.server.ts +18 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/request-context.server.ts +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/icons/types.ts +26 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/root.tsx +164 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/forgot-password.tsx +90 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/login.tsx +104 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/logout.tsx +44 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/reset-password.tsx +130 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/signup.tsx +91 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/_layout.tsx +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/c.new.tsx +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/index.tsx +3 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/api/hide-post.$postId.tsx +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/api/like.$postId.tsx +22 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug/_layout.tsx +108 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug/post.new.tsx +59 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.edit.tsx +53 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/_layout.tsx +123 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/add-moderator.tsx +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/transfer-ownership.tsx +71 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.post.$postId.tsx +119 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/files.$.tsx +22 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/health.tsx +3 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/settings.tsx +128 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/enum-colors.ts +4 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/file-input.ts +52 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-element-size.ts +35 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-number-input.ts +143 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-pagination.ts +41 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-profile.ts +7 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-tab-indicator.ts +107 -0
- package/skills/maestro-examples/references/chorus/chorus-web/environment.d.ts +15 -0
- package/skills/maestro-examples/references/chorus/chorus-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/package.json +54 -0
- package/skills/maestro-examples/references/chorus/chorus-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/chorus/chorus-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-landing.test.ts +8 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-login.test.ts +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-logout.test.ts +8 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-signup.test.ts +15 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth.setup.ts +42 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-browse.test.ts +43 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-create.test.ts +34 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-edit.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-manage-moderators.test.ts +59 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-manage-permissions.test.ts +55 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-transfer.test.ts +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/constants.ts +7 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/forgot-password.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/hide-post-author.test.ts +62 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/hide-post-nonauthor.test.ts +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/like-toggle.test.ts +60 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/like-unauth.test.ts +37 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-create.test.ts +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-listing.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-reply.test.ts +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-view-unauth.test.ts +37 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-duplicate.test.ts +28 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-update.test.ts +34 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-view.test.ts +17 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/signup-validation.test.ts +51 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tsconfig.json +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/vite.config.ts +38 -0
- package/skills/maestro-examples/references/chorus/config/app-a.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/chorus-web.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/chorus/config/queue-processor.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/services.ts +15 -0
- package/skills/maestro-examples/references/chorus/infrastructure/cdk/index.ts +60 -0
- package/skills/maestro-examples/references/chorus/infrastructure/cdk.context.json +5 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260303005902_init_authentication.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260308234835_add-maestro-queue-table.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315051819_channels.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052202_channel-moderators.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052539_posts.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052734_post-images.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315053110_likes.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260303005902_init_authentication-down.sql +3 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260303005902_init_authentication-up.sql +40 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260308234835_add-maestro-queue-table-down.sql +3 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260308234835_add-maestro-queue-table-up.sql +23 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315051819_channels-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315051819_channels-up.sql +11 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052202_channel-moderators-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052202_channel-moderators-up.sql +7 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052539_posts-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052539_posts-up.sql +14 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052734_post-images-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052734_post-images-up.sql +7 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315053110_likes-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315053110_likes-up.sql +8 -0
- package/skills/maestro-examples/references/chorus/infrastructure/package.json +20 -0
- package/skills/maestro-examples/references/chorus/infrastructure/tsconfig.json +8 -0
- package/skills/maestro-examples/references/chorus/package.json +17 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/other/generate-email-modules.ts +22 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/other/generate-email-previews.ts +18 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/package.json +27 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/button.tsx +19 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/root-container.tsx +29 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/tailwind-wrapper.tsx +113 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/email-modules.ts +9 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/emails/password-reset.tsx +41 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/index.ts +3 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/render-email-template.ts +24 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/utils/tw.ts +5 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/tsconfig.json +11 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/package.json +17 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/channel/index.ts +41 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/index.ts +4 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/like/index.ts +16 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/post/index.ts +44 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/profile/index.ts +80 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/chorus/packages/services/environment.d.ts +7 -0
- package/skills/maestro-examples/references/chorus/packages/services/package.json +47 -0
- package/skills/maestro-examples/references/chorus/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/auth-use-cases.ts +178 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/authenticatable-db-provider.ts +59 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/index.ts +29 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/password-reset-token-db-provider.ts +58 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/remember-me-token-db-provider.ts +82 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-db-provider.test.ts +85 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-db-provider.ts +158 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-use-cases.ts +303 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/index.ts +29 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/index.ts +21 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/index.ts +6 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-db-provider.test.ts +47 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-db-provider.ts +53 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-use-cases.ts +31 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/maestro.ts +215 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/permissions.ts +16 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/index.ts +25 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-db-provider.test.ts +68 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-db-provider.ts +219 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-use-cases.ts +184 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/redact-hidden-post.ts +4 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/index.ts +17 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-db-provider.test.ts +57 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-db-provider.ts +75 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-use-cases.ts +61 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/channel-seed.ts +27 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/like-seed.ts +21 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/post-seed.ts +27 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/profile-seed.ts +28 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/chorus/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/chorus/scripts/foreign-key-delete-behavior.ts +111 -0
- package/skills/maestro-examples/references/chorus/specs/app.md +73 -0
- package/skills/maestro-examples/references/chorus/specs/data-models.md +48 -0
- package/skills/maestro-examples/references/chorus/specs/database-schema.md +59 -0
- package/skills/maestro-examples/references/chorus/specs/use-cases.md +33 -0
- package/skills/maestro-examples/references/chorus/tsconfig.json +29 -0
- package/skills/maestro-examples/references/setlist/IMPLEMENTATION_PLAN.md +88 -0
- package/skills/maestro-examples/references/setlist/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/setlist/config/queue-processor.ts +11 -0
- package/skills/maestro-examples/references/setlist/config/services.ts +16 -0
- package/skills/maestro-examples/references/setlist/config/setlist-web.ts +12 -0
- package/skills/maestro-examples/references/setlist/infrastructure/cdk/index.ts +56 -0
- package/skills/maestro-examples/references/setlist/infrastructure/cdk.context.json +5 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/20260316000000_init.ts +16 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/20260317000000_add-queue-and-recurring-job-tables.ts +22 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260316000000_init-down.sql +5 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260316000000_init-up.sql +68 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260317000000_add-queue-and-recurring-job-tables-down.sql +2 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260317000000_add-queue-and-recurring-job-tables-up.sql +21 -0
- package/skills/maestro-examples/references/setlist/infrastructure/package.json +20 -0
- package/skills/maestro-examples/references/setlist/infrastructure/tsconfig.json +8 -0
- package/skills/maestro-examples/references/setlist/package.json +19 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/other/generate-email-modules.ts +22 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/other/generate-email-previews.ts +18 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/package.json +27 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/button.tsx +19 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/root-container.tsx +29 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/tailwind-wrapper.tsx +113 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/email-modules.ts +9 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/emails/magic-link-login.tsx +49 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/index.ts +3 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/render-email-template.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/utils/tw.ts +5 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/tsconfig.json +11 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/tsup.config.ts +10 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/package.json +21 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/area/index.ts +28 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/index.ts +6 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/magic-link/index.ts +20 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/position.ts +137 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/project/index.ts +44 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/task/index.ts +41 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/user/index.ts +28 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/tests/position.test.ts +106 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/setlist/packages/services/package.json +40 -0
- package/skills/maestro-examples/references/setlist/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-db-provider.test.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-db-provider.ts +81 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-use-cases.ts +130 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/index.ts +27 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-db-provider.test.ts +34 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-db-provider.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-use-cases.ts +110 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/index.ts +18 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/magic-link-db-provider.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/magic-link-db-provider.ts +59 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/user-db-provider.test.ts +33 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/user-db-provider.ts +55 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/verify-magic-link-credentials.ts +39 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/index.ts +20 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/maestro.ts +175 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/index.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-db-provider.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-db-provider.ts +114 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-use-cases.ts +183 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/index.ts +39 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-db-provider.test.ts +53 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-db-provider.ts +219 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-use-cases.ts +303 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/area-seed.ts +25 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/project-seed.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/task-seed.ts +31 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/user-seed.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/setlist/queue-processor/package.json +17 -0
- package/skills/maestro-examples/references/setlist/queue-processor/src/index.ts +29 -0
- package/skills/maestro-examples/references/setlist/queue-processor/src/recurring-jobs.ts +13 -0
- package/skills/maestro-examples/references/setlist/queue-processor/tsconfig.json +4 -0
- package/skills/maestro-examples/references/setlist/scripts/foreign-key-delete-behavior.ts +107 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/app.css +158 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/alert-dialog.tsx +153 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/app-dnd-provider.tsx +180 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/area-form.tsx +113 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/context-menu.tsx +78 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/date-picker.tsx +214 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/dnd-collision-detection.ts +28 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/dnd-utils.ts +11 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/drag-ghost.tsx +24 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/form.tsx +107 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/project-form.tsx +153 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/helpers.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/index.ts +6 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/inline-inputs.tsx +128 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/sidebar.tsx +333 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/sortable-rows.tsx +256 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/use-sidebar-dnd.ts +261 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-inline-create.tsx +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/index.ts +3 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-date-helpers.ts +16 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-expanded-detail.tsx +273 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-keyboard-helpers.ts +5 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-list.tsx +134 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-row.tsx +100 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/types.ts +49 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-expanded-cache.ts +22 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-sticky-task.ts +24 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-task-reorder.ts +140 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/text-area.tsx +151 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/toast.tsx +179 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/entry.server.tsx +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/custom-error-status-code-middleware.server.ts +31 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/request-context.server.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-area-actions.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-csrf-token.ts +6 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-project-actions.ts +80 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-task-actions.ts +65 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/root.tsx +218 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.edit.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.reorder.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.complete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.edit.tsx +32 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.move.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.reorder.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.uncomplete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.complete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.edit.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.move.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.reorder.tsx +32 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.uncomplete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/areas.$id.tsx +155 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.login.tsx +98 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.logout.tsx +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.verify.tsx +154 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/completed.tsx +121 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/inbox.tsx +68 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/projects.$id.tsx +217 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/today.tsx +86 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/upcoming.tsx +83 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-dismiss.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-fetcher-complete.ts +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/setlist/setlist-web/environment.d.ts +8 -0
- package/skills/maestro-examples/references/setlist/setlist-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/knip.json +3 -0
- package/skills/maestro-examples/references/setlist/setlist-web/package.json +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-create.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-delete.test.ts +45 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-deleted-not-in-sidebar.test.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-detail-view.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-edit-title.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-expand-collapse.test.ts +50 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-list-in-sidebar.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-reorder-dnd.test.ts +82 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-logout.test.ts +18 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-reject-invalid-token.test.ts +35 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-reject-used-token.test.ts +104 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-request-magic-link.test.ts +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-verify-via-pasted-token.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-verify-via-url.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth.setup.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/constants.ts +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-inbox-to-area.test.ts +74 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-inbox-to-project.test.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-move-to-inbox.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-project-to-area.test.ts +76 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-project-to-project.test.ts +75 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/home.test.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-complete.test.ts +53 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-completed-not-in-sidebar.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-create-in-area.test.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-create-top-level.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-delete.test.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-edit-notes-date.test.ts +69 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-edit-title.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-move-into-area.test.ts +72 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-move-out-of-area.test.ts +76 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-reorder-top-level.test.ts +82 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-reorder-within-area.test.ts +91 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-uncomplete.test.ts +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-view-tasks.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-complete-removes-from-active-views.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-complete.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-create-in-inbox.test.ts +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-create-in-project.test.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-delete.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-edit.test.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-in-project.test.ts +81 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-inbox.test.ts +80 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-today.test.ts +103 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-uncomplete.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-view-details.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-active-sidebar-highlight.test.ts +38 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-inbox-unassigned-only.test.ts +43 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-completed.test.ts +59 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-inbox.test.ts +21 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-today.test.ts +42 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-upcoming.test.ts +59 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-sidebar-built-in-views.test.ts +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-today-shows-overdue.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tsconfig.json +22 -0
- package/skills/maestro-examples/references/setlist/setlist-web/vite.config.ts +37 -0
- package/skills/maestro-examples/references/setlist/specs/app.md +69 -0
- package/skills/maestro-examples/references/setlist/specs/data-models.md +92 -0
- package/skills/maestro-examples/references/setlist/specs/database-schema.md +76 -0
- package/skills/maestro-examples/references/setlist/specs/drag-and-drop.md +89 -0
- package/skills/maestro-examples/references/setlist/specs/magic-link-login.md +300 -0
- package/skills/maestro-examples/references/setlist/specs/position-ordering.md +329 -0
- package/skills/maestro-examples/references/setlist/specs/sidebar.md +65 -0
- package/skills/maestro-examples/references/setlist/specs/use-cases.md +55 -0
- package/skills/maestro-examples/references/setlist/tsconfig.json +28 -0
- package/{dist → skills}/maestro-packages/SKILL.md +3 -3
- package/skills/maestro-packages/permissions.md +215 -0
- package/skills/maestro-packages/queue.md +442 -0
- package/skills/maestro-packages/recurring-jobs.md +238 -0
- package/skills/react-use-effect/SKILL.md +65 -0
- package/skills/react-use-effect/references/patterns.md +283 -0
- package/dist/maestro-packages/queue.md +0 -284
- package/dist/maestro-packages/recurring-jobs.md +0 -353
- package/dist/maestro-packages/tracing.md +0 -240
- /package/{dist → skills}/maestro-cli/SKILL.md +0 -0
- /package/{dist → skills}/maestro-packages/broadcast.md +0 -0
- /package/{dist → skills}/maestro-packages/cache-control.md +0 -0
- /package/{dist → skills}/maestro-packages/cache.md +0 -0
- /package/{dist → skills}/maestro-packages/config.md +0 -0
- /package/{dist → skills}/maestro-packages/context.md +0 -0
- /package/{dist → skills}/maestro-packages/crypt.md +0 -0
- /package/{dist → skills}/maestro-packages/db.md +0 -0
- /package/{dist → skills}/maestro-packages/events.md +0 -0
- /package/{dist → skills}/maestro-packages/exceptions.md +0 -0
- /package/{dist → skills}/maestro-packages/form.md +0 -0
- /package/{dist → skills}/maestro-packages/hash.md +0 -0
- /package/{dist → skills}/maestro-packages/helpers.md +0 -0
- /package/{dist → skills}/maestro-packages/log.md +0 -0
- /package/{dist → skills}/maestro-packages/mail.md +0 -0
- /package/{dist → skills}/maestro-packages/metrics.md +0 -0
- /package/{dist → skills}/maestro-packages/rate-limiting.md +0 -0
- /package/{dist → skills}/maestro-packages/react-router-file-routes.md +0 -0
- /package/{dist → skills}/maestro-packages/sql.md +0 -0
package/skills/maestro-examples/references/cadence/packages/services/src/user/user-use-cases.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import { Hash } from '@maestro-js/hash'
|
|
4
|
+
import { Mail } from '@maestro-js/mail'
|
|
5
|
+
import { Log } from '@maestro-js/log'
|
|
6
|
+
import { Permissions } from '@maestro-js/permissions'
|
|
7
|
+
import { CustomErrors } from '@maestro-js/custom-errors'
|
|
8
|
+
import { UserSchemas } from '@cadence/schemas'
|
|
9
|
+
import { userDbProvider } from './user-db-provider.ts'
|
|
10
|
+
import { authDbProvider } from '../auth/auth-db-provider.ts'
|
|
11
|
+
|
|
12
|
+
const log = Log.channel('user')
|
|
13
|
+
|
|
14
|
+
const Profile = UserSchemas.profileModel
|
|
15
|
+
|
|
16
|
+
// ─── Errors ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export const EmailAlreadyInUseError = CustomErrors.create({
|
|
19
|
+
name: 'EmailAlreadyInUse',
|
|
20
|
+
message: 'A user with this email already exists in the organization',
|
|
21
|
+
httpStatusCode: 'conflict' as const
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const UserNotFoundError = CustomErrors.create({
|
|
25
|
+
name: 'UserNotFound',
|
|
26
|
+
message: 'User not found',
|
|
27
|
+
httpStatusCode: 'notFound' as const
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const CannotChangeOwnRoleError = CustomErrors.create({
|
|
31
|
+
name: 'CannotChangeOwnRole',
|
|
32
|
+
message: 'You cannot change your own role',
|
|
33
|
+
httpStatusCode: 'forbidden' as const
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const CannotRemoveSelfError = CustomErrors.create({
|
|
37
|
+
name: 'CannotRemoveSelf',
|
|
38
|
+
message: 'You cannot remove yourself',
|
|
39
|
+
httpStatusCode: 'forbidden' as const
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const CannotDemoteLastAdminError = CustomErrors.create({
|
|
43
|
+
name: 'CannotDemoteLastAdmin',
|
|
44
|
+
message: 'Cannot demote the last admin of the organization',
|
|
45
|
+
httpStatusCode: 'forbidden' as const
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const CannotRemoveLastAdminError = CustomErrors.create({
|
|
49
|
+
name: 'CannotRemoveLastAdmin',
|
|
50
|
+
message: 'Cannot remove the last admin of the organization',
|
|
51
|
+
httpStatusCode: 'forbidden' as const
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function generateTempPassword(): string {
|
|
57
|
+
return crypto.randomBytes(16).toString('base64url')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function countAdminsInOrg(orgId: number): Promise<number> {
|
|
61
|
+
const users = await userDbProvider.listByOrganizationId(orgId)
|
|
62
|
+
return users.filter((u) => u.role === 'admin').length
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── InviteUser ────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export const InviteUser = z
|
|
68
|
+
.function({
|
|
69
|
+
input: [UserSchemas.inviteUserForm, Profile]
|
|
70
|
+
})
|
|
71
|
+
.implement(async ({ email: emailRaw, name, role }, profile) => {
|
|
72
|
+
Permissions.gate(profile, 'user', 'invite')
|
|
73
|
+
|
|
74
|
+
const email = emailRaw.toLowerCase().trim()
|
|
75
|
+
|
|
76
|
+
const existing = await userDbProvider.getByEmailAndOrgId(email, profile.organizationId)
|
|
77
|
+
if (existing) throw new EmailAlreadyInUseError()
|
|
78
|
+
|
|
79
|
+
const tempPassword = generateTempPassword()
|
|
80
|
+
const passwordHash = await Hash.provider('bcrypt').hash(tempPassword)
|
|
81
|
+
|
|
82
|
+
const userId = await userDbProvider.create({
|
|
83
|
+
organizationId: profile.organizationId,
|
|
84
|
+
name,
|
|
85
|
+
email,
|
|
86
|
+
passwordHash,
|
|
87
|
+
role
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await Mail.queue({
|
|
91
|
+
to: email,
|
|
92
|
+
subject: 'You have been invited to Cadence CRM',
|
|
93
|
+
content: {
|
|
94
|
+
html: `<p>You have been invited to Cadence CRM.</p><p>Email: <strong>${email}</strong></p><p>Temporary password: <strong>${tempPassword}</strong></p><p>Please log in and change your password.</p>`
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
log.info('User invited', { action: 'user.invite', invitedBy: profile.userId, userId, email, role })
|
|
99
|
+
|
|
100
|
+
return { userId }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// ─── ListUsers ─────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export const ListUsers = z
|
|
106
|
+
.function({
|
|
107
|
+
input: [Profile]
|
|
108
|
+
})
|
|
109
|
+
.implement(async (profile) => {
|
|
110
|
+
Permissions.gate(profile, 'user', 'list')
|
|
111
|
+
|
|
112
|
+
return userDbProvider.listByOrganizationId(profile.organizationId)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ─── ChangeUserRole ────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export const ChangeUserRole = z
|
|
118
|
+
.function({
|
|
119
|
+
input: [z.object({ userId: z.number(), role: z.enum(UserSchemas.roleOptions) }), Profile]
|
|
120
|
+
})
|
|
121
|
+
.implement(async ({ userId, role }, profile) => {
|
|
122
|
+
Permissions.gate(profile, 'user', 'changeRole')
|
|
123
|
+
|
|
124
|
+
if (userId === profile.userId) throw new CannotChangeOwnRoleError()
|
|
125
|
+
|
|
126
|
+
const target = await userDbProvider.getById(userId)
|
|
127
|
+
if (!target || target.organizationId !== profile.organizationId) throw new UserNotFoundError()
|
|
128
|
+
|
|
129
|
+
if (target.role === 'admin' && role !== 'admin') {
|
|
130
|
+
const adminCount = await countAdminsInOrg(profile.organizationId)
|
|
131
|
+
if (adminCount <= 1) throw new CannotDemoteLastAdminError()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await userDbProvider.updateRole(userId, role)
|
|
135
|
+
|
|
136
|
+
log.info('User role changed', { action: 'user.changeRole', changedBy: profile.userId, userId, role })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ─── RemoveUser ────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export const RemoveUser = z
|
|
142
|
+
.function({
|
|
143
|
+
input: [z.object({ userId: z.number() }), Profile]
|
|
144
|
+
})
|
|
145
|
+
.implement(async ({ userId }, profile) => {
|
|
146
|
+
Permissions.gate(profile, 'user', 'remove')
|
|
147
|
+
|
|
148
|
+
if (userId === profile.userId) throw new CannotRemoveSelfError()
|
|
149
|
+
|
|
150
|
+
const target = await userDbProvider.getById(userId)
|
|
151
|
+
if (!target || target.organizationId !== profile.organizationId) throw new UserNotFoundError()
|
|
152
|
+
|
|
153
|
+
if (target.role === 'admin') {
|
|
154
|
+
const adminCount = await countAdminsInOrg(profile.organizationId)
|
|
155
|
+
if (adminCount <= 1) throw new CannotRemoveLastAdminError()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await userDbProvider.softDelete(userId)
|
|
159
|
+
await authDbProvider.rememberMeToken.revokeByAuthenticatableId(userId)
|
|
160
|
+
|
|
161
|
+
log.info('User removed', { action: 'user.remove', removedBy: profile.userId, userId })
|
|
162
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Db } from '@maestro-js/db'
|
|
2
|
+
|
|
3
|
+
Db.Provider.register(
|
|
4
|
+
'default',
|
|
5
|
+
Db.Provider.create({
|
|
6
|
+
driver: Db.drivers.mysql({
|
|
7
|
+
user: process.env.DB_USER!,
|
|
8
|
+
password: process.env.DB_PASSWORD!,
|
|
9
|
+
host: process.env.DB_HOST!,
|
|
10
|
+
database: process.env.DB_DATABASE!,
|
|
11
|
+
port: Number(process.env.DB_PORT!)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const FKS = {
|
|
17
|
+
fk_users_organizationId: 'CASCADE',
|
|
18
|
+
fk_contacts_organizationId: 'CASCADE',
|
|
19
|
+
fk_interactions_contactId: 'CASCADE',
|
|
20
|
+
fk_interactions_userId: 'CASCADE',
|
|
21
|
+
fk_rememberMeTokens_userId: 'CASCADE',
|
|
22
|
+
fk_passwordResetTokens_userId: 'CASCADE'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ForeignKey = {
|
|
26
|
+
TABLE_NAME: string
|
|
27
|
+
COLUMN_NAME: string
|
|
28
|
+
REFERENCED_TABLE_NAME: string
|
|
29
|
+
REFERENCED_COLUMN_NAME: string
|
|
30
|
+
CONSTRAINT_NAME: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const foreignKeys = await Db.select<ForeignKey>(
|
|
34
|
+
`SELECT
|
|
35
|
+
TABLE_NAME,
|
|
36
|
+
COLUMN_NAME,
|
|
37
|
+
REFERENCED_TABLE_NAME,
|
|
38
|
+
REFERENCED_COLUMN_NAME,
|
|
39
|
+
CONSTRAINT_NAME
|
|
40
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
41
|
+
WHERE TABLE_SCHEMA = ?
|
|
42
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
43
|
+
ORDER BY TABLE_NAME, COLUMN_NAME`,
|
|
44
|
+
[process.env.DB_DATABASE!]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const dbFkNames = new Set(foreignKeys.map((fk) => fk.CONSTRAINT_NAME))
|
|
48
|
+
const localFkNames = new Set(Object.keys(FKS))
|
|
49
|
+
|
|
50
|
+
const missingFromLocal = [...dbFkNames].filter((name) => !localFkNames.has(name))
|
|
51
|
+
const extraInLocal = [...localFkNames].filter((name) => !dbFkNames.has(name))
|
|
52
|
+
|
|
53
|
+
if (missingFromLocal.length > 0) {
|
|
54
|
+
console.error('Foreign keys in database but missing from FKS:', missingFromLocal)
|
|
55
|
+
}
|
|
56
|
+
if (extraInLocal.length > 0) {
|
|
57
|
+
console.error('Foreign keys in FKS but not in database:', extraInLocal)
|
|
58
|
+
}
|
|
59
|
+
if (missingFromLocal.length > 0 || extraInLocal.length > 0) {
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('FKS matches database foreign keys')
|
|
64
|
+
|
|
65
|
+
async function applyOnDeleteRestrict() {
|
|
66
|
+
for (const constraintName of Object.keys(FKS)) {
|
|
67
|
+
const fk = foreignKeys.find((f) => f.CONSTRAINT_NAME === constraintName)!
|
|
68
|
+
await Db.statement(`ALTER TABLE ${fk.TABLE_NAME} DROP FOREIGN KEY ${constraintName}`)
|
|
69
|
+
await Db.statement(
|
|
70
|
+
`ALTER TABLE ${fk.TABLE_NAME}
|
|
71
|
+
ADD CONSTRAINT ${constraintName}
|
|
72
|
+
FOREIGN KEY (${fk.COLUMN_NAME})
|
|
73
|
+
REFERENCES ${fk.REFERENCED_TABLE_NAME}(${fk.REFERENCED_COLUMN_NAME})
|
|
74
|
+
ON DELETE RESTRICT`
|
|
75
|
+
)
|
|
76
|
+
console.log(`${constraintName}: ON DELETE RESTRICT applied`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function applyOnDeleteCascade() {
|
|
81
|
+
for (const [constraintName, behavior] of Object.entries(FKS)) {
|
|
82
|
+
const fk = foreignKeys.find((f) => f.CONSTRAINT_NAME === constraintName)!
|
|
83
|
+
await Db.statement(`ALTER TABLE ${fk.TABLE_NAME} DROP FOREIGN KEY ${constraintName}`)
|
|
84
|
+
await Db.statement(
|
|
85
|
+
`ALTER TABLE ${fk.TABLE_NAME}
|
|
86
|
+
ADD CONSTRAINT ${constraintName}
|
|
87
|
+
FOREIGN KEY (${fk.COLUMN_NAME})
|
|
88
|
+
REFERENCES ${fk.REFERENCED_TABLE_NAME}(${fk.REFERENCED_COLUMN_NAME})
|
|
89
|
+
ON DELETE ${behavior}`
|
|
90
|
+
)
|
|
91
|
+
console.log(`${constraintName}: ON DELETE ${behavior} applied`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const action = process.argv[2]
|
|
96
|
+
|
|
97
|
+
if (action === 'cascade') {
|
|
98
|
+
await applyOnDeleteCascade()
|
|
99
|
+
} else if (action === 'restrict') {
|
|
100
|
+
await applyOnDeleteRestrict()
|
|
101
|
+
} else {
|
|
102
|
+
console.error('Usage: foreign-key-delete-behavior <cascade|restrict>')
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
process.exit(0)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# App Requirements
|
|
2
|
+
|
|
3
|
+
A multi-tenant CRM for managing contacts and communication. Teams organize people into contacts, log emails and notes, and use tags and filters to stay on top of relationships. Each organization has its own isolated workspace with role-based access.
|
|
4
|
+
|
|
5
|
+
## Organizations
|
|
6
|
+
|
|
7
|
+
An organization is the top-level tenant — all data is scoped to one org.
|
|
8
|
+
|
|
9
|
+
- An org has a name (required) and a slug (required, unique, URL-safe, immutable)
|
|
10
|
+
- An org contains users and contacts
|
|
11
|
+
- Data is fully isolated between orgs — no cross-org access
|
|
12
|
+
- An org can be deleted by an admin (soft delete)
|
|
13
|
+
|
|
14
|
+
## Users
|
|
15
|
+
|
|
16
|
+
A user is a person who logs into the app and belongs to one organization.
|
|
17
|
+
|
|
18
|
+
- A user has a name (required) and email (required, unique within the org)
|
|
19
|
+
- A user has one role: **admin**, **member**, or **viewer**
|
|
20
|
+
- **Admin** — full access: manage users, delete the org, and all member capabilities
|
|
21
|
+
- **Member** — create, edit, and delete contacts and interactions
|
|
22
|
+
- **Viewer** — read-only access to contacts and interactions
|
|
23
|
+
- An admin can invite new users by email and assign their role
|
|
24
|
+
- An admin can change a user's role or remove them from the org
|
|
25
|
+
- Authentication uses username and password. The login page has three fields: org slug, email, and password. The org slug scopes the login to the correct organization. Passwords are hashed and stored per user.
|
|
26
|
+
|
|
27
|
+
## Contacts
|
|
28
|
+
|
|
29
|
+
A contact is an individual person the team communicates with.
|
|
30
|
+
|
|
31
|
+
- A contact has a first name (required), last name (optional), and email (optional)
|
|
32
|
+
- A contact can have a phone number and a job title
|
|
33
|
+
- A contact can have zero or more tags
|
|
34
|
+
- A contact has a created date
|
|
35
|
+
- A contact can be archived — archived contacts are hidden from default views but not deleted
|
|
36
|
+
- A contact can be deleted
|
|
37
|
+
- Contacts are listed in a table view with sorting and filtering by name, tag, and last-contacted date
|
|
38
|
+
|
|
39
|
+
## Interactions
|
|
40
|
+
|
|
41
|
+
An interaction is a logged communication with a contact. This is the core activity record.
|
|
42
|
+
|
|
43
|
+
- An interaction belongs to one contact (required)
|
|
44
|
+
- An interaction has a type: **email** or **note**
|
|
45
|
+
- An interaction has a subject (optional) and a body (required, rich text)
|
|
46
|
+
- An interaction has a date — defaults to now but can be backdated
|
|
47
|
+
- An interaction records which user logged it
|
|
48
|
+
- An interaction can be edited or deleted by the user who created it, or by an admin
|
|
49
|
+
- Interactions appear on the contact detail page in reverse chronological order
|
|
50
|
+
- **Email**: represents a sent or received email
|
|
51
|
+
- **Note**: a freeform record of a conversation, meeting, or observation — no direction
|
|
52
|
+
|
|
53
|
+
## Tags
|
|
54
|
+
|
|
55
|
+
Tags are free-form strings attached to contacts for flexible categorization.
|
|
56
|
+
|
|
57
|
+
- When tagging a contact, existing tag strings within the org are auto-suggested, and typing a new name adds it directly
|
|
58
|
+
- Tags appear as chips on contact rows
|
|
59
|
+
|
|
60
|
+
## Views
|
|
61
|
+
|
|
62
|
+
The app provides several built-in views:
|
|
63
|
+
|
|
64
|
+
- **Contacts** — all active contacts in a sortable, filterable table. Default landing page.
|
|
65
|
+
- **Recent Activity** — a feed of interactions across all contacts, reverse chronological. Filterable by interaction type and user.
|
|
66
|
+
- **Archive** — archived contacts with the option to restore them
|
|
67
|
+
|
|
68
|
+
## Filtering and Sorting
|
|
69
|
+
|
|
70
|
+
- The contacts table supports multi-column sorting
|
|
71
|
+
- Filters can be combined: e.g. contacts tagged "Lead" with last contact older than 30 days
|
|
72
|
+
- Active filters are shown as removable chips above the table
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Data Models
|
|
2
|
+
|
|
3
|
+
Zod schemas for the multi-tenant CRM. Each section defines a zod model. Note: zod models are not necessarily one-to-one with database tables.
|
|
4
|
+
|
|
5
|
+
## Organizations
|
|
6
|
+
|
|
7
|
+
An organization is the top-level tenant. All data is scoped to one org.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const organizationModel = z.object({
|
|
11
|
+
id: z.number(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
slug: z.string(),
|
|
14
|
+
deletedDate: isoZ.instant().nullable(),
|
|
15
|
+
createdDate: isoZ.instant(),
|
|
16
|
+
lastModifiedDate: isoZ.instant()
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Users
|
|
21
|
+
|
|
22
|
+
A user belongs to one organization and authenticates with email and password scoped to the org slug.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const roleOptions = ['admin', 'member', 'viewer'] as const
|
|
26
|
+
|
|
27
|
+
const userModel = z.object({
|
|
28
|
+
id: z.number(),
|
|
29
|
+
organizationId: z.number(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
email: z.string(),
|
|
32
|
+
passwordHash: z.string(),
|
|
33
|
+
role: z.enum(roleOptions),
|
|
34
|
+
deletedDate: isoZ.instant().nullable(),
|
|
35
|
+
createdDate: isoZ.instant(),
|
|
36
|
+
lastModifiedDate: isoZ.instant()
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Contacts
|
|
41
|
+
|
|
42
|
+
A contact is an individual person the team communicates with. Archiving hides the contact from default views without deleting it. Tags are free-form strings for flexible categorization.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const contactModel = z.object({
|
|
46
|
+
id: z.number(),
|
|
47
|
+
organizationId: z.number(),
|
|
48
|
+
firstName: z.string(),
|
|
49
|
+
lastName: z.string().nullable(),
|
|
50
|
+
email: z.string().nullable(),
|
|
51
|
+
phone: z.string().nullable(),
|
|
52
|
+
jobTitle: z.string().nullable(),
|
|
53
|
+
tags: z.array(z.string()),
|
|
54
|
+
archivedDate: isoZ.instant().nullable(),
|
|
55
|
+
deletedDate: isoZ.instant().nullable(),
|
|
56
|
+
createdDate: isoZ.instant(),
|
|
57
|
+
lastModifiedDate: isoZ.instant()
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Interactions
|
|
62
|
+
|
|
63
|
+
An interaction is a logged communication with a contact.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const interactionTypeOptions = ['email', 'note'] as const
|
|
67
|
+
|
|
68
|
+
const interactionModel = z.object({
|
|
69
|
+
id: z.number(),
|
|
70
|
+
contactId: z.number(),
|
|
71
|
+
userId: z.number(),
|
|
72
|
+
type: z.enum(interactionTypeOptions),
|
|
73
|
+
subject: z.string().nullable(),
|
|
74
|
+
body: z.string(),
|
|
75
|
+
date: isoZ.instant(),
|
|
76
|
+
deletedDate: isoZ.instant().nullable(),
|
|
77
|
+
createdDate: isoZ.instant(),
|
|
78
|
+
lastModifiedDate: isoZ.instant()
|
|
79
|
+
})
|
|
80
|
+
```
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Multi-Tenant Security
|
|
2
|
+
|
|
3
|
+
How authentication, authorization, and tenant isolation work in Cadence CRM. Every organization is a fully isolated tenant identified by its slug. The login flow requires the org slug upfront — similar to how AWS requires an account ID before credentials.
|
|
4
|
+
|
|
5
|
+
## Login Flow
|
|
6
|
+
|
|
7
|
+
The login screen has three fields: **org slug**, **email**, and **password**. The org slug acts as the account identifier and must be entered first.
|
|
8
|
+
|
|
9
|
+
### Step-by-step
|
|
10
|
+
|
|
11
|
+
1. User enters the org slug
|
|
12
|
+
2. User enters their email and password
|
|
13
|
+
3. The server resolves the org by slug — if not found (or soft-deleted), return a generic "invalid credentials" error
|
|
14
|
+
4. The server looks up the user by email scoped to that org — if not found (or soft-deleted), return the same generic error
|
|
15
|
+
5. `Password.verifyCredentials()` compares the submitted password against the stored hash
|
|
16
|
+
6. If valid, `Auth.login()` creates a session and optionally sets a remember-me cookie
|
|
17
|
+
7. The session stores the user ID and the auth provider name — the org is derived from the user record
|
|
18
|
+
|
|
19
|
+
The generic error in steps 3 and 4 is intentional. Never reveal whether the org slug or email was the invalid part — this prevents enumeration of valid org slugs and user emails.
|
|
20
|
+
|
|
21
|
+
### Password Service Configuration
|
|
22
|
+
|
|
23
|
+
`@maestro-js/password` handles credential verification. The config wires it to the user table scoped by org:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
Password.Provider.register(
|
|
27
|
+
'default',
|
|
28
|
+
Password.Provider.create({
|
|
29
|
+
getAuthenticatableByCredentials: async ({ orgSlug, email }) => {
|
|
30
|
+
// JOIN users ON organizations WHERE slug = orgSlug AND email = email AND deletedDate IS NULL
|
|
31
|
+
// Returns { id, passwordHash, credentialsInvalidBefore } or null
|
|
32
|
+
},
|
|
33
|
+
updatePasswordHash: async (userId, passwordHash) => {
|
|
34
|
+
// UPDATE users SET passwordHash = ? WHERE id = ?
|
|
35
|
+
},
|
|
36
|
+
updateCredentialsInvalidBefore: async (userId, date) => {
|
|
37
|
+
// UPDATE users SET credentialsInvalidBefore = ? WHERE id = ?
|
|
38
|
+
},
|
|
39
|
+
hash: Hash.provider('bcrypt')
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The credentials type is `{ orgSlug: string, email: string, password: string }`. The `orgSlug` + `email` pair identifies the user. The password field is handled internally by the Password service — `getAuthenticatableByCredentials` receives everything except `password`.
|
|
45
|
+
|
|
46
|
+
### Auth Service Configuration
|
|
47
|
+
|
|
48
|
+
`@maestro-js/auth` manages sessions and remember-me tokens. It delegates credential verification to the Password service:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
Auth.Provider.register(
|
|
52
|
+
'default',
|
|
53
|
+
Auth.Provider.create({
|
|
54
|
+
name: 'user',
|
|
55
|
+
verifyCredentials: async (credentials) => {
|
|
56
|
+
const result = await Password.verifyCredentials(credentials)
|
|
57
|
+
if (!result) return null
|
|
58
|
+
// Return full authenticatable: { id, credentialsInvalidBefore }
|
|
59
|
+
},
|
|
60
|
+
getAuthenticatableById: async (id) => {
|
|
61
|
+
// SELECT id, credentialsInvalidBefore FROM users WHERE id = ? AND deletedDate IS NULL
|
|
62
|
+
},
|
|
63
|
+
updateCredentialsInvalidBefore: async (id, date) => {
|
|
64
|
+
// UPDATE users SET credentialsInvalidBefore = ? WHERE id = ?
|
|
65
|
+
},
|
|
66
|
+
hash: Hash.provider('sha256'),
|
|
67
|
+
crypt: Crypt.provider('default'),
|
|
68
|
+
rememberMeOptions: {
|
|
69
|
+
maxRememberMeSeconds: 60 * 60 * 24 * 30, // 30 days
|
|
70
|
+
// remember-me token CRUD backed by a database table
|
|
71
|
+
create: async (record) => { /* INSERT INTO rememberMeTokens ... */ },
|
|
72
|
+
getBySelector: async (selector) => { /* SELECT ... WHERE selector = ? */ },
|
|
73
|
+
rotateBySelector: async (selector, token) => { /* UPDATE ... */ },
|
|
74
|
+
revokeBySelector: async (selector) => { /* DELETE ... WHERE selector = ? */ },
|
|
75
|
+
revokeByAuthenticatableId: async (id) => { /* DELETE ... WHERE userId = ? */ }
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Session Contents
|
|
82
|
+
|
|
83
|
+
After login, the session contains:
|
|
84
|
+
|
|
85
|
+
| Key | Value |
|
|
86
|
+
| ------------------------------ | ------------------------------- |
|
|
87
|
+
| `_auth_id` | User ID |
|
|
88
|
+
| `_auth_provider_name` | `"user"` |
|
|
89
|
+
| `_auth_session_created_date` | ISO instant of session creation |
|
|
90
|
+
|
|
91
|
+
The org is not stored in the session directly. It is derived by loading the user record and reading `organizationId`. This keeps the session minimal and avoids stale org data.
|
|
92
|
+
|
|
93
|
+
### Returning Visitors
|
|
94
|
+
|
|
95
|
+
On subsequent requests, `Auth.authenticate()` checks the session:
|
|
96
|
+
|
|
97
|
+
1. Read `_auth_id` from the session
|
|
98
|
+
2. Load the user via `getAuthenticatableById`
|
|
99
|
+
3. Compare `_auth_session_created_date` against `credentialsInvalidBefore` — if the session predates a credential change, reject it
|
|
100
|
+
4. If the session is missing but a remember-me cookie exists, validate the token and create a new session
|
|
101
|
+
|
|
102
|
+
## Tenant Isolation
|
|
103
|
+
|
|
104
|
+
Tenant isolation is enforced at two layers: the database query layer and the use case layer. There is no row-level security in MySQL, so the application is responsible for scoping every query.
|
|
105
|
+
|
|
106
|
+
### The Profile Object
|
|
107
|
+
|
|
108
|
+
Every authenticated request produces a **profile** — a lightweight object derived from the session user that is passed into every use case and permission check:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const profile = z.object({
|
|
112
|
+
userId: z.number(),
|
|
113
|
+
organizationId: z.number(),
|
|
114
|
+
role: z.enum(['admin', 'member', 'viewer'])
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The profile is built once per request after `Auth.authenticate()` succeeds. It is the single source of truth for "who is this user and what org do they belong to."
|
|
119
|
+
|
|
120
|
+
### Scoping Queries by Organization
|
|
121
|
+
|
|
122
|
+
Every database query that reads or writes tenant data must include `WHERE organizationId = ?` using `profile.organizationId`. This applies to:
|
|
123
|
+
|
|
124
|
+
- Contacts: `SELECT ... FROM contacts WHERE organizationId = ?`
|
|
125
|
+
- Users: `SELECT ... FROM users WHERE organizationId = ?`
|
|
126
|
+
- Interactions: joined through contacts — `JOIN contacts ON interactions.contactId = contacts.id WHERE contacts.organizationId = ?`
|
|
127
|
+
|
|
128
|
+
There are no exceptions. A query that touches tenant data without filtering by `organizationId` is a bug.
|
|
129
|
+
|
|
130
|
+
### Cross-Tenant Protection in Use Cases
|
|
131
|
+
|
|
132
|
+
Use cases receive the profile as their second parameter. The profile's `organizationId` is used to scope all operations — never trust an org ID from the request body or URL params for data access. The URL may contain an org slug for routing purposes, but the actual data scoping always comes from the authenticated profile.
|
|
133
|
+
|
|
134
|
+
Example — editing a contact:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const editContact = z.function(
|
|
138
|
+
z.tuple([editContactInput, profile]),
|
|
139
|
+
async (input, profile) => {
|
|
140
|
+
Permissions.gate(profile, 'Contact', 'update')
|
|
141
|
+
// The WHERE clause uses profile.organizationId, not a request param
|
|
142
|
+
await db.query(`UPDATE contacts SET ... WHERE id = ? AND organizationId = ?`, [
|
|
143
|
+
input.contactId,
|
|
144
|
+
profile.organizationId
|
|
145
|
+
])
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Even if an attacker manipulates the contact ID to reference a contact in another org, the `AND organizationId = ?` clause ensures no rows are affected.
|
|
151
|
+
|
|
152
|
+
## Authorization
|
|
153
|
+
|
|
154
|
+
`@maestro-js/permissions` enforces role-based access using CASL's declarative rule model. Rules are computed from the profile on every check — no persisted permission records.
|
|
155
|
+
|
|
156
|
+
### Action Map
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
type ActionMap = {
|
|
160
|
+
Organization: ['read', 'update', 'delete']
|
|
161
|
+
User: ['list', 'invite', 'changeRole', 'remove']
|
|
162
|
+
Contact: ['list', 'read', 'create', 'update', 'archive', 'restore', 'delete']
|
|
163
|
+
Interaction: ['list', 'create', 'update', 'delete']
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Permission Rules
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const permissionsDriver = Permissions.drivers.static<ActionMap, Profile>((profile, allow, forbid) => {
|
|
171
|
+
// All authenticated users (viewer, member, admin)
|
|
172
|
+
allow('Organization', 'read')
|
|
173
|
+
allow('Contact', ['list', 'read'])
|
|
174
|
+
allow('Interaction', 'list')
|
|
175
|
+
allow('User', 'list')
|
|
176
|
+
|
|
177
|
+
// Members and admins
|
|
178
|
+
if (profile.role === 'member' || profile.role === 'admin') {
|
|
179
|
+
allow('Contact', ['create', 'update', 'archive', 'restore', 'delete'])
|
|
180
|
+
allow('Interaction', ['create', 'update', 'delete'])
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Admins only
|
|
184
|
+
if (profile.role === 'admin') {
|
|
185
|
+
allow('Organization', ['update', 'delete'])
|
|
186
|
+
allow('User', ['invite', 'changeRole', 'remove'])
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Interaction Ownership
|
|
192
|
+
|
|
193
|
+
Members can only edit or delete their own interactions. Admins can edit or delete any interaction. This is enforced in the use case layer, not the permission rules, because it requires checking the interaction's `userId` against the profile:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const deleteInteraction = z.function(
|
|
197
|
+
z.tuple([deleteInteractionInput, profile]),
|
|
198
|
+
async (input, profile) => {
|
|
199
|
+
Permissions.gate(profile, 'Interaction', 'delete')
|
|
200
|
+
const interaction = await getInteraction(input.interactionId, profile.organizationId)
|
|
201
|
+
if (interaction.userId !== profile.userId && profile.role !== 'admin') {
|
|
202
|
+
throw new ForbiddenError('You can only delete your own interactions')
|
|
203
|
+
}
|
|
204
|
+
// proceed with delete
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Password Reset
|
|
210
|
+
|
|
211
|
+
`@maestro-js/password` provides token-based password reset. The flow is scoped to the org:
|
|
212
|
+
|
|
213
|
+
1. User submits org slug + email on the reset form
|
|
214
|
+
2. Server looks up the user by org slug + email
|
|
215
|
+
3. `Password.createPasswordResetToken(userId)` generates a selector-verifier token pair, hashes the verifier, and stores the record
|
|
216
|
+
4. The token is sent to the user's email (via `@maestro-js/mail`)
|
|
217
|
+
5. User clicks the link and submits a new password
|
|
218
|
+
6. `Password.resetPassword({ token, newPassword })` validates the token, updates the hash, and sets `credentialsInvalidBefore` to now — invalidating all existing sessions
|
|
219
|
+
|
|
220
|
+
Built-in protections:
|
|
221
|
+
- **Throttling** — A new reset token cannot be generated within 60 seconds of the previous one
|
|
222
|
+
- **Expiration** — Tokens expire after 60 minutes
|
|
223
|
+
- **Single use** — The token record is deleted after successful reset
|
|
224
|
+
- **Session invalidation** — Setting `credentialsInvalidBefore` forces all existing sessions to re-authenticate
|
|
225
|
+
|
|
226
|
+
## Security Summary
|
|
227
|
+
|
|
228
|
+
| Concern | How it's handled |
|
|
229
|
+
| ---------------------- | -------------------------------------------------------------------------------- |
|
|
230
|
+
| Tenant isolation | Every query includes `WHERE organizationId = ?` from the authenticated profile |
|
|
231
|
+
| Credential enumeration | Generic error for bad slug, email, or password |
|
|
232
|
+
| Password storage | Bcrypt hashing via `@maestro-js/hash` with automatic rehashing |
|
|
233
|
+
| Session fixation | Session is regenerated on login |
|
|
234
|
+
| Stale sessions | `credentialsInvalidBefore` timestamp invalidates sessions after password changes |
|
|
235
|
+
| Remember-me tokens | Selector-verifier pattern, hashed verifier, per-device revocation |
|
|
236
|
+
| Reset token abuse | Throttled, time-limited, single-use, hashed verifier |
|
|
237
|
+
| Role enforcement | CASL-based permission checks via `Permissions.gate()` in every use case |
|
|
238
|
+
| Ownership enforcement | Use case logic checks `interaction.userId` against `profile.userId` |
|
|
239
|
+
| Cross-tenant requests | Profile `organizationId` is from session, never from request input |
|