From e068c6678cfbb433dc4871a9da938666d8c7bc15 Mon Sep 17 00:00:00 2001 From: yyyyaaa Date: Wed, 22 Jul 2026 11:39:19 +0700 Subject: [PATCH 01/60] refactor(blocks): remove obsolete block implementations --- .../account-api-keys-list.tsx | 382 - .../auth-account-api-keys-list.requires.json | 6 - .../auth/account-api-keys-list/messages.ts | 71 - .../account-connected-accounts.tsx | 436 - ...h-account-connected-accounts.requires.json | 6 - .../account-connected-accounts/messages.ts | 61 - .../account-danger-card.tsx | 222 - .../auth-account-danger-card.requires.json | 6 - .../auth/account-danger-card/messages.ts | 51 - .../account-deletion-confirm-page.tsx | 369 - ...ccount-deletion-confirm-page.requires.json | 6 - .../account-deletion-confirm-page/messages.ts | 45 - .../account-emails-list.tsx | 594 - .../auth-account-emails-list.requires.json | 6 - .../auth/account-emails-list/messages.ts | 70 - .../account-phones-list.tsx | 1062 -- .../auth-account-phones-list.requires.json | 6 - .../auth/account-phones-list/messages.ts | 106 - .../account-profile-card.tsx | 503 - .../auth-account-profile-card.requires.json | 6 - .../auth/account-profile-card/messages.ts | 60 - .../account-security-card.tsx | 322 - .../auth-account-security-card.requires.json | 6 - .../auth/account-security-card/messages.ts | 69 - .../account-sessions-list.tsx | 455 - .../auth-account-sessions-list.requires.json | 6 - .../auth/account-sessions-list/messages.ts | 74 - .../account-settings-page.tsx | 330 - .../auth-account-settings-page.requires.json | 6 - .../auth/account-settings-page/messages.ts | 33 - .../anonymous-sign-in-button.tsx | 177 - ...uth-anonymous-sign-in-button.requires.json | 6 - .../auth/anonymous-sign-in-button/messages.ts | 46 - .../api-key-create-dialog.tsx | 514 - .../auth-api-key-create-dialog.requires.json | 6 - .../auth/api-key-create-dialog/messages.ts | 68 - .../api-key-created-modal.tsx | 317 - .../auth/api-key-created-modal/messages.ts | 36 - .../auth-change-password-form.requires.json | 6 - .../change-password-form.tsx | 320 - .../auth/change-password-form/messages.ts | 65 - .../auth-cross-origin-link.requires.json | 6 - .../cross-origin-link/cross-origin-link.tsx | 207 - .../blocks/auth/cross-origin-link/messages.ts | 34 - .../domain-verification-step.tsx | 216 - .../auth/domain-verification-step/messages.ts | 40 - .../auth-email-otp-input.requires.json | 6 - .../auth/email-otp-input/email-otp-input.tsx | 466 - .../blocks/auth/email-otp-input/messages.ts | 56 - .../auth-email-otp-request-card.requires.json | 6 - .../email-otp-request-card.tsx | 374 - .../auth/email-otp-request-card/messages.ts | 66 - .../auth-forgot-password-card.requires.json | 6 - .../forgot-password-card.tsx | 282 - .../auth/forgot-password-card/messages.ts | 52 - .../forgot-password-page.tsx | 45 - ...h-invitation-acceptance-card.requires.json | 6 - .../invitation-acceptance-card.tsx | 352 - .../invitation-acceptance-card/messages.ts | 101 - ...h-invitation-acceptance-page.requires.json | 6 - .../invitation-acceptance-page.tsx | 184 - ...uth-magic-link-callback-page.requires.json | 6 - .../magic-link-callback-page.tsx | 404 - .../auth/magic-link-callback-page/messages.ts | 71 - ...auth-magic-link-request-card.requires.json | 6 - .../magic-link-request-card.tsx | 335 - .../auth/magic-link-request-card/messages.ts | 62 - .../auth-magic-link-sent-page.requires.json | 6 - .../magic-link-sent-page.tsx | 287 - .../auth/magic-link-sent-page/messages.ts | 46 - .../auth/mfa-backup-codes-display/messages.ts | 52 - .../mfa-backup-codes-display.tsx | 203 - ...-mfa-backup-codes-regenerate.requires.json | 6 - .../mfa-backup-codes-regenerate/messages.ts | 54 - .../mfa-backup-codes-regenerate.tsx | 250 - .../auth/mfa-totp-challenge-page/messages.ts | 29 - .../mfa-totp-challenge-page.tsx | 187 - .../auth-mfa-totp-challenge.requires.json | 6 - .../auth/mfa-totp-challenge/messages.ts | 53 - .../mfa-totp-challenge/mfa-totp-challenge.tsx | 315 - ...uth-mfa-totp-disable-confirm.requires.json | 6 - .../auth/mfa-totp-disable-confirm/messages.ts | 52 - .../mfa-totp-disable-confirm.tsx | 189 - .../auth-mfa-totp-enroll.requires.json | 6 - .../blocks/auth/mfa-totp-enroll/messages.ts | 67 - .../mfa-totp-enroll/mfa-totp-enroll.test.tsx | 454 - .../auth/mfa-totp-enroll/mfa-totp-enroll.tsx | 439 - .../auth-passkey-enroll.requires.json | 6 - .../hooks/use-passkey-enroll.ts | 179 - .../blocks/auth/passkey-enroll/messages.ts | 64 - .../auth/passkey-enroll/passkey-enroll.tsx | 253 - ...auth-passkey-management-list.requires.json | 6 - .../hooks/use-passkey-management.ts | 130 - .../auth/passkey-management-list/messages.ts | 78 - .../passkey-management-list.tsx | 450 - .../auth-passkey-sign-in.requires.json | 6 - .../hooks/use-passkey-sign-in.ts | 240 - .../blocks/auth/passkey-sign-in/messages.ts | 49 - .../passkey-sign-in/passkey-sign-in.test.tsx | 308 - .../auth/passkey-sign-in/passkey-sign-in.tsx | 246 - .../auth-reset-password-card.requires.json | 6 - .../auth/reset-password-card/messages.ts | 77 - .../reset-password-card.tsx | 371 - .../reset-password-page.tsx | 72 - .../auth-sign-in-card.requires.json | 6 - .../src/blocks/auth/sign-in-card/messages.ts | 64 - .../auth/sign-in-card/sign-in-card.test.tsx | 152 - .../blocks/auth/sign-in-card/sign-in-card.tsx | 326 - .../blocks/auth/sign-in-page/sign-in-page.tsx | 112 - .../auth-sign-out-button.requires.json | 6 - .../blocks/auth/sign-out-button/messages.ts | 26 - .../auth/sign-out-button/sign-out-button.tsx | 120 - .../auth-sign-up-card.requires.json | 6 - .../src/blocks/auth/sign-up-card/messages.ts | 81 - .../blocks/auth/sign-up-card/sign-up-card.tsx | 366 - .../blocks/auth/sign-up-page/sign-up-page.tsx | 80 - .../auth-social-buttons.requires.json | 6 - .../blocks/auth/social-buttons/messages.ts | 55 - .../auth/social-buttons/social-buttons.tsx | 403 - .../auth/social-providers-grid/messages.ts | 46 - .../social-providers-grid.tsx | 248 - .../blocks/auth/sso-setup-card/messages.ts | 41 - .../auth/sso-setup-card/sso-setup-card.tsx | 84 - .../blocks/auth/sso-sign-in-card/messages.ts | 43 - .../sso-sign-in-card/sso-sign-in-card.tsx | 251 - .../auth-step-up-dialog.requires.json | 6 - .../blocks/auth/step-up-dialog/messages.ts | 59 - .../auth/step-up-dialog/step-up-dialog.tsx | 429 - .../auth/use-step-up/step-up-provider.tsx | 145 - .../auth/use-step-up/use-step-up.test.tsx | 290 - .../blocks/auth/use-step-up/use-step-up.ts | 110 - .../auth-verify-email-banner.requires.json | 6 - .../auth/verify-email-banner/messages.ts | 36 - .../verify-email-banner.tsx | 196 - .../auth-verify-email-page.requires.json | 6 - .../blocks/auth/verify-email-page/messages.ts | 60 - .../verify-email-page/verify-email-page.tsx | 289 - apps/blocks/src/blocks/chat/api-route.ts | 212 - .../blocks/src/blocks/chat/api-routes.test.ts | 221 - apps/blocks/src/blocks/chat/api-test-route.ts | 161 - .../blocks/chat/chat-accessibility.test.tsx | 181 - .../src/blocks/chat/chat-context.test.tsx | 207 - apps/blocks/src/blocks/chat/chat-context.tsx | 238 - apps/blocks/src/blocks/chat/chat-fab.tsx | 59 - apps/blocks/src/blocks/chat/chat-input.tsx | 100 - .../blocks/chat/chat-message-content.test.tsx | 49 - .../src/blocks/chat/chat-message-content.tsx | 30 - apps/blocks/src/blocks/chat/chat-messages.tsx | 123 - apps/blocks/src/blocks/chat/chat-panel.tsx | 166 - apps/blocks/src/blocks/chat/chat-settings.tsx | 311 - apps/blocks/src/blocks/chat/chat-widget.tsx | 44 - apps/blocks/src/blocks/chat/chat.types.ts | 81 - .../src/blocks/chat/dom-scraper.test.ts | 31 - apps/blocks/src/blocks/chat/dom-scraper.ts | 33 - apps/blocks/src/blocks/chat/index.ts | 10 - apps/blocks/src/blocks/chat/shimmer.tsx | 22 - apps/blocks/src/blocks/chat/tool-message.tsx | 244 - apps/blocks/src/blocks/chat/tool-registry.ts | 35 - apps/blocks/src/blocks/chat/tool-status.tsx | 38 - apps/blocks/src/blocks/chat/tool-ui-config.ts | 68 - .../blocks/src/blocks/lib/auth-errors.test.ts | 157 - apps/blocks/src/blocks/lib/auth-errors.ts | 423 - apps/blocks/src/blocks/lib/cn.ts | 14 - .../src/blocks/lib/password-strength.test.ts | 33 - .../src/blocks/lib/password-strength.ts | 75 - apps/blocks/src/blocks/lib/schemas.ts | 77 - .../org/app-memberships/app-memberships.tsx | 428 - .../blocks/org/app-memberships/messages.ts | 65 - .../org-app-memberships.requires.json | 6 - .../org/create-card/create-card.test.tsx | 324 - .../blocks/org/create-card/create-card.tsx | 563 - .../src/blocks/org/create-card/messages.ts | 94 - .../create-card/org-create-card.requires.json | 6 - .../org/invite-dialog/invite-dialog.tsx | 465 - .../src/blocks/org/invite-dialog/messages.ts | 80 - .../org-invite-dialog.requires.json | 6 - .../org/members-list/members-list.test.tsx | 570 - .../blocks/org/members-list/members-list.tsx | 519 - .../src/blocks/org/members-list/messages.ts | 82 - .../org-members-list.requires.json | 6 - .../src/blocks/org/roles-editor/messages.ts | 77 - .../org-roles-editor.requires.json | 6 - .../org/roles-editor/roles-editor.test.tsx | 370 - .../blocks/org/roles-editor/roles-editor.tsx | 548 - .../org/scim-connections-list/messages.ts | 60 - .../scim-connections-list.tsx | 104 - .../blocks/org/scim-setup-guide/messages.ts | 43 - .../org/scim-setup-guide/scim-setup-guide.tsx | 288 - .../scim-token-generation-card/messages.ts | 60 - .../scim-token-generation-card.tsx | 240 - .../src/blocks/org/settings-form/messages.ts | 96 - .../org-settings-form.requires.json | 6 - .../org/settings-form/settings-form.tsx | 487 - .../blocks/primitives/auth-error-alert.tsx | 45 - .../blocks/primitives/auth-loading-button.tsx | 35 - .../src/blocks/primitives/form-field.tsx | 60 - .../blocks/runtime/blocks-runtime.test.tsx | 287 - .../src/blocks/runtime/blocks-runtime.tsx | 192 - .../shell/account-menu/account-menu.tsx | 234 - .../src/blocks/shell/account-menu/messages.ts | 41 - .../shell-account-menu.requires.json | 6 - .../blocks/shell/breadcrumbs/breadcrumbs.tsx | 291 - .../src/blocks/shell/breadcrumbs/messages.ts | 33 - .../command-palette/command-palette.test.tsx | 360 - .../shell/command-palette/command-palette.tsx | 227 - .../blocks/shell/command-palette/messages.ts | 37 - .../shell-command-palette-provider.test.tsx | 111 - .../shell-command-palette-provider.tsx | 128 - .../shell-command-palette.requires.json | 6 - .../command-palette/use-command-palette.ts | 144 - .../blocks/src/blocks/shell/header/header.tsx | 238 - .../src/blocks/shell/header/messages.ts | 34 - .../notifications/hooks/use-notifications.ts | 81 - .../blocks/shell/notifications/messages.ts | 67 - .../shell/notifications/notifications.tsx | 313 - .../shell-notifications.requires.json | 6 - .../src/blocks/shell/sidebar/messages.ts | 30 - .../blocks/shell/sidebar/sidebar.stories.tsx | 218 - .../src/blocks/shell/sidebar/sidebar.test.tsx | 304 - .../src/blocks/shell/sidebar/sidebar.tsx | 510 - .../context-switcher.test.tsx | 355 - .../context-switcher/context-switcher.tsx | 329 - .../hooks/use-switch-context.ts | 101 - .../hooks/use-user-contexts.ts | 186 - .../blocks/user/context-switcher/messages.ts | 74 - .../user-context-switcher.requires.json | 16 - .../blocks/user/user-avatar/user-avatar.tsx | 69 - .../blocks/user/user-avatar/user-initials.ts | 37 - apps/blocks/src/generated/README.md | 37 - .../src/generated/admin/hooks/client.ts | 42 - .../blocks/src/generated/admin/hooks/index.ts | 19 - .../src/generated/admin/hooks/invalidation.ts | 1125 -- .../generated/admin/hooks/mutation-keys.ts | 582 - .../generated/admin/hooks/mutations/index.ts | 154 - .../mutations/useCheckPasswordMutation.ts | 55 - .../useConfirmDeleteAccountMutation.ts | 58 - .../mutations/useConfirmUploadMutation.ts | 55 - .../mutations/useCreateApiKeyMutation.ts | 55 - .../useCreateAppAchievementMutation.ts | 91 - .../useCreateAppAdminGrantMutation.ts | 88 - .../useCreateAppClaimedInviteMutation.ts | 91 - .../mutations/useCreateAppGrantMutation.ts | 88 - .../mutations/useCreateAppInviteMutation.ts | 88 - .../mutations/useCreateAppLevelMutation.ts | 88 - .../useCreateAppLevelRequirementMutation.ts | 91 - .../useCreateAppLimitDefaultMutation.ts | 91 - .../mutations/useCreateAppLimitMutation.ts | 88 - .../useCreateAppMembershipDefaultMutation.ts | 91 - .../useCreateAppMembershipMutation.ts | 88 - .../useCreateAppOwnerGrantMutation.ts | 88 - .../useCreateAppPermissionDefaultMutation.ts | 91 - .../useCreateAppPermissionMutation.ts | 88 - ...CreateAppProfileDefinitionGrantMutation.ts | 97 - .../useCreateAppProfileGrantMutation.ts | 91 - .../mutations/useCreateAppProfileMutation.ts | 88 - .../useCreateAppProfilePermissionMutation.ts | 91 - .../mutations/useCreateAppStepMutation.ts | 88 - .../useCreateIdentityProviderMutation.ts | 91 - .../useCreateMembershipTypeMutation.ts | 91 - .../useCreateOrgAdminGrantMutation.ts | 88 - .../useCreateOrgChartEdgeGrantMutation.ts | 91 - .../useCreateOrgChartEdgeMutation.ts | 88 - .../useCreateOrgClaimedInviteMutation.ts | 91 - .../useCreateOrgGetManagersRecordMutation.ts | 91 - ...eCreateOrgGetSubordinatesRecordMutation.ts | 91 - .../mutations/useCreateOrgGrantMutation.ts | 88 - .../mutations/useCreateOrgInviteMutation.ts | 88 - .../useCreateOrgLimitDefaultMutation.ts | 91 - .../mutations/useCreateOrgLimitMutation.ts | 88 - .../mutations/useCreateOrgMemberMutation.ts | 88 - .../useCreateOrgMemberProfileMutation.ts | 91 - .../useCreateOrgMembershipDefaultMutation.ts | 91 - .../useCreateOrgMembershipMutation.ts | 88 - .../useCreateOrgMembershipSettingMutation.ts | 91 - .../useCreateOrgOwnerGrantMutation.ts | 88 - .../useCreateOrgPermissionDefaultMutation.ts | 91 - .../useCreateOrgPermissionMutation.ts | 88 - ...CreateOrgProfileDefinitionGrantMutation.ts | 97 - .../useCreateOrgProfileGrantMutation.ts | 91 - .../mutations/useCreateOrgProfileMutation.ts | 88 - .../useCreateOrgProfilePermissionMutation.ts | 91 - .../useDeleteAppAchievementMutation.ts | 98 - .../useDeleteAppAdminGrantMutation.ts | 98 - .../useDeleteAppClaimedInviteMutation.ts | 98 - .../mutations/useDeleteAppGrantMutation.ts | 98 - .../mutations/useDeleteAppInviteMutation.ts | 98 - .../mutations/useDeleteAppLevelMutation.ts | 98 - .../useDeleteAppLevelRequirementMutation.ts | 104 - .../useDeleteAppLimitDefaultMutation.ts | 98 - .../mutations/useDeleteAppLimitMutation.ts | 98 - .../useDeleteAppMembershipDefaultMutation.ts | 104 - .../useDeleteAppMembershipMutation.ts | 98 - .../useDeleteAppOwnerGrantMutation.ts | 98 - .../useDeleteAppPermissionDefaultMutation.ts | 104 - .../useDeleteAppPermissionMutation.ts | 98 - ...DeleteAppProfileDefinitionGrantMutation.ts | 106 - .../useDeleteAppProfileGrantMutation.ts | 98 - .../mutations/useDeleteAppProfileMutation.ts | 98 - .../useDeleteAppProfilePermissionMutation.ts | 104 - .../mutations/useDeleteAppStepMutation.ts | 98 - .../useDeleteMembershipTypeMutation.ts | 98 - .../useDeleteOrgAdminGrantMutation.ts | 98 - .../useDeleteOrgChartEdgeGrantMutation.ts | 104 - .../useDeleteOrgChartEdgeMutation.ts | 98 - .../useDeleteOrgClaimedInviteMutation.ts | 98 - .../mutations/useDeleteOrgGrantMutation.ts | 98 - .../mutations/useDeleteOrgInviteMutation.ts | 98 - .../useDeleteOrgLimitDefaultMutation.ts | 98 - .../mutations/useDeleteOrgLimitMutation.ts | 98 - .../mutations/useDeleteOrgMemberMutation.ts | 98 - .../useDeleteOrgMemberProfileMutation.ts | 98 - .../useDeleteOrgMembershipDefaultMutation.ts | 104 - .../useDeleteOrgMembershipMutation.ts | 98 - .../useDeleteOrgMembershipSettingMutation.ts | 104 - .../useDeleteOrgOwnerGrantMutation.ts | 98 - .../useDeleteOrgPermissionDefaultMutation.ts | 104 - .../useDeleteOrgPermissionMutation.ts | 98 - ...DeleteOrgProfileDefinitionGrantMutation.ts | 106 - .../useDeleteOrgProfileGrantMutation.ts | 98 - .../mutations/useDeleteOrgProfileMutation.ts | 98 - .../useDeleteOrgProfilePermissionMutation.ts | 104 - .../mutations/useDisconnectAccountMutation.ts | 58 - .../useExtendTokenExpiresMutation.ts | 58 - .../mutations/useForgotPasswordMutation.ts | 55 - .../mutations/useProvisionBucketMutation.ts | 55 - .../mutations/useProvisionNewUserMutation.ts | 55 - .../useRequestCrossOriginTokenMutation.ts | 58 - .../mutations/useRequestUploadUrlMutation.ts | 55 - .../mutations/useResetPasswordMutation.ts | 55 - .../mutations/useRevokeApiKeyMutation.ts | 55 - .../mutations/useRevokeSessionMutation.ts | 55 - .../useSendAccountDeletionEmailMutation.ts | 60 - .../useSendVerificationEmailMutation.ts | 58 - .../hooks/mutations/useSetPasswordMutation.ts | 55 - .../mutations/useSignInCrossOriginMutation.ts | 58 - .../hooks/mutations/useSignInMutation.ts | 55 - .../hooks/mutations/useSignOutMutation.ts | 55 - .../hooks/mutations/useSignUpMutation.ts | 55 - .../useSubmitAppInviteCodeMutation.ts | 58 - .../useSubmitOrgInviteCodeMutation.ts | 58 - .../useUpdateAppAchievementMutation.ts | 116 - .../useUpdateAppAdminGrantMutation.ts | 116 - .../useUpdateAppClaimedInviteMutation.ts | 116 - .../mutations/useUpdateAppGrantMutation.ts | 102 - .../mutations/useUpdateAppInviteMutation.ts | 110 - .../mutations/useUpdateAppLevelMutation.ts | 102 - .../useUpdateAppLevelRequirementMutation.ts | 116 - .../useUpdateAppLimitDefaultMutation.ts | 116 - .../mutations/useUpdateAppLimitMutation.ts | 102 - .../useUpdateAppMembershipDefaultMutation.ts | 116 - .../useUpdateAppMembershipMutation.ts | 116 - .../useUpdateAppOwnerGrantMutation.ts | 116 - .../useUpdateAppPermissionDefaultMutation.ts | 116 - .../useUpdateAppPermissionMutation.ts | 116 - ...UpdateAppProfileDefinitionGrantMutation.ts | 118 - .../useUpdateAppProfileGrantMutation.ts | 116 - .../mutations/useUpdateAppProfileMutation.ts | 110 - .../useUpdateAppProfilePermissionMutation.ts | 116 - .../mutations/useUpdateAppStepMutation.ts | 102 - .../useUpdateMembershipTypeMutation.ts | 116 - .../useUpdateOrgAdminGrantMutation.ts | 116 - .../useUpdateOrgChartEdgeGrantMutation.ts | 116 - .../useUpdateOrgChartEdgeMutation.ts | 110 - .../useUpdateOrgClaimedInviteMutation.ts | 116 - .../mutations/useUpdateOrgGrantMutation.ts | 102 - .../mutations/useUpdateOrgInviteMutation.ts | 110 - .../useUpdateOrgLimitDefaultMutation.ts | 116 - .../mutations/useUpdateOrgLimitMutation.ts | 102 - .../mutations/useUpdateOrgMemberMutation.ts | 110 - .../useUpdateOrgMemberProfileMutation.ts | 116 - .../useUpdateOrgMembershipDefaultMutation.ts | 116 - .../useUpdateOrgMembershipMutation.ts | 116 - .../useUpdateOrgMembershipSettingMutation.ts | 116 - .../useUpdateOrgOwnerGrantMutation.ts | 116 - .../useUpdateOrgPermissionDefaultMutation.ts | 116 - .../useUpdateOrgPermissionMutation.ts | 116 - ...UpdateOrgProfileDefinitionGrantMutation.ts | 118 - .../useUpdateOrgProfileGrantMutation.ts | 116 - .../mutations/useUpdateOrgProfileMutation.ts | 110 - .../useUpdateOrgProfilePermissionMutation.ts | 116 - .../hooks/mutations/useVerifyEmailMutation.ts | 55 - .../mutations/useVerifyPasswordMutation.ts | 55 - .../hooks/mutations/useVerifyTotpMutation.ts | 55 - .../generated/admin/hooks/queries/index.ts | 104 - .../hooks/queries/useAppAchievementQuery.ts | 138 - .../hooks/queries/useAppAchievementsQuery.ts | 159 - .../hooks/queries/useAppAdminGrantQuery.ts | 138 - .../hooks/queries/useAppAdminGrantsQuery.ts | 151 - .../hooks/queries/useAppClaimedInviteQuery.ts | 138 - .../queries/useAppClaimedInvitesQuery.ts | 163 - .../admin/hooks/queries/useAppGrantQuery.ts | 138 - .../admin/hooks/queries/useAppGrantsQuery.ts | 145 - .../admin/hooks/queries/useAppInviteQuery.ts | 138 - .../admin/hooks/queries/useAppInvitesQuery.ts | 145 - .../admin/hooks/queries/useAppLevelQuery.ts | 138 - .../queries/useAppLevelRequirementQuery.ts | 144 - .../queries/useAppLevelRequirementsQuery.ts | 174 - .../admin/hooks/queries/useAppLevelsQuery.ts | 145 - .../hooks/queries/useAppLimitDefaultQuery.ts | 138 - .../hooks/queries/useAppLimitDefaultsQuery.ts | 163 - .../admin/hooks/queries/useAppLimitQuery.ts | 138 - .../admin/hooks/queries/useAppLimitsQuery.ts | 145 - .../queries/useAppMembershipDefaultQuery.ts | 144 - .../queries/useAppMembershipDefaultsQuery.ts | 178 - .../hooks/queries/useAppMembershipQuery.ts | 138 - .../hooks/queries/useAppMembershipsQuery.ts | 151 - .../hooks/queries/useAppOwnerGrantQuery.ts | 138 - .../hooks/queries/useAppOwnerGrantsQuery.ts | 151 - .../queries/useAppPermissionDefaultQuery.ts | 144 - .../queries/useAppPermissionDefaultsQuery.ts | 178 - .../hooks/queries/useAppPermissionQuery.ts | 138 - .../useAppPermissionsGetByMaskQuery.ts | 108 - .../useAppPermissionsGetMaskByNamesQuery.ts | 107 - .../queries/useAppPermissionsGetMaskQuery.ts | 107 - .../useAppPermissionsGetPaddedMaskQuery.ts | 107 - .../hooks/queries/useAppPermissionsQuery.ts | 151 - .../useAppProfileDefinitionGrantQuery.ts | 151 - .../useAppProfileDefinitionGrantsQuery.ts | 182 - .../hooks/queries/useAppProfileGrantQuery.ts | 138 - .../hooks/queries/useAppProfileGrantsQuery.ts | 163 - .../queries/useAppProfilePermissionQuery.ts | 144 - .../queries/useAppProfilePermissionsQuery.ts | 178 - .../admin/hooks/queries/useAppProfileQuery.ts | 138 - .../hooks/queries/useAppProfilesQuery.ts | 145 - .../admin/hooks/queries/useAppStepQuery.ts | 138 - .../admin/hooks/queries/useAppStepsQuery.ts | 145 - .../hooks/queries/useCurrentIpAddressQuery.ts | 90 - .../hooks/queries/useCurrentUserAgentQuery.ts | 90 - .../hooks/queries/useCurrentUserIdQuery.ts | 90 - .../hooks/queries/useCurrentUserQuery.ts | 125 - .../queries/useIdentityProvidersQuery.ts | 163 - .../hooks/queries/useMembershipTypeQuery.ts | 138 - .../hooks/queries/useMembershipTypesQuery.ts | 159 - .../hooks/queries/useOrgAdminGrantQuery.ts | 138 - .../hooks/queries/useOrgAdminGrantsQuery.ts | 151 - .../queries/useOrgChartEdgeGrantQuery.ts | 144 - .../queries/useOrgChartEdgeGrantsQuery.ts | 163 - .../hooks/queries/useOrgChartEdgeQuery.ts | 138 - .../hooks/queries/useOrgChartEdgesQuery.ts | 145 - .../hooks/queries/useOrgClaimedInviteQuery.ts | 138 - .../queries/useOrgClaimedInvitesQuery.ts | 163 - .../hooks/queries/useOrgGetManagersQuery.ts | 172 - .../queries/useOrgGetSubordinatesQuery.ts | 178 - .../admin/hooks/queries/useOrgGrantQuery.ts | 138 - .../admin/hooks/queries/useOrgGrantsQuery.ts | 145 - .../admin/hooks/queries/useOrgInviteQuery.ts | 138 - .../admin/hooks/queries/useOrgInvitesQuery.ts | 145 - .../hooks/queries/useOrgIsManagerOfQuery.ts | 105 - .../hooks/queries/useOrgLimitDefaultQuery.ts | 138 - .../hooks/queries/useOrgLimitDefaultsQuery.ts | 163 - .../admin/hooks/queries/useOrgLimitQuery.ts | 138 - .../admin/hooks/queries/useOrgLimitsQuery.ts | 145 - .../hooks/queries/useOrgMemberProfileQuery.ts | 138 - .../queries/useOrgMemberProfilesQuery.ts | 163 - .../admin/hooks/queries/useOrgMemberQuery.ts | 138 - .../admin/hooks/queries/useOrgMembersQuery.ts | 145 - .../queries/useOrgMembershipDefaultQuery.ts | 144 - .../queries/useOrgMembershipDefaultsQuery.ts | 178 - .../hooks/queries/useOrgMembershipQuery.ts | 138 - .../queries/useOrgMembershipSettingQuery.ts | 144 - .../queries/useOrgMembershipSettingsQuery.ts | 178 - .../hooks/queries/useOrgMembershipsQuery.ts | 151 - .../hooks/queries/useOrgOwnerGrantQuery.ts | 138 - .../hooks/queries/useOrgOwnerGrantsQuery.ts | 151 - .../queries/useOrgPermissionDefaultQuery.ts | 144 - .../queries/useOrgPermissionDefaultsQuery.ts | 178 - .../hooks/queries/useOrgPermissionQuery.ts | 138 - .../useOrgPermissionsGetByMaskQuery.ts | 108 - .../useOrgPermissionsGetMaskByNamesQuery.ts | 107 - .../queries/useOrgPermissionsGetMaskQuery.ts | 107 - .../useOrgPermissionsGetPaddedMaskQuery.ts | 107 - .../hooks/queries/useOrgPermissionsQuery.ts | 151 - .../useOrgProfileDefinitionGrantQuery.ts | 151 - .../useOrgProfileDefinitionGrantsQuery.ts | 182 - .../hooks/queries/useOrgProfileGrantQuery.ts | 138 - .../hooks/queries/useOrgProfileGrantsQuery.ts | 163 - .../queries/useOrgProfilePermissionQuery.ts | 144 - .../queries/useOrgProfilePermissionsQuery.ts | 178 - .../admin/hooks/queries/useOrgProfileQuery.ts | 138 - .../hooks/queries/useOrgProfilesQuery.ts | 145 - .../hooks/queries/useRequireStepUpQuery.ts | 105 - .../hooks/queries/useStepsAchievedQuery.ts | 105 - .../hooks/queries/useStepsRequiredQuery.ts | 106 - .../src/generated/admin/hooks/query-keys.ts | 517 - .../src/generated/admin/hooks/selection.ts | 60 - apps/blocks/src/generated/admin/index.ts | 5 - apps/blocks/src/generated/admin/orm/client.ts | 137 - apps/blocks/src/generated/admin/orm/index.ts | 132 - .../src/generated/admin/orm/input-types.ts | 8210 --------- .../admin/orm/models/appAchievement.ts | 237 - .../admin/orm/models/appAdminGrant.ts | 237 - .../admin/orm/models/appClaimedInvite.ts | 237 - .../generated/admin/orm/models/appGrant.ts | 237 - .../generated/admin/orm/models/appInvite.ts | 237 - .../generated/admin/orm/models/appLevel.ts | 237 - .../admin/orm/models/appLevelRequirement.ts | 237 - .../generated/admin/orm/models/appLimit.ts | 237 - .../admin/orm/models/appLimitDefault.ts | 237 - .../admin/orm/models/appMembership.ts | 237 - .../admin/orm/models/appMembershipDefault.ts | 239 - .../admin/orm/models/appOwnerGrant.ts | 237 - .../admin/orm/models/appPermission.ts | 237 - .../admin/orm/models/appPermissionDefault.ts | 239 - .../generated/admin/orm/models/appProfile.ts | 237 - .../orm/models/appProfileDefinitionGrant.ts | 244 - .../admin/orm/models/appProfileGrant.ts | 237 - .../admin/orm/models/appProfilePermission.ts | 239 - .../src/generated/admin/orm/models/appStep.ts | 237 - .../admin/orm/models/identityProvider.ts | 127 - .../src/generated/admin/orm/models/index.ts | 48 - .../admin/orm/models/membershipType.ts | 237 - .../admin/orm/models/orgAdminGrant.ts | 237 - .../admin/orm/models/orgChartEdge.ts | 237 - .../admin/orm/models/orgChartEdgeGrant.ts | 237 - .../admin/orm/models/orgClaimedInvite.ts | 237 - .../admin/orm/models/orgGetManagersRecord.ts | 127 - .../orm/models/orgGetSubordinatesRecord.ts | 134 - .../generated/admin/orm/models/orgGrant.ts | 237 - .../generated/admin/orm/models/orgInvite.ts | 237 - .../generated/admin/orm/models/orgLimit.ts | 237 - .../admin/orm/models/orgLimitDefault.ts | 237 - .../generated/admin/orm/models/orgMember.ts | 237 - .../admin/orm/models/orgMemberProfile.ts | 237 - .../admin/orm/models/orgMembership.ts | 237 - .../admin/orm/models/orgMembershipDefault.ts | 239 - .../admin/orm/models/orgMembershipSetting.ts | 239 - .../admin/orm/models/orgOwnerGrant.ts | 237 - .../admin/orm/models/orgPermission.ts | 237 - .../admin/orm/models/orgPermissionDefault.ts | 239 - .../generated/admin/orm/models/orgProfile.ts | 237 - .../orm/models/orgProfileDefinitionGrant.ts | 244 - .../admin/orm/models/orgProfileGrant.ts | 237 - .../admin/orm/models/orgProfilePermission.ts | 239 - .../src/generated/admin/orm/mutation/index.ts | 945 -- .../src/generated/admin/orm/query-builder.ts | 884 - .../src/generated/admin/orm/query/index.ts | 572 - .../src/generated/admin/orm/select-types.ts | 142 - apps/blocks/src/generated/admin/types.ts | 622 - .../blocks/src/generated/auth/hooks/client.ts | 42 - apps/blocks/src/generated/auth/hooks/index.ts | 37 - .../src/generated/auth/hooks/invalidation.ts | 262 - .../src/generated/auth/hooks/mutation-keys.ts | 232 - .../generated/auth/hooks/mutations/index.ts | 52 - .../mutations/useCheckPasswordMutation.ts | 55 - .../useConfirmDeleteAccountMutation.ts | 58 - .../mutations/useConfirmUploadMutation.ts | 55 - .../mutations/useCreateApiKeyMutation.ts | 55 - .../mutations/useCreateAuditLogMutation.ts | 88 - .../useCreateCryptoAddressMutation.ts | 88 - .../hooks/mutations/useCreateEmailMutation.ts | 80 - .../useCreateIdentityProviderMutation.ts | 91 - .../mutations/useCreatePhoneNumberMutation.ts | 88 - .../mutations/useCreateRoleTypeMutation.ts | 88 - .../useCreateUserConnectedAccountMutation.ts | 91 - .../hooks/mutations/useCreateUserMutation.ts | 80 - .../useCreateWebauthnCredentialMutation.ts | 91 - .../mutations/useDeleteAuditLogMutation.ts | 98 - .../useDeleteCryptoAddressMutation.ts | 98 - .../hooks/mutations/useDeleteEmailMutation.ts | 98 - .../mutations/useDeletePhoneNumberMutation.ts | 98 - .../mutations/useDeleteRoleTypeMutation.ts | 98 - .../hooks/mutations/useDeleteUserMutation.ts | 98 - .../useDeleteWebauthnCredentialMutation.ts | 104 - .../mutations/useDisconnectAccountMutation.ts | 58 - .../useExtendTokenExpiresMutation.ts | 58 - .../mutations/useForgotPasswordMutation.ts | 55 - .../mutations/useProvisionBucketMutation.ts | 55 - .../mutations/useProvisionNewUserMutation.ts | 55 - .../useRequestCrossOriginTokenMutation.ts | 58 - .../mutations/useRequestUploadUrlMutation.ts | 55 - .../mutations/useResetPasswordMutation.ts | 55 - .../mutations/useRevokeApiKeyMutation.ts | 55 - .../mutations/useRevokeSessionMutation.ts | 55 - .../useSendAccountDeletionEmailMutation.ts | 60 - .../useSendVerificationEmailMutation.ts | 58 - .../hooks/mutations/useSetPasswordMutation.ts | 55 - .../mutations/useSignInCrossOriginMutation.ts | 58 - .../auth/hooks/mutations/useSignInMutation.ts | 55 - .../hooks/mutations/useSignOutMutation.ts | 55 - .../auth/hooks/mutations/useSignUpMutation.ts | 55 - .../mutations/useUpdateAuditLogMutation.ts | 102 - .../useUpdateCryptoAddressMutation.ts | 116 - .../hooks/mutations/useUpdateEmailMutation.ts | 102 - .../mutations/useUpdatePhoneNumberMutation.ts | 110 - .../mutations/useUpdateRoleTypeMutation.ts | 102 - .../hooks/mutations/useUpdateUserMutation.ts | 102 - .../useUpdateWebauthnCredentialMutation.ts | 116 - .../hooks/mutations/useVerifyEmailMutation.ts | 55 - .../mutations/useVerifyPasswordMutation.ts | 55 - .../hooks/mutations/useVerifyTotpMutation.ts | 55 - .../src/generated/auth/hooks/queries/index.ts | 27 - .../auth/hooks/queries/useAuditLogQuery.ts | 138 - .../auth/hooks/queries/useAuditLogsQuery.ts | 145 - .../hooks/queries/useCryptoAddressQuery.ts | 138 - .../hooks/queries/useCryptoAddressesQuery.ts | 151 - .../hooks/queries/useCurrentIpAddressQuery.ts | 90 - .../hooks/queries/useCurrentUserAgentQuery.ts | 90 - .../hooks/queries/useCurrentUserIdQuery.ts | 90 - .../auth/hooks/queries/useCurrentUserQuery.ts | 125 - .../auth/hooks/queries/useEmailQuery.ts | 138 - .../auth/hooks/queries/useEmailsQuery.ts | 139 - .../queries/useIdentityProvidersQuery.ts | 163 - .../auth/hooks/queries/usePhoneNumberQuery.ts | 138 - .../hooks/queries/usePhoneNumbersQuery.ts | 145 - .../hooks/queries/useRequireStepUpQuery.ts | 105 - .../auth/hooks/queries/useRoleTypeQuery.ts | 138 - .../auth/hooks/queries/useRoleTypesQuery.ts | 145 - .../queries/useUserConnectedAccountQuery.ts | 144 - .../queries/useUserConnectedAccountsQuery.ts | 178 - .../auth/hooks/queries/useUserQuery.ts | 138 - .../auth/hooks/queries/useUsersQuery.ts | 129 - .../queries/useWebauthnCredentialQuery.ts | 144 - .../queries/useWebauthnCredentialsQuery.ts | 171 - .../src/generated/auth/hooks/query-keys.ts | 151 - .../src/generated/auth/hooks/selection.ts | 60 - apps/blocks/src/generated/auth/index.ts | 6 - apps/blocks/src/generated/auth/orm/client.ts | 137 - apps/blocks/src/generated/auth/orm/index.ts | 64 - .../src/generated/auth/orm/input-types.ts | 2966 ---- .../src/generated/auth/orm/models/auditLog.ts | 237 - .../auth/orm/models/cryptoAddress.ts | 237 - .../src/generated/auth/orm/models/email.ts | 237 - .../auth/orm/models/identityProvider.ts | 127 - .../src/generated/auth/orm/models/index.ts | 14 - .../generated/auth/orm/models/phoneNumber.ts | 237 - .../src/generated/auth/orm/models/roleType.ts | 237 - .../src/generated/auth/orm/models/user.ts | 237 - .../auth/orm/models/userConnectedAccount.ts | 169 - .../auth/orm/models/webauthnCredential.ts | 237 - .../src/generated/auth/orm/mutation/index.ts | 875 - .../src/generated/auth/orm/query-builder.ts | 884 - .../src/generated/auth/orm/query/index.ts | 126 - .../src/generated/auth/orm/select-types.ts | 142 - apps/blocks/src/generated/auth/types.ts | 324 - .../src/generated/fixture-manifest.json | 904 - .../src/generated/modules/hooks/client.ts | 42 - .../src/generated/modules/hooks/index.ts | 5 - .../generated/modules/hooks/mutation-keys.ts | 694 - .../useCreateRelationProvisionMutation.ts | 105 - .../useCreateSecureTableProvisionMutation.ts | 91 - .../queries/useRelationProvisionsQuery.ts | 191 - .../src/generated/modules/hooks/query-keys.ts | 619 - .../src/generated/modules/hooks/selection.ts | 60 - apps/blocks/src/generated/modules/index.ts | 1 - .../src/generated/modules/orm/client.ts | 137 - .../blocks/src/generated/modules/orm/index.ts | 158 - .../src/generated/modules/orm/input-types.ts | 13842 ---------------- .../modules/orm/models/agentModule.ts | 237 - .../modules/orm/models/billingModule.ts | 237 - .../orm/models/billingProviderModule.ts | 239 - .../generated/modules/orm/models/blueprint.ts | 237 - .../orm/models/blueprintConstruction.ts | 239 - .../modules/orm/models/blueprintTemplate.ts | 237 - .../modules/orm/models/computeLogModule.ts | 237 - .../modules/orm/models/configSecretsModule.ts | 237 - .../orm/models/configSecretsOrgModule.ts | 239 - .../orm/models/configSecretsUserModule.ts | 239 - .../orm/models/connectedAccountsModule.ts | 239 - .../orm/models/cryptoAddressesModule.ts | 239 - .../modules/orm/models/cryptoAuthModule.ts | 237 - .../orm/models/databaseProvisionModule.ts | 239 - .../modules/orm/models/dbUsageModule.ts | 237 - .../modules/orm/models/defaultIdsModule.ts | 237 - .../orm/models/denormalizedTableField.ts | 239 - .../modules/orm/models/devicesModule.ts | 237 - .../modules/orm/models/emailsModule.ts | 237 - .../modules/orm/models/entityTypeProvision.ts | 237 - .../modules/orm/models/eventsModule.ts | 237 - .../modules/orm/models/functionModule.ts | 237 - .../modules/orm/models/graphModule.ts | 237 - .../modules/orm/models/hierarchyModule.ts | 237 - .../modules/orm/models/i18NModule.ts | 237 - .../orm/models/identityProvidersModule.ts | 239 - .../src/generated/modules/orm/models/index.ts | 61 - .../modules/orm/models/inferenceLogModule.ts | 237 - .../modules/orm/models/invitesModule.ts | 237 - .../modules/orm/models/limitsModule.ts | 237 - .../orm/models/membershipTypesModule.ts | 239 - .../modules/orm/models/membershipsModule.ts | 237 - .../modules/orm/models/merkleStoreModule.ts | 237 - .../modules/orm/models/namespaceModule.ts | 237 - .../modules/orm/models/notificationsModule.ts | 237 - .../modules/orm/models/permissionsModule.ts | 237 - .../modules/orm/models/phoneNumbersModule.ts | 237 - .../modules/orm/models/plansModule.ts | 237 - .../modules/orm/models/profilesModule.ts | 237 - .../orm/models/rateLimitMetersModule.ts | 239 - .../modules/orm/models/rateLimitsModule.ts | 237 - .../modules/orm/models/realtimeModule.ts | 237 - .../modules/orm/models/relationProvision.ts | 237 - .../generated/modules/orm/models/rlsModule.ts | 237 - .../orm/models/secureTableProvision.ts | 239 - .../orm/models/sessionSecretsModule.ts | 239 - .../modules/orm/models/sessionsModule.ts | 237 - .../modules/orm/models/storageLogModule.ts | 237 - .../modules/orm/models/storageModule.ts | 237 - .../modules/orm/models/transferLogModule.ts | 237 - .../modules/orm/models/userAuthModule.ts | 237 - .../orm/models/userCredentialsModule.ts | 239 - .../modules/orm/models/userSettingsModule.ts | 237 - .../modules/orm/models/userStateModule.ts | 237 - .../modules/orm/models/usersModule.ts | 237 - .../modules/orm/models/webauthnAuthModule.ts | 237 - .../orm/models/webauthnCredentialsModule.ts | 244 - .../generated/modules/orm/mutation/index.ts | 408 - .../generated/modules/orm/query-builder.ts | 884 - .../src/generated/modules/orm/query/index.ts | 115 - .../src/generated/modules/orm/select-types.ts | 142 - .../generated/schema-builder/hooks/client.ts | 42 - .../generated/schema-builder/hooks/index.ts | 34 - .../schema-builder/hooks/mutation-keys.ts | 473 - .../hooks/mutations/useCreateFieldMutation.ts | 80 - .../useCreateForeignKeyConstraintMutation.ts | 91 - .../hooks/mutations/useCreateIndexMutation.ts | 80 - .../useCreatePrimaryKeyConstraintMutation.ts | 91 - .../mutations/useCreateTableGrantMutation.ts | 88 - .../hooks/mutations/useCreateTableMutation.ts | 80 - .../useCreateUniqueConstraintMutation.ts | 91 - .../hooks/mutations/useDeleteFieldMutation.ts | 98 - .../useDeleteForeignKeyConstraintMutation.ts | 104 - .../hooks/mutations/useDeleteIndexMutation.ts | 98 - .../mutations/useDeletePolicyMutation.ts | 98 - .../useDeletePrimaryKeyConstraintMutation.ts | 104 - .../hooks/mutations/useDeleteTableMutation.ts | 98 - .../useDeleteUniqueConstraintMutation.ts | 98 - .../hooks/mutations/useUpdateFieldMutation.ts | 102 - .../useUpdateForeignKeyConstraintMutation.ts | 116 - .../hooks/mutations/useUpdateIndexMutation.ts | 102 - .../mutations/useUpdatePolicyMutation.ts | 102 - .../useUpdatePrimaryKeyConstraintMutation.ts | 116 - .../hooks/mutations/useUpdateTableMutation.ts | 102 - .../hooks/queries/useApiSchemasQuery.ts | 145 - .../hooks/queries/useApisQuery.ts | 129 - .../hooks/queries/useDatabasesQuery.ts | 145 - .../hooks/queries/useFieldsQuery.ts | 139 - .../queries/useForeignKeyConstraintsQuery.ts | 178 - .../hooks/queries/useIndicesQuery.ts | 139 - .../hooks/queries/usePoliciesQuery.ts | 139 - .../queries/usePrimaryKeyConstraintsQuery.ts | 178 - .../hooks/queries/useSchemasQuery.ts | 139 - .../hooks/queries/useTableQuery.ts | 138 - .../hooks/queries/useTablesQuery.ts | 139 - .../queries/useUniqueConstraintsQuery.ts | 163 - .../schema-builder/hooks/query-keys.ts | 487 - .../schema-builder/hooks/selection.ts | 60 - .../src/generated/schema-builder/index.ts | 4 - .../generated/schema-builder/orm/client.ts | 137 - .../src/generated/schema-builder/orm/index.ts | 132 - .../schema-builder/orm/input-types.ts | 12788 -------------- .../schema-builder/orm/models/api.ts | 237 - .../schema-builder/orm/models/apiModule.ts | 237 - .../schema-builder/orm/models/apiSchema.ts | 237 - .../schema-builder/orm/models/apiSetting.ts | 237 - .../schema-builder/orm/models/app.ts | 237 - .../orm/models/checkConstraint.ts | 237 - .../orm/models/compositeType.ts | 237 - .../schema-builder/orm/models/corsSetting.ts | 237 - .../schema-builder/orm/models/database.ts | 237 - .../orm/models/databaseSetting.ts | 237 - .../orm/models/databaseTransfer.ts | 237 - .../orm/models/defaultPrivilege.ts | 237 - .../schema-builder/orm/models/domain.ts | 237 - .../orm/models/embeddingChunk.ts | 237 - .../schema-builder/orm/models/enum.ts | 237 - .../schema-builder/orm/models/field.ts | 237 - .../orm/models/foreignKeyConstraint.ts | 239 - .../orm/models/fullTextSearch.ts | 237 - .../schema-builder/orm/models/function.ts | 237 - .../schema-builder/orm/models/index.ts | 48 - .../schema-builder/orm/models/indexModel.ts | 237 - .../orm/models/nodeTypeRegistry.ts | 237 - .../schema-builder/orm/models/partition.ts | 237 - .../schema-builder/orm/models/policy.ts | 237 - .../orm/models/primaryKeyConstraint.ts | 239 - .../orm/models/pubkeySetting.ts | 237 - .../schema-builder/orm/models/rlsSetting.ts | 237 - .../schema-builder/orm/models/schema.ts | 237 - .../schema-builder/orm/models/schemaGrant.ts | 237 - .../schema-builder/orm/models/site.ts | 237 - .../orm/models/siteMetadatum.ts | 237 - .../schema-builder/orm/models/siteModule.ts | 237 - .../schema-builder/orm/models/siteTheme.ts | 237 - .../orm/models/spatialRelation.ts | 237 - .../schema-builder/orm/models/table.ts | 237 - .../schema-builder/orm/models/tableGrant.ts | 237 - .../schema-builder/orm/models/trigger.ts | 237 - .../orm/models/triggerFunction.ts | 237 - .../orm/models/uniqueConstraint.ts | 237 - .../schema-builder/orm/models/view.ts | 237 - .../schema-builder/orm/models/viewGrant.ts | 237 - .../schema-builder/orm/models/viewRule.ts | 237 - .../schema-builder/orm/models/viewTable.ts | 237 - .../orm/models/webauthnSetting.ts | 237 - .../schema-builder/orm/mutation/index.ts | 181 - .../schema-builder/orm/query-builder.ts | 884 - .../schema-builder/orm/query/index.ts | 50 - .../schema-builder/orm/select-types.ts | 142 - .../generated/schema-builder/schema-types.ts | 7712 --------- .../src/generated/schema-builder/types.ts | 779 - 799 files changed, 170724 deletions(-) delete mode 100644 apps/blocks/src/blocks/auth/account-api-keys-list/account-api-keys-list.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-api-keys-list/auth-account-api-keys-list.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-api-keys-list/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-connected-accounts/account-connected-accounts.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-connected-accounts/auth-account-connected-accounts.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-connected-accounts/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-danger-card/account-danger-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-danger-card/auth-account-danger-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-danger-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-deletion-confirm-page/account-deletion-confirm-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-deletion-confirm-page/auth-account-deletion-confirm-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-deletion-confirm-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-emails-list/account-emails-list.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-emails-list/auth-account-emails-list.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-emails-list/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-phones-list/account-phones-list.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-phones-list/auth-account-phones-list.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-phones-list/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-profile-card/account-profile-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-profile-card/auth-account-profile-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-profile-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-security-card/account-security-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-security-card/auth-account-security-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-security-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-sessions-list/account-sessions-list.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-sessions-list/auth-account-sessions-list.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-sessions-list/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/account-settings-page/account-settings-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/account-settings-page/auth-account-settings-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/account-settings-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/anonymous-sign-in-button/anonymous-sign-in-button.tsx delete mode 100644 apps/blocks/src/blocks/auth/anonymous-sign-in-button/auth-anonymous-sign-in-button.requires.json delete mode 100644 apps/blocks/src/blocks/auth/anonymous-sign-in-button/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/api-key-create-dialog/api-key-create-dialog.tsx delete mode 100644 apps/blocks/src/blocks/auth/api-key-create-dialog/auth-api-key-create-dialog.requires.json delete mode 100644 apps/blocks/src/blocks/auth/api-key-create-dialog/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/api-key-created-modal/api-key-created-modal.tsx delete mode 100644 apps/blocks/src/blocks/auth/api-key-created-modal/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/change-password-form/auth-change-password-form.requires.json delete mode 100644 apps/blocks/src/blocks/auth/change-password-form/change-password-form.tsx delete mode 100644 apps/blocks/src/blocks/auth/change-password-form/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/cross-origin-link/auth-cross-origin-link.requires.json delete mode 100644 apps/blocks/src/blocks/auth/cross-origin-link/cross-origin-link.tsx delete mode 100644 apps/blocks/src/blocks/auth/cross-origin-link/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/domain-verification-step/domain-verification-step.tsx delete mode 100644 apps/blocks/src/blocks/auth/domain-verification-step/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/email-otp-input/auth-email-otp-input.requires.json delete mode 100644 apps/blocks/src/blocks/auth/email-otp-input/email-otp-input.tsx delete mode 100644 apps/blocks/src/blocks/auth/email-otp-input/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/email-otp-request-card/auth-email-otp-request-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/email-otp-request-card/email-otp-request-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/email-otp-request-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/forgot-password-card/auth-forgot-password-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/forgot-password-card/forgot-password-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/forgot-password-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/forgot-password-page/forgot-password-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/invitation-acceptance-card/auth-invitation-acceptance-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/invitation-acceptance-card/invitation-acceptance-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/invitation-acceptance-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/invitation-acceptance-page/auth-invitation-acceptance-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/invitation-acceptance-page/invitation-acceptance-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/magic-link-callback-page/auth-magic-link-callback-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/magic-link-callback-page/magic-link-callback-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/magic-link-callback-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/magic-link-request-card/auth-magic-link-request-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/magic-link-request-card/magic-link-request-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/magic-link-request-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/magic-link-sent-page/auth-magic-link-sent-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/magic-link-sent-page/magic-link-sent-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/magic-link-sent-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-backup-codes-display/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-backup-codes-display/mfa-backup-codes-display.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/auth-mfa-backup-codes-regenerate.requires.json delete mode 100644 apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/mfa-backup-codes-regenerate.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-challenge-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-challenge-page/mfa-totp-challenge-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-challenge/auth-mfa-totp-challenge.requires.json delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-challenge/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-challenge/mfa-totp-challenge.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/auth-mfa-totp-disable-confirm.requires.json delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/mfa-totp-disable-confirm.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-enroll/auth-mfa-totp-enroll.requires.json delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-enroll/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-enroll/mfa-totp-enroll.test.tsx delete mode 100644 apps/blocks/src/blocks/auth/mfa-totp-enroll/mfa-totp-enroll.tsx delete mode 100644 apps/blocks/src/blocks/auth/passkey-enroll/auth-passkey-enroll.requires.json delete mode 100644 apps/blocks/src/blocks/auth/passkey-enroll/hooks/use-passkey-enroll.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-enroll/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-enroll/passkey-enroll.tsx delete mode 100644 apps/blocks/src/blocks/auth/passkey-management-list/auth-passkey-management-list.requires.json delete mode 100644 apps/blocks/src/blocks/auth/passkey-management-list/hooks/use-passkey-management.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-management-list/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-management-list/passkey-management-list.tsx delete mode 100644 apps/blocks/src/blocks/auth/passkey-sign-in/auth-passkey-sign-in.requires.json delete mode 100644 apps/blocks/src/blocks/auth/passkey-sign-in/hooks/use-passkey-sign-in.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-sign-in/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/passkey-sign-in/passkey-sign-in.test.tsx delete mode 100644 apps/blocks/src/blocks/auth/passkey-sign-in/passkey-sign-in.tsx delete mode 100644 apps/blocks/src/blocks/auth/reset-password-card/auth-reset-password-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/reset-password-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/reset-password-card/reset-password-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/reset-password-page/reset-password-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-in-card/auth-sign-in-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/sign-in-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/sign-in-card/sign-in-card.test.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-in-card/sign-in-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-in-page/sign-in-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-out-button/auth-sign-out-button.requires.json delete mode 100644 apps/blocks/src/blocks/auth/sign-out-button/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/sign-out-button/sign-out-button.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-up-card/auth-sign-up-card.requires.json delete mode 100644 apps/blocks/src/blocks/auth/sign-up-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/sign-up-card/sign-up-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/sign-up-page/sign-up-page.tsx delete mode 100644 apps/blocks/src/blocks/auth/social-buttons/auth-social-buttons.requires.json delete mode 100644 apps/blocks/src/blocks/auth/social-buttons/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/social-buttons/social-buttons.tsx delete mode 100644 apps/blocks/src/blocks/auth/social-providers-grid/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/social-providers-grid/social-providers-grid.tsx delete mode 100644 apps/blocks/src/blocks/auth/sso-setup-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/sso-setup-card/sso-setup-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/sso-sign-in-card/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/sso-sign-in-card/sso-sign-in-card.tsx delete mode 100644 apps/blocks/src/blocks/auth/step-up-dialog/auth-step-up-dialog.requires.json delete mode 100644 apps/blocks/src/blocks/auth/step-up-dialog/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/step-up-dialog/step-up-dialog.tsx delete mode 100644 apps/blocks/src/blocks/auth/use-step-up/step-up-provider.tsx delete mode 100644 apps/blocks/src/blocks/auth/use-step-up/use-step-up.test.tsx delete mode 100644 apps/blocks/src/blocks/auth/use-step-up/use-step-up.ts delete mode 100644 apps/blocks/src/blocks/auth/verify-email-banner/auth-verify-email-banner.requires.json delete mode 100644 apps/blocks/src/blocks/auth/verify-email-banner/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/verify-email-banner/verify-email-banner.tsx delete mode 100644 apps/blocks/src/blocks/auth/verify-email-page/auth-verify-email-page.requires.json delete mode 100644 apps/blocks/src/blocks/auth/verify-email-page/messages.ts delete mode 100644 apps/blocks/src/blocks/auth/verify-email-page/verify-email-page.tsx delete mode 100644 apps/blocks/src/blocks/chat/api-route.ts delete mode 100644 apps/blocks/src/blocks/chat/api-routes.test.ts delete mode 100644 apps/blocks/src/blocks/chat/api-test-route.ts delete mode 100644 apps/blocks/src/blocks/chat/chat-accessibility.test.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-context.test.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-context.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-fab.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-input.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-message-content.test.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-message-content.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-messages.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-panel.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-settings.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat-widget.tsx delete mode 100644 apps/blocks/src/blocks/chat/chat.types.ts delete mode 100644 apps/blocks/src/blocks/chat/dom-scraper.test.ts delete mode 100644 apps/blocks/src/blocks/chat/dom-scraper.ts delete mode 100644 apps/blocks/src/blocks/chat/index.ts delete mode 100644 apps/blocks/src/blocks/chat/shimmer.tsx delete mode 100644 apps/blocks/src/blocks/chat/tool-message.tsx delete mode 100644 apps/blocks/src/blocks/chat/tool-registry.ts delete mode 100644 apps/blocks/src/blocks/chat/tool-status.tsx delete mode 100644 apps/blocks/src/blocks/chat/tool-ui-config.ts delete mode 100644 apps/blocks/src/blocks/lib/auth-errors.test.ts delete mode 100644 apps/blocks/src/blocks/lib/auth-errors.ts delete mode 100644 apps/blocks/src/blocks/lib/cn.ts delete mode 100644 apps/blocks/src/blocks/lib/password-strength.test.ts delete mode 100644 apps/blocks/src/blocks/lib/password-strength.ts delete mode 100644 apps/blocks/src/blocks/lib/schemas.ts delete mode 100644 apps/blocks/src/blocks/org/app-memberships/app-memberships.tsx delete mode 100644 apps/blocks/src/blocks/org/app-memberships/messages.ts delete mode 100644 apps/blocks/src/blocks/org/app-memberships/org-app-memberships.requires.json delete mode 100644 apps/blocks/src/blocks/org/create-card/create-card.test.tsx delete mode 100644 apps/blocks/src/blocks/org/create-card/create-card.tsx delete mode 100644 apps/blocks/src/blocks/org/create-card/messages.ts delete mode 100644 apps/blocks/src/blocks/org/create-card/org-create-card.requires.json delete mode 100644 apps/blocks/src/blocks/org/invite-dialog/invite-dialog.tsx delete mode 100644 apps/blocks/src/blocks/org/invite-dialog/messages.ts delete mode 100644 apps/blocks/src/blocks/org/invite-dialog/org-invite-dialog.requires.json delete mode 100644 apps/blocks/src/blocks/org/members-list/members-list.test.tsx delete mode 100644 apps/blocks/src/blocks/org/members-list/members-list.tsx delete mode 100644 apps/blocks/src/blocks/org/members-list/messages.ts delete mode 100644 apps/blocks/src/blocks/org/members-list/org-members-list.requires.json delete mode 100644 apps/blocks/src/blocks/org/roles-editor/messages.ts delete mode 100644 apps/blocks/src/blocks/org/roles-editor/org-roles-editor.requires.json delete mode 100644 apps/blocks/src/blocks/org/roles-editor/roles-editor.test.tsx delete mode 100644 apps/blocks/src/blocks/org/roles-editor/roles-editor.tsx delete mode 100644 apps/blocks/src/blocks/org/scim-connections-list/messages.ts delete mode 100644 apps/blocks/src/blocks/org/scim-connections-list/scim-connections-list.tsx delete mode 100644 apps/blocks/src/blocks/org/scim-setup-guide/messages.ts delete mode 100644 apps/blocks/src/blocks/org/scim-setup-guide/scim-setup-guide.tsx delete mode 100644 apps/blocks/src/blocks/org/scim-token-generation-card/messages.ts delete mode 100644 apps/blocks/src/blocks/org/scim-token-generation-card/scim-token-generation-card.tsx delete mode 100644 apps/blocks/src/blocks/org/settings-form/messages.ts delete mode 100644 apps/blocks/src/blocks/org/settings-form/org-settings-form.requires.json delete mode 100644 apps/blocks/src/blocks/org/settings-form/settings-form.tsx delete mode 100644 apps/blocks/src/blocks/primitives/auth-error-alert.tsx delete mode 100644 apps/blocks/src/blocks/primitives/auth-loading-button.tsx delete mode 100644 apps/blocks/src/blocks/primitives/form-field.tsx delete mode 100644 apps/blocks/src/blocks/runtime/blocks-runtime.test.tsx delete mode 100644 apps/blocks/src/blocks/runtime/blocks-runtime.tsx delete mode 100644 apps/blocks/src/blocks/shell/account-menu/account-menu.tsx delete mode 100644 apps/blocks/src/blocks/shell/account-menu/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/account-menu/shell-account-menu.requires.json delete mode 100644 apps/blocks/src/blocks/shell/breadcrumbs/breadcrumbs.tsx delete mode 100644 apps/blocks/src/blocks/shell/breadcrumbs/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/command-palette/command-palette.test.tsx delete mode 100644 apps/blocks/src/blocks/shell/command-palette/command-palette.tsx delete mode 100644 apps/blocks/src/blocks/shell/command-palette/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/command-palette/shell-command-palette-provider.test.tsx delete mode 100644 apps/blocks/src/blocks/shell/command-palette/shell-command-palette-provider.tsx delete mode 100644 apps/blocks/src/blocks/shell/command-palette/shell-command-palette.requires.json delete mode 100644 apps/blocks/src/blocks/shell/command-palette/use-command-palette.ts delete mode 100644 apps/blocks/src/blocks/shell/header/header.tsx delete mode 100644 apps/blocks/src/blocks/shell/header/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/notifications/hooks/use-notifications.ts delete mode 100644 apps/blocks/src/blocks/shell/notifications/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/notifications/notifications.tsx delete mode 100644 apps/blocks/src/blocks/shell/notifications/shell-notifications.requires.json delete mode 100644 apps/blocks/src/blocks/shell/sidebar/messages.ts delete mode 100644 apps/blocks/src/blocks/shell/sidebar/sidebar.stories.tsx delete mode 100644 apps/blocks/src/blocks/shell/sidebar/sidebar.test.tsx delete mode 100644 apps/blocks/src/blocks/shell/sidebar/sidebar.tsx delete mode 100644 apps/blocks/src/blocks/user/context-switcher/context-switcher.test.tsx delete mode 100644 apps/blocks/src/blocks/user/context-switcher/context-switcher.tsx delete mode 100644 apps/blocks/src/blocks/user/context-switcher/hooks/use-switch-context.ts delete mode 100644 apps/blocks/src/blocks/user/context-switcher/hooks/use-user-contexts.ts delete mode 100644 apps/blocks/src/blocks/user/context-switcher/messages.ts delete mode 100644 apps/blocks/src/blocks/user/context-switcher/user-context-switcher.requires.json delete mode 100644 apps/blocks/src/blocks/user/user-avatar/user-avatar.tsx delete mode 100644 apps/blocks/src/blocks/user/user-avatar/user-initials.ts delete mode 100644 apps/blocks/src/generated/README.md delete mode 100644 apps/blocks/src/generated/admin/hooks/client.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/index.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/invalidation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutation-keys.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/index.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCheckPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useConfirmDeleteAccountMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useConfirmUploadMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateApiKeyMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppAchievementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppLevelMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppLevelRequirementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateAppStepMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateIdentityProviderMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateMembershipTypeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgChartEdgeGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgChartEdgeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgGetManagersRecordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgGetSubordinatesRecordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgMemberMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgMemberProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgMembershipSettingMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useCreateOrgProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppAchievementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppLevelMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppLevelRequirementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteAppStepMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteMembershipTypeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgChartEdgeGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgChartEdgeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgMemberMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgMemberProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgMembershipSettingMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDeleteOrgProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useDisconnectAccountMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useExtendTokenExpiresMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useForgotPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useProvisionBucketMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useProvisionNewUserMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useRequestCrossOriginTokenMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useRequestUploadUrlMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useResetPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useRevokeApiKeyMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useRevokeSessionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSendAccountDeletionEmailMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSendVerificationEmailMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSetPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSignInCrossOriginMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSignInMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSignOutMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSignUpMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSubmitAppInviteCodeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useSubmitOrgInviteCodeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppAchievementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppLevelMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppLevelRequirementMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateAppStepMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateMembershipTypeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgAdminGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgChartEdgeGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgChartEdgeMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgClaimedInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgInviteMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgLimitDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgLimitMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgMemberMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgMemberProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgMembershipDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgMembershipMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgMembershipSettingMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgOwnerGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgPermissionDefaultMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgPermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgProfileDefinitionGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgProfileGrantMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgProfileMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useUpdateOrgProfilePermissionMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useVerifyEmailMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useVerifyPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/mutations/useVerifyTotpMutation.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/index.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppAchievementQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppAchievementsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppAdminGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppAdminGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppClaimedInviteQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppClaimedInvitesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppInviteQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppInvitesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLevelQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLevelRequirementQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLevelRequirementsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLevelsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLimitDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLimitDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLimitQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppLimitsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppMembershipDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppMembershipDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppMembershipQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppMembershipsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppOwnerGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppOwnerGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionsGetByMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionsGetMaskByNamesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionsGetMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionsGetPaddedMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppPermissionsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfileDefinitionGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfileDefinitionGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfileGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfileGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfilePermissionQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfilePermissionsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfileQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppProfilesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppStepQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useAppStepsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useCurrentIpAddressQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useCurrentUserAgentQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useCurrentUserIdQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useCurrentUserQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useIdentityProvidersQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useMembershipTypeQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useMembershipTypesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgAdminGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgAdminGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgChartEdgeGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgChartEdgeGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgChartEdgeQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgChartEdgesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgClaimedInviteQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgClaimedInvitesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgGetManagersQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgGetSubordinatesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgInviteQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgInvitesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgIsManagerOfQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgLimitDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgLimitDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgLimitQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgLimitsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMemberProfileQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMemberProfilesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMemberQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembersQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipSettingQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipSettingsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgMembershipsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgOwnerGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgOwnerGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionDefaultQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionDefaultsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionsGetByMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionsGetMaskByNamesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionsGetMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionsGetPaddedMaskQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgPermissionsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfileDefinitionGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfileDefinitionGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfileGrantQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfileGrantsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfilePermissionQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfilePermissionsQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfileQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useOrgProfilesQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useRequireStepUpQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useStepsAchievedQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/queries/useStepsRequiredQuery.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/query-keys.ts delete mode 100644 apps/blocks/src/generated/admin/hooks/selection.ts delete mode 100644 apps/blocks/src/generated/admin/index.ts delete mode 100644 apps/blocks/src/generated/admin/orm/client.ts delete mode 100644 apps/blocks/src/generated/admin/orm/index.ts delete mode 100644 apps/blocks/src/generated/admin/orm/input-types.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appAchievement.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appAdminGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appClaimedInvite.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appInvite.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appLevel.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appLevelRequirement.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appLimit.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appLimitDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appMembership.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appMembershipDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appOwnerGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appPermission.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appPermissionDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appProfile.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appProfileDefinitionGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appProfileGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appProfilePermission.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/appStep.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/identityProvider.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/index.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/membershipType.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgAdminGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgChartEdge.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgChartEdgeGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgClaimedInvite.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgGetManagersRecord.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgGetSubordinatesRecord.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgInvite.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgLimit.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgLimitDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgMember.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgMemberProfile.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgMembership.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgMembershipDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgMembershipSetting.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgOwnerGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgPermission.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgPermissionDefault.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgProfile.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgProfileDefinitionGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgProfileGrant.ts delete mode 100644 apps/blocks/src/generated/admin/orm/models/orgProfilePermission.ts delete mode 100644 apps/blocks/src/generated/admin/orm/mutation/index.ts delete mode 100644 apps/blocks/src/generated/admin/orm/query-builder.ts delete mode 100644 apps/blocks/src/generated/admin/orm/query/index.ts delete mode 100644 apps/blocks/src/generated/admin/orm/select-types.ts delete mode 100644 apps/blocks/src/generated/admin/types.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/client.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/index.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/invalidation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutation-keys.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/index.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCheckPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useConfirmDeleteAccountMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useConfirmUploadMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateApiKeyMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateAuditLogMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateCryptoAddressMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateIdentityProviderMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreatePhoneNumberMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateRoleTypeMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateUserConnectedAccountMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateUserMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useCreateWebauthnCredentialMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteAuditLogMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteCryptoAddressMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeletePhoneNumberMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteRoleTypeMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteUserMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDeleteWebauthnCredentialMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useDisconnectAccountMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useExtendTokenExpiresMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useForgotPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useProvisionBucketMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useProvisionNewUserMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useRequestCrossOriginTokenMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useRequestUploadUrlMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useResetPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useRevokeApiKeyMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useRevokeSessionMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSendAccountDeletionEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSendVerificationEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSetPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSignInCrossOriginMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSignInMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSignOutMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useSignUpMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateAuditLogMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateCryptoAddressMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdatePhoneNumberMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateRoleTypeMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateUserMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useUpdateWebauthnCredentialMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useVerifyEmailMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useVerifyPasswordMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/mutations/useVerifyTotpMutation.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/index.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useAuditLogQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useAuditLogsQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCryptoAddressQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCryptoAddressesQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCurrentIpAddressQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCurrentUserAgentQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCurrentUserIdQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useCurrentUserQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useEmailQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useEmailsQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useIdentityProvidersQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/usePhoneNumberQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/usePhoneNumbersQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useRequireStepUpQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useRoleTypeQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useRoleTypesQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useUserConnectedAccountQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useUserConnectedAccountsQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useUserQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useUsersQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useWebauthnCredentialQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/queries/useWebauthnCredentialsQuery.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/query-keys.ts delete mode 100644 apps/blocks/src/generated/auth/hooks/selection.ts delete mode 100644 apps/blocks/src/generated/auth/index.ts delete mode 100644 apps/blocks/src/generated/auth/orm/client.ts delete mode 100644 apps/blocks/src/generated/auth/orm/index.ts delete mode 100644 apps/blocks/src/generated/auth/orm/input-types.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/auditLog.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/cryptoAddress.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/email.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/identityProvider.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/index.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/phoneNumber.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/roleType.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/user.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/userConnectedAccount.ts delete mode 100644 apps/blocks/src/generated/auth/orm/models/webauthnCredential.ts delete mode 100644 apps/blocks/src/generated/auth/orm/mutation/index.ts delete mode 100644 apps/blocks/src/generated/auth/orm/query-builder.ts delete mode 100644 apps/blocks/src/generated/auth/orm/query/index.ts delete mode 100644 apps/blocks/src/generated/auth/orm/select-types.ts delete mode 100644 apps/blocks/src/generated/auth/types.ts delete mode 100644 apps/blocks/src/generated/fixture-manifest.json delete mode 100644 apps/blocks/src/generated/modules/hooks/client.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/index.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/mutation-keys.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/mutations/useCreateRelationProvisionMutation.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/mutations/useCreateSecureTableProvisionMutation.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/queries/useRelationProvisionsQuery.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/query-keys.ts delete mode 100644 apps/blocks/src/generated/modules/hooks/selection.ts delete mode 100644 apps/blocks/src/generated/modules/index.ts delete mode 100644 apps/blocks/src/generated/modules/orm/client.ts delete mode 100644 apps/blocks/src/generated/modules/orm/index.ts delete mode 100644 apps/blocks/src/generated/modules/orm/input-types.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/agentModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/billingModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/billingProviderModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/blueprint.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/blueprintConstruction.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/blueprintTemplate.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/computeLogModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/configSecretsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/configSecretsOrgModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/configSecretsUserModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/connectedAccountsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/cryptoAddressesModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/cryptoAuthModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/databaseProvisionModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/dbUsageModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/defaultIdsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/denormalizedTableField.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/devicesModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/emailsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/entityTypeProvision.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/eventsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/functionModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/graphModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/hierarchyModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/i18NModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/identityProvidersModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/index.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/inferenceLogModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/invitesModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/limitsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/membershipTypesModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/membershipsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/merkleStoreModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/namespaceModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/notificationsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/permissionsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/phoneNumbersModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/plansModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/profilesModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/rateLimitMetersModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/rateLimitsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/realtimeModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/relationProvision.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/rlsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/secureTableProvision.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/sessionSecretsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/sessionsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/storageLogModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/storageModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/transferLogModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/userAuthModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/userCredentialsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/userSettingsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/userStateModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/usersModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/webauthnAuthModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/models/webauthnCredentialsModule.ts delete mode 100644 apps/blocks/src/generated/modules/orm/mutation/index.ts delete mode 100644 apps/blocks/src/generated/modules/orm/query-builder.ts delete mode 100644 apps/blocks/src/generated/modules/orm/query/index.ts delete mode 100644 apps/blocks/src/generated/modules/orm/select-types.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/client.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutation-keys.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateFieldMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateForeignKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateIndexMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreatePrimaryKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateTableGrantMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateTableMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useCreateUniqueConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeleteFieldMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeleteForeignKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeleteIndexMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeletePolicyMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeletePrimaryKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeleteTableMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useDeleteUniqueConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdateFieldMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdateForeignKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdateIndexMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdatePolicyMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdatePrimaryKeyConstraintMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/mutations/useUpdateTableMutation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useApiSchemasQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useApisQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useDatabasesQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useFieldsQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useForeignKeyConstraintsQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useIndicesQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/usePoliciesQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/usePrimaryKeyConstraintsQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useSchemasQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useTableQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useTablesQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/queries/useUniqueConstraintsQuery.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/query-keys.ts delete mode 100644 apps/blocks/src/generated/schema-builder/hooks/selection.ts delete mode 100644 apps/blocks/src/generated/schema-builder/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/client.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/input-types.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/api.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/apiModule.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/apiSchema.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/apiSetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/app.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/checkConstraint.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/compositeType.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/corsSetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/database.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/databaseSetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/databaseTransfer.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/defaultPrivilege.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/domain.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/embeddingChunk.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/enum.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/field.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/foreignKeyConstraint.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/fullTextSearch.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/function.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/indexModel.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/nodeTypeRegistry.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/partition.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/policy.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/primaryKeyConstraint.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/pubkeySetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/rlsSetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/schema.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/schemaGrant.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/site.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/siteMetadatum.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/siteModule.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/siteTheme.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/spatialRelation.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/table.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/tableGrant.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/trigger.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/triggerFunction.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/uniqueConstraint.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/view.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/viewGrant.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/viewRule.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/viewTable.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/models/webauthnSetting.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/mutation/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/query-builder.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/query/index.ts delete mode 100644 apps/blocks/src/generated/schema-builder/orm/select-types.ts delete mode 100644 apps/blocks/src/generated/schema-builder/schema-types.ts delete mode 100644 apps/blocks/src/generated/schema-builder/types.ts diff --git a/apps/blocks/src/blocks/auth/account-api-keys-list/account-api-keys-list.tsx b/apps/blocks/src/blocks/auth/account-api-keys-list/account-api-keys-list.tsx deleted file mode 100644 index 9653f0f..0000000 --- a/apps/blocks/src/blocks/auth/account-api-keys-list/account-api-keys-list.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client'; - -/** - * account-api-keys-list (registry: auth-account-api-keys-list) - * - * Displays the signed-in user's API keys and provides revoke + create actions. - * Because `user_api_keys` is a `constructive_auth_private` view with NO public - * API, there is no generated list hook — the list is supplied by the host via the - * `keys` adapter prop (default: empty array, renders the empty state). - * Only the `revokeApiKey` mutation is bindable today via `useRevokeApiKeyMutation` - * from `@/generated/auth`. - * - * SDK gap: no `useUserApiKeysQuery` hook exists (sdk-binding-contract.md §10). - * When a `UserApiKeysConnection` ships, add `useUserApiKeysQuery` from - * `@/generated/auth` and update `requires.json` with `"queries":["userApiKeys"]`. - * - * Key creation: delegated to `ApiKeyCreateDialog` (auth-api-key-create-dialog), - * which enforces step-up tier:'high' internally. After creation, `ApiKeyCreatedModal` - * (auth-api-key-created-modal) shows the one-time raw key. - * - * Revocation: single revoke uses a confirmation dialog (no step-up — spec §Step-up). - * - * Binding doctrine (sdk-binding-contract.md §3, §5): - * • Generated hook imported from `@/generated/auth` — never `@constructive-io/data`. - * • No `configure()`/`getClient()`, no `QueryClientProvider`. Host mounts blocks-runtime. - * • `onRevokeSubmit` override seam replaces the default hook call. - */ - -import { useState } from 'react'; - -import { Badge } from '@constructive-io/ui/badge'; -import { Button } from '@constructive-io/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@constructive-io/ui/dialog'; -import { Separator } from '@constructive-io/ui/separator'; - -import { cn } from '@/lib/utils'; -import { useRevokeApiKeyMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { ApiKeyCreateDialog, type ApiKeyCreatedResult } from '@/blocks/auth/api-key-create-dialog/api-key-create-dialog'; -import { ApiKeyCreatedModal } from '@/blocks/auth/api-key-created-modal/api-key-created-modal'; - -import { - defaultAccountApiKeysListMessages, - type AccountApiKeysListMessages, - type AccountApiKeysListMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export type { ApiKeyCreatedResult }; - -/** - * A single API key row. The host supplies rows from whatever list source it has. - * There is NO generated list hook for `user_api_keys` (private view, no public API - * → no `*Connection` type). sdk-binding-contract.md §10 documents this gap. - */ -export type ApiKeyRow = { - id: string; - name: string; - /** First visible chars of the raw key, stored at creation time. */ - keyPrefix: string; - accessLevel: string; - mfaLevel: string; - lastUsedAt: string | null; - expiresAt: string | null; - createdAt: string; -}; - -/** Variables passed to the `onRevokeSubmit` override. */ -export type RevokeApiKeyVars = { - keyId: string; -}; - -/** Result shape; mirrors the `revokeApiKey` payload fields this block selects. */ -export type RevokeApiKeyResult = { - result: boolean | null; -}; - -export type AccountApiKeysListProps = { - /** - * The list of API keys to display. There is NO generated list hook for - * `user_api_keys` (it is in `constructive_auth_private`, no public API → - * no `*Connection` type). The host must supply rows; the default is `[]` - * which renders the empty state. - * - * sdk-binding-contract.md §10 documents this gap. - */ - keys?: ApiKeyRow[]; - /** Maximum number of API keys allowed per user. Used to gate the create button. */ - maxKeys?: number; - /** Override the `useRevokeApiKeyMutation` call. */ - onRevokeSubmit?: (vars: RevokeApiKeyVars) => Promise; - /** Fires after a key is successfully revoked. Always fires. */ - onKeyRevoked?: (keyId: string) => void; - /** Fires after `auth-api-key-create-dialog` succeeds. Always fires. */ - onKeyCreated?: (result: ApiKeyCreatedResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - messages?: AccountApiKeysListMessageOverrides; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Format ISO date for display. */ -function formatDate(iso: string | null, fallback: string): string { - if (!iso) return fallback; - try { - return new Date(iso).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - } catch { - return fallback; - } -} - -/** Returns true if expiresAt is in the past. */ -function isExpired(expiresAt: string | null): boolean { - if (!expiresAt) return false; - return new Date(expiresAt).getTime() < Date.now(); -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountApiKeysList({ - keys = [], - maxKeys, - onRevokeSubmit: onRevokeSubmitOverride, - onKeyRevoked, - onKeyCreated, - onError, - onMessage, - messages: messageOverrides, - className -}: AccountApiKeysListProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountApiKeysListMessages = { - ...defaultAccountApiKeysListMessages, - ...messageOverrides, - errors: { ...defaultAccountApiKeysListMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook — `revokeApiKey` takes `{ input: { keyId } }`. - // Payload: `{ revokeApiKey: { result: boolean | null } | null }`. - const defaultRevokeMutation = useRevokeApiKeyMutation({ - selection: { - fields: { result: true } - } - }); - - // Hybrid pending: override path tracks its own pending state. - const [overridePending, setOverridePending] = useState(false); - const isRevokePending = onRevokeSubmitOverride ? overridePending : defaultRevokeMutation.isPending; - - // Confirmation dialog state - const [confirmKey, setConfirmKey] = useState(null); - const [error, setError] = useState(null); - - // Create dialog state - const [createOpen, setCreateOpen] = useState(false); - - // Created modal state — holds the raw key after creation (shown once) - const [pendingCreatedKey, setPendingCreatedKey] = useState(null); - - const isMaxReached = maxKeys !== undefined && keys.length >= maxKeys; - - async function runRevoke(keyId: string): Promise { - if (onRevokeSubmitOverride) return onRevokeSubmitOverride({ keyId }); - const data = await defaultRevokeMutation.mutateAsync({ input: { keyId } }); - if (!data.revokeApiKey) return null; - return { result: data.revokeApiKey.result ?? null }; - } - - async function handleRevoke(key: ApiKeyRow) { - setError(null); - if (onRevokeSubmitOverride) setOverridePending(true); - try { - await runRevoke(key.id); - setConfirmKey(null); - onMessage?.({ kind: 'success', key: 'revokeApiKey.success', message: merged.keyRevokedMessage }); - onKeyRevoked?.(key.id); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const errKey = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key: errKey, message }); - onError?.({ message, code: errKey }); - } finally { - if (onRevokeSubmitOverride) setOverridePending(false); - } - } - - function handleCreateSuccess(result: ApiKeyCreatedResult) { - // Close create dialog and open created-modal with the raw key. - setCreateOpen(false); - setPendingCreatedKey(result); - onKeyCreated?.(result); - onMessage?.({ kind: 'success', key: 'createApiKey.success' }); - } - - function handleCreatedModalDismissed() { - setPendingCreatedKey(null); - } - - return ( - - -
-
- {merged.title} - {merged.description} -
- -
- {isMaxReached && ( -

- {merged.maxKeysReached} -

- )} -
- - - - - {keys.length === 0 ? ( -

{merged.noKeysDescription}

- ) : ( -
    - {keys.map((key, idx) => { - const expired = isExpired(key.expiresAt); - const expiryLabel = key.expiresAt - ? expired - ? merged.expired - : formatDate(key.expiresAt, merged.noExpiry) - : merged.noExpiry; - const lastUsedLabel = formatDate(key.lastUsedAt, merged.neverUsed); - - return ( -
  • - {idx > 0 && } -
    -
    -
    - - {key.name} - - {expired && ( - - {merged.expired} - - )} -
    - - {key.keyPrefix}… - -
    - - {merged.accessLevelHeader}: {key.accessLevel} - - - {merged.lastUsedHeader}: {lastUsedLabel} - - - {merged.expiresHeader}: {expiryLabel} - -
    -
    - - -
    -
  • - ); - })} -
- )} -
- - {/* Revoke confirmation dialog */} - { - if (!open) setConfirmKey(null); - }} - > - - - {merged.revokeConfirmTitle} - {merged.revokeConfirmDescription} - - - - - - { - if (confirmKey) handleRevoke(confirmKey); - }} - data-testid="revoke-confirm-button" - > - {merged.revokeConfirmButton} - - - - - - {/* Create API key dialog (step-up is handled inside ApiKeyCreateDialog) */} - - - {/* Created-modal: shows the one-time raw key after creation */} - {pendingCreatedKey && ( - { - if (!open) setPendingCreatedKey(null); - }} - apiKey={pendingCreatedKey.rawKey} - keyName={pendingCreatedKey.name} - expiresAt={pendingCreatedKey.expiresAt} - onDismissed={handleCreatedModalDismissed} - /> - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-api-keys-list/auth-account-api-keys-list.requires.json b/apps/blocks/src/blocks/auth/account-api-keys-list/auth-account-api-keys-list.requires.json deleted file mode 100644 index abaf575..0000000 --- a/apps/blocks/src/blocks/auth/account-api-keys-list/auth-account-api-keys-list.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["revokeApiKey"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-api-keys-list/messages.ts b/apps/blocks/src/blocks/auth/account-api-keys-list/messages.ts deleted file mode 100644 index 5a80557..0000000 --- a/apps/blocks/src/blocks/auth/account-api-keys-list/messages.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * account-api-keys-list — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * NOTE: The list-query surface is out-of-frontend-scope (sdk-binding-contract.md §10). - * Only the `revokeApiKey` mutation is bindable today. The host supplies rows via - * the `keys` adapter prop; the default is `[]` which renders the empty state. - */ - -export type AccountApiKeysListMessages = { - title: string; - description: string; - createButton: string; - nameHeader: string; - prefixHeader: string; - accessLevelHeader: string; - lastUsedHeader: string; - expiresHeader: string; - revokeButton: string; - revokeConfirmTitle: string; - revokeConfirmDescription: string; - revokeConfirmButton: string; - revokeCancelButton: string; - keyRevokedMessage: string; - neverUsed: string; - noExpiry: string; - expired: string; - maxKeysReached: string; - noKeysDescription: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: top-level copy is shallow-partial; `errors` is - * itself partial so a host can localize a single error code without restating - * the whole map. - */ -export type AccountApiKeysListMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAccountApiKeysListMessages: AccountApiKeysListMessages = { - title: 'API keys', - description: 'API keys allow programmatic access to your account. Treat them like passwords.', - createButton: 'Create API key', - nameHeader: 'Name', - prefixHeader: 'Key', - accessLevelHeader: 'Access', - lastUsedHeader: 'Last used', - expiresHeader: 'Expires', - revokeButton: 'Revoke', - revokeConfirmTitle: 'Revoke API key?', - revokeConfirmDescription: 'This key will stop working immediately. This action cannot be undone.', - revokeConfirmButton: 'Revoke key', - revokeCancelButton: 'Cancel', - keyRevokedMessage: 'API key revoked.', - neverUsed: 'Never', - noExpiry: 'No expiry', - expired: 'Expired', - maxKeysReached: 'Maximum number of API keys reached.', - noKeysDescription: 'No API keys yet. Create one to get started.', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-connected-accounts/account-connected-accounts.tsx b/apps/blocks/src/blocks/auth/account-connected-accounts/account-connected-accounts.tsx deleted file mode 100644 index 0bcaf31..0000000 --- a/apps/blocks/src/blocks/auth/account-connected-accounts/account-connected-accounts.tsx +++ /dev/null @@ -1,436 +0,0 @@ -'use client'; - -/** - * account-connected-accounts (registry: auth-account-connected-accounts) - * - * Settings card listing linked OAuth providers with a disconnect action. - * Also renders "Connect [provider]" links for configured providers that are - * not yet linked. The disconnect action is gated behind a step-up (tier:medium) - * identity re-verification dialog. - * - * Binding doctrine (sdk-binding-contract.md, MASTER-PROMPT §5): - * • Data path = `useDisconnectAccountMutation` from `@/generated/auth`, called - * with a `selection` field-picker. No fetch, no GraphQL document string, no - * hardcoded URL, no `@constructive-io/data`. - * • NO client bootstrap: never calls `configure()`/`getClient()`, never mounts - * ``. The host's `@constructive/blocks-runtime` does that. - * • Override seam: `onSubmitDisconnect` fully replaces the generated-hook call. - * • Error mapping via `parseGraphQLError` from the `auth-errors` foundation lib. - * • Connected-account list and identity-provider list are CONDITIONAL: the spec - * confirms no public Connection types exist yet (sdk-binding-contract.md §10). - * The block accepts `connectedAccounts` and `providers` as props (static data - * supplied by the host) until the backend exposes Connection types. If the - * host omits them, the block renders an empty but valid state. - * - * Step-up flow: - * Disconnect button → confirmation dialog → step-up (tier: medium) → mutation. - * If step-up is cancelled, the confirmation dialog re-opens silently. - */ - -import { useState } from 'react'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Separator } from '@constructive-io/ui/separator'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; -import { Avatar, AvatarFallback } from '@constructive-io/ui/avatar'; - -import { cn } from '@/lib/utils'; -import { useDisconnectAccountMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; - -import { - defaultAccountConnectedAccountsMessages, - type AccountConnectedAccountsMessageOverrides, - type AccountConnectedAccountsMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -/** A linked OAuth account row. */ -export type ConnectedAccountRow = { - /** Internal record id (uuid). */ - id: string; - /** Provider slug, e.g. 'google', 'github', 'apple'. */ - service: string; - /** Display name for the linked identity (email or username). */ - identifier: string; - /** Whether the OAuth identity has been verified. */ - isVerified: boolean; - /** ISO timestamp when the link was created. */ - createdAt: string; -}; - -/** - * An identity provider that is configured but may not be linked. - * Coordinate this shape with auth-social-providers-grid. - */ -export type IdentityProvider = { - id: string; - /** Provider slug, e.g. 'google', 'github'. */ - slug: string; - /** Human-readable display name shown in the UI. */ - displayName: string; - kind: 'oidc' | 'oauth2'; - enabled: boolean; -}; - -/** - * Variables the disconnect call receives. - * The override `onSubmitDisconnect` gets these verbatim. - */ -export type DisconnectAccountVars = { - accountId: string; -}; - -/** Result returned by the disconnect call. */ -export type DisconnectAccountResult = { - success: boolean; -}; - -export type AccountConnectedAccountsProps = { - /** - * Pre-fetched connected account rows. - * When omitted, the block renders an empty connected list (no Connection query - * exists yet — sdk-binding-contract.md §10 FLAG). The host supplies these from - * its own query until a public ConnectedAccountsConnection type is confirmed. - */ - connectedAccounts?: ConnectedAccountRow[]; - /** - * Static list of identity providers to render "Connect" links for. - * Providers already in `connectedAccounts` are shown as connected; others as - * "not connected". When omitted, falls back to an empty list. - */ - providers?: IdentityProvider[]; - /** - * Base URL for initiating an OAuth connection flow. - * The block appends `?provider=&action=connect` to this URL. - * Default: '/auth/social'. - */ - oauthRedirectBase?: string; - messages?: AccountConnectedAccountsMessageOverrides; - /** Replace the default `useDisconnectAccountMutation` call. */ - onSubmitDisconnect?: (vars: DisconnectAccountVars) => Promise; - /** Fires after a successful disconnect. Always fires. */ - onAccountDisconnected?: (accountId: string, provider: string) => void; - /** Fires when the host signals a successful OAuth connection back to the block. */ - onAccountConnected?: (provider: string) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Provider initials fallback for the avatar. */ -function providerInitials(slug: string): string { - return slug.slice(0, 2).toUpperCase(); -} - -/** Build an OAuth connect URL from the base + provider slug. */ -function connectUrl(base: string, slug: string): string { - const sep = base.includes('?') ? '&' : '?'; - return `${base}${sep}provider=${encodeURIComponent(slug)}&action=connect`; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountConnectedAccounts({ - connectedAccounts = [], - providers = [], - oauthRedirectBase = '/auth/social', - messages: messageOverrides, - onSubmitDisconnect: onSubmitDisconnectOverride, - onAccountDisconnected, - onAccountConnected: _onAccountConnected, - onError, - onMessage, - className -}: AccountConnectedAccountsProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountConnectedAccountsMessages = { - ...defaultAccountConnectedAccountsMessages, - ...messageOverrides, - errors: { ...defaultAccountConnectedAccountsMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook from the host's `auth` SDK. - // Payload shape (verified from DisconnectAccountPayload): - // { disconnectAccount: { clientMutationId?: string|null; result?: boolean|null } | null } - const defaultMutation = useDisconnectAccountMutation({ - selection: { fields: { result: true } } - }); - - // Hybrid pending: generated hook tracks its own; the override path does not. - const [overridePending, setOverridePending] = useState(false); - // Step-up pending: prevents double-clicking confirm while step-up is awaiting user input. - const [stepUpPending, setStepUpPending] = useState(false); - const isPending = stepUpPending || (onSubmitDisconnectOverride ? overridePending : defaultMutation.isPending); - - // Step-up hook (tier: medium — password re-verification, per step-up-contract §6). - const stepUp = useStepUp(); - - // Row-level error state — shown inline above the list. - const [rowError, setRowError] = useState(null); - - // Disconnect confirmation state. - const [confirmTarget, setConfirmTarget] = useState(null); - - // --------------------------------------------------------------------------- - // Derived data — build a unified provider list sorted: connected first, - // then unconnected; each group alphabetical by service/slug. - // --------------------------------------------------------------------------- - - const connectedIds = new Set(connectedAccounts.map((a) => a.service)); - - // Rows for connected accounts that have provider metadata. - const connectedRows = connectedAccounts - .map((account) => { - const meta = providers.find((p) => p.slug === account.service); - return { account, meta }; - }) - .sort((a, b) => a.account.service.localeCompare(b.account.service)); - - // Providers not yet connected. - const unconnectedProviders = providers - .filter((p) => p.enabled && !connectedIds.has(p.slug)) - .sort((a, b) => a.slug.localeCompare(b.slug)); - - const isEmpty = connectedRows.length === 0 && unconnectedProviders.length === 0; - - // --------------------------------------------------------------------------- - // Handlers - // --------------------------------------------------------------------------- - - /** Called after the confirmation dialog is confirmed — gates on step-up. */ - async function handleDisconnectConfirm() { - if (!confirmTarget) return; - const target = confirmTarget; - setRowError(null); - - try { - // Step-up: tier medium → password re-verification (step-up-contract.md §6). - // setStepUpPending guards against double-click during the step-up modal. - setStepUpPending(true); - await stepUp({ tier: 'medium' }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - // User cancelled step-up — re-show the confirmation dialog silently. - setConfirmTarget(target); - return; - } - // Unexpected step-up failure — surface as error. - const message = merged.errors.UNKNOWN_ERROR; - const code = 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key: code, message }); - onError?.({ message, code }); - return; - } finally { - // Always clear step-up pending (runs before any early return too). - setStepUpPending(false); - } - - // Step-up passed — execute the disconnect. - if (onSubmitDisconnectOverride) setOverridePending(true); - try { - const vars: DisconnectAccountVars = { accountId: target.id }; - - if (onSubmitDisconnectOverride) { - await onSubmitDisconnectOverride(vars); - } else { - const data = await defaultMutation.mutateAsync({ input: vars }); - const success = data.disconnectAccount?.result ?? false; - if (!success) { - throw Object.assign(new Error('Disconnect returned false'), { - extensions: { code: 'UNKNOWN_ERROR' } - }); - } - } - - setConfirmTarget(null); - onMessage?.({ kind: 'success', key: 'disconnectAccount.success', message: merged.disconnectedToast }); - onAccountDisconnected?.(target.id, target.service); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - setConfirmTarget(null); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitDisconnectOverride) setOverridePending(false); - } - } - - // --------------------------------------------------------------------------- - // Render - // --------------------------------------------------------------------------- - - return ( - <> - - - {merged.title} - {merged.description} - - - - {rowError && ( -
- -
- )} - - {isEmpty ? ( -

- {merged.noProvidersMessage} -

- ) : ( -
    - {/* Connected accounts */} - {connectedRows.map(({ account, meta }, idx) => { - const displayName = meta?.displayName ?? account.service; - return ( -
  • - {idx > 0 && } -
    - - - {providerInitials(account.service)} - - - -
    - - {displayName} - - - {account.identifier} - -
    - -
    - {account.isVerified ? ( - - {merged.verifiedBadge} - - ) : ( - - {merged.connectedLabel} - - )} - -
    -
    -
  • - ); - })} - - {/* Unconnected providers */} - {unconnectedProviders.map((provider, idx) => ( -
  • - {(connectedRows.length > 0 || idx > 0) && } -
    - - - {providerInitials(provider.slug)} - - - -
    - - {provider.displayName} - - - {merged.notConnectedLabel} - -
    - - -
    -
  • - ))} -
- )} -
-
- - {/* Disconnect confirmation dialog */} - { - if (!isOpen) setConfirmTarget(null); - }} - > - - - {merged.disconnectConfirmTitle} - {merged.disconnectConfirmDescription} - - - - - - {merged.disconnectConfirmButton} - - - - - - ); -} diff --git a/apps/blocks/src/blocks/auth/account-connected-accounts/auth-account-connected-accounts.requires.json b/apps/blocks/src/blocks/auth/account-connected-accounts/auth-account-connected-accounts.requires.json deleted file mode 100644 index 559fbc0..0000000 --- a/apps/blocks/src/blocks/auth/account-connected-accounts/auth-account-connected-accounts.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["disconnectAccount"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-connected-accounts/messages.ts b/apps/blocks/src/blocks/auth/account-connected-accounts/messages.ts deleted file mode 100644 index 4635dc6..0000000 --- a/apps/blocks/src/blocks/auth/account-connected-accounts/messages.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * account-connected-accounts — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localises any code by overriding a single key. - * - * The override type uses a deep-partial so callers can override a single error - * code without restating the entire map. - */ - -export type AccountConnectedAccountsMessages = { - title: string; - description: string; - connectedLabel: string; - notConnectedLabel: string; - disconnectButton: string; - connectButton: (providerName: string) => string; - disconnectConfirmTitle: string; - disconnectConfirmDescription: string; - disconnectConfirmButton: string; - disconnectCancelButton: string; - disconnectedToast: string; - verifiedBadge: string; - loadingLabel: string; - noProvidersMessage: string; - errors: { - LAST_AUTH_METHOD: string; - UNKNOWN_ERROR: string; - }; -}; - -export type AccountConnectedAccountsMessageOverrides = Partial< - Omit -> & { - connectButton?: (providerName: string) => string; - errors?: Partial; -}; - -export const defaultAccountConnectedAccountsMessages: AccountConnectedAccountsMessages = { - title: 'Connected accounts', - description: 'Link third-party accounts for sign-in and data access.', - connectedLabel: 'Connected', - notConnectedLabel: 'Not connected', - disconnectButton: 'Disconnect', - connectButton: (name) => `Connect ${name}`, - disconnectConfirmTitle: 'Disconnect account?', - disconnectConfirmDescription: 'You will no longer be able to sign in with this account.', - disconnectConfirmButton: 'Disconnect', - disconnectCancelButton: 'Cancel', - disconnectedToast: 'Account disconnected.', - verifiedBadge: 'Verified', - loadingLabel: 'Loading…', - noProvidersMessage: 'No identity providers are configured.', - errors: { - LAST_AUTH_METHOD: - 'Cannot disconnect your only sign-in method. Add a password or another account first.', - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-danger-card/account-danger-card.tsx b/apps/blocks/src/blocks/auth/account-danger-card/account-danger-card.tsx deleted file mode 100644 index 0cb70f0..0000000 --- a/apps/blocks/src/blocks/auth/account-danger-card/account-danger-card.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client'; - -/** - * account-danger-card (registry: auth-account-danger-card) - * - * Danger zone card that initiates the account-deletion flow. The flow is: - * 1. User clicks "Delete account" → confirmation dialog opens. - * 2. User clicks "Send deletion email" in dialog → step-up tier:high fires. - * 3. Step-up resolves → `sendAccountDeletionEmail` mutation is called. - * 4. Success → dialog closes; card shows inline success state. - * - * Data path: `useSendAccountDeletionEmailMutation` from `@/generated/auth`. - * The hook name is VERIFIED against the generated SDK source. Input shape is - * `{ input: SendAccountDeletionEmailInput }` where the input is empty (only - * optional `clientMutationId`). Payload: `{ sendAccountDeletionEmail: { result } }`. - * - * Step-up: `useStepUp()` from the `use-step-up` registry block, tier: 'high'. - * If the user cancels step-up the dialog re-opens (returns to confirm state). - * - * Binding rules (sdk-binding-contract.md §11 — ALL honoured): - * • Generated hook only; no fetch, no doc string, no configure()/getClient(). - * • No QueryClientProvider / QueryClient in this file. - * • Override seam: `onSubmit` fully replaces the mutation call. - * • `requires.json` co-located. - */ - -import { useState } from 'react'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@constructive-io/ui/dialog'; - -import { cn } from '@/lib/utils'; -import { useSendAccountDeletionEmailMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; - -import { defaultAccountDangerCardMessages, type AccountDangerCardMessages, type AccountDangerCardMessageOverrides } from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type AccountDangerCardProps = { - messages?: AccountDangerCardMessageOverrides; - /** Replace the default `useSendAccountDeletionEmailMutation` call. */ - onSubmit?: () => Promise; - /** Fires after `sendAccountDeletionEmail` succeeds. */ - onDeletionEmailSent?: () => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountDangerCard({ - messages: messageOverrides, - onSubmit: onSubmitOverride, - onDeletionEmailSent, - onError, - onMessage, - className -}: AccountDangerCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountDangerCardMessages = { - ...defaultAccountDangerCardMessages, - ...messageOverrides, - errors: { ...defaultAccountDangerCardMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook from the host's `auth` SDK. - // Payload: { sendAccountDeletionEmail: { result?: boolean | null } | null } - const defaultMutation = useSendAccountDeletionEmailMutation({ - selection: { - fields: { - result: true - } - } - }); - - // Hybrid pending: generated hook tracks its own; override path does not. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - const stepUp = useStepUp(); - - // Dialog state: 'closed' | 'confirm' (dialog open) | 'done' (email sent) - const [dialogOpen, setDialogOpen] = useState(false); - const [emailSent, setEmailSent] = useState(false); - const [error, setError] = useState(null); - - async function handleConfirm() { - setError(null); - try { - // Step-up gate: tier 'high' → MFA preferred, password fallback. - await stepUp({ tier: 'high', messages: { passwordDescription: merged.stepUpPrompt } }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - // Cancelled: keep the dialog open so the user can re-attempt. - onMessage?.({ kind: 'warning', key: 'STEP_UP_CANCELLED', message: merged.stepUpCancelled }); - return; - } - // Step-up error (not cancel) — close dialog, surface error. - setDialogOpen(false); - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - return; - } - - // Step-up passed — fire the mutation. - try { - if (onSubmitOverride) { - setOverridePending(true); - await onSubmitOverride(); - } else { - await defaultMutation.mutateAsync({ input: {} }).then((d) => d.sendAccountDeletionEmail); - } - - setDialogOpen(false); - setEmailSent(true); - onMessage?.({ kind: 'success', key: 'sendAccountDeletionEmail.success', message: merged.emailSentTitle }); - onDeletionEmailSent?.(); - } catch (err) { - setDialogOpen(false); - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - return ( - - - {merged.title} - {merged.description} - - - - - - {emailSent ? ( -
-

{merged.emailSentTitle}

-

{merged.emailSentDescription}

-
- ) : ( - - )} -
- - {/* Confirmation dialog */} - { if (!open) setDialogOpen(false); }}> - - - {merged.confirmDialogTitle} - {merged.confirmDialogDescription} - - -

{merged.confirmDialogBody}

- - - - - {merged.confirmButton} - - -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-danger-card/auth-account-danger-card.requires.json b/apps/blocks/src/blocks/auth/account-danger-card/auth-account-danger-card.requires.json deleted file mode 100644 index 53d72a3..0000000 --- a/apps/blocks/src/blocks/auth/account-danger-card/auth-account-danger-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["sendAccountDeletionEmail"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-danger-card/messages.ts b/apps/blocks/src/blocks/auth/account-danger-card/messages.ts deleted file mode 100644 index 0e2c067..0000000 --- a/apps/blocks/src/blocks/auth/account-danger-card/messages.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * account-danger-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localises any code by overriding a single key. - */ - -export type AccountDangerCardMessages = { - title: string; - description: string; - deleteButton: string; - confirmDialogTitle: string; - confirmDialogDescription: string; - confirmDialogBody: string; - confirmButton: string; - cancelButton: string; - stepUpPrompt: string; - emailSentTitle: string; - emailSentDescription: string; - stepUpCancelled: string; - loadingLabel: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -export type AccountDangerCardMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAccountDangerCardMessages: AccountDangerCardMessages = { - title: 'Danger zone', - description: 'Permanently delete your account and all associated data.', - deleteButton: 'Delete account', - confirmDialogTitle: 'Delete your account?', - confirmDialogDescription: 'This action cannot be undone. All your data will be permanently deleted.', - confirmDialogBody: 'We will send you a confirmation email. Click the link in that email to complete deletion.', - confirmButton: 'Send deletion email', - cancelButton: 'Cancel', - stepUpPrompt: 'Confirm your identity before deleting your account.', - emailSentTitle: 'Check your inbox', - emailSentDescription: - 'A confirmation email has been sent. Follow the link in the email to permanently delete your account.', - stepUpCancelled: 'Step-up verification cancelled.', - loadingLabel: 'Sending...', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/account-deletion-confirm-page.tsx b/apps/blocks/src/blocks/auth/account-deletion-confirm-page/account-deletion-confirm-page.tsx deleted file mode 100644 index 808657e..0000000 --- a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/account-deletion-confirm-page.tsx +++ /dev/null @@ -1,369 +0,0 @@ -'use client'; - -/** - * account-deletion-confirm-page (registry: auth-account-deletion-confirm-page) - * - * Next.js page that handles the /auth/delete-account?token=…&user_id=… link - * from the deletion confirmation email. Calls `confirmDeleteAccount` once on - * mount and renders three outcome states: - * - * • processing — spinner while the mutation is in-flight - * • success — account deleted; redirects to sign-in after 2 s - * • error — expired or invalid token (inline state, no redirect) - * - * Data path: generated hook `useConfirmDeleteAccountMutation` imported from - * `@/generated/auth`. No fetch, no GraphQL document string, no client bootstrap. - * - * Binding doctrine: sdk-binding-contract.md §3–§7 - * Canonical anatomy: MASTER-PROMPT §5 - * - * Editable constants after install: - * const DEFAULT_REDIRECT = '/auth/sign-in'; - * const ACCOUNT_SETTINGS_HREF = '/account/settings'; - * const REDIRECT_DELAY_MS = 2000; - */ - -import { useEffect, useRef, useState } from 'react'; -import { useRouter } from 'next/navigation'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { useConfirmDeleteAccountMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; - -import { - defaultAccountDeletionConfirmMessages, - type AccountDeletionConfirmMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Editable constants (installed page — consumer modifies these in place) -// --------------------------------------------------------------------------- -const DEFAULT_REDIRECT = '/auth/sign-in'; -const ACCOUNT_SETTINGS_HREF = '/account/settings'; -const REDIRECT_DELAY_MS = 2000; - -// --------------------------------------------------------------------------- -// Outcome states -// --------------------------------------------------------------------------- - -type ConfirmStatus = 'pending' | 'success' | 'expired' | 'invalid' | 'error'; - -// --------------------------------------------------------------------------- -// Error code → status mapping -// --------------------------------------------------------------------------- - -const EXPIRED_CODES = new Set(['TOKEN_EXPIRED', 'LINK_EXPIRED', 'DELETION_TOKEN_EXPIRED']); -const INVALID_CODES = new Set([ - 'TOKEN_INVALID', - 'LINK_INVALID', - 'INVALID_TOKEN', - 'TOKEN_ALREADY_USED', - 'DELETION_TOKEN_INVALID' -]); - -function codeToStatus(code: string | null): 'expired' | 'invalid' | 'error' { - if (code && EXPIRED_CODES.has(code)) return 'expired'; - if (code && INVALID_CODES.has(code)) return 'invalid'; - return 'error'; -} - -/** - * Extract the raw error code from an error object before parseGraphQLError - * may strip it (parseGraphQLError returns code: null for unknown codes). - * Used for status routing (expired vs invalid vs generic error). - */ -function extractRawCode(err: unknown): string | null { - if (!err || typeof err !== 'object') return null; - const e = err as Record; - // extensions.code (GraphQL format) - if (e.extensions && typeof e.extensions === 'object') { - const ext = e.extensions as Record; - if (typeof ext.code === 'string') return ext.code; - } - // .errors[0].extensions.code (GraphQLRequestError format) - if (Array.isArray(e.errors) && e.errors.length > 0) { - const first = e.errors[0] as Record; - if (first.extensions && typeof first.extensions === 'object') { - const ext = first.extensions as Record; - if (typeof ext.code === 'string') return ext.code; - } - } - return null; -} - -// --------------------------------------------------------------------------- -// Message overrides type -// --------------------------------------------------------------------------- - -export type AccountDeletionConfirmMessageOverrides = Partial> & { - errors?: Partial; -}; - -// --------------------------------------------------------------------------- -// Props -// --------------------------------------------------------------------------- - -export type AccountDeletionConfirmPageProps = { - /** Deletion token read from URL query param `token`. */ - token: string; - /** User id read from URL query param `user_id`. */ - userId: string; - messages?: AccountDeletionConfirmMessageOverrides; - /** Path to redirect to after successful deletion. Defaults to `/auth/sign-in`. */ - redirectTo?: string; - /** - * Override the account settings href shown in the expired-token CTA. - * Defaults to `/account/settings`. - * v1 extension — not in the base spec `AccountDeletionConfirmViewProps`. - */ - accountSettingsHref?: string; - /** Replace the default `useConfirmDeleteAccountMutation` call. */ - onSubmit?: (vars: { userId: string; token: string }) => Promise; - /** Fires after successful deletion (before redirect). */ - onSuccess?: (result: { userId: string }) => void; - /** Fires on expired token error. */ - onExpired?: () => void; - /** Fires on invalid or already-used token error. */ - onInvalid?: () => void; - /** Fires after a mapped error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, errors, and non-fatal branches. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountDeletionConfirmPage({ - token, - userId, - messages: messageOverrides, - redirectTo = DEFAULT_REDIRECT, - accountSettingsHref = ACCOUNT_SETTINGS_HREF, - onSubmit: onSubmitOverride, - onSuccess, - onExpired, - onInvalid, - onError, - onMessage, - className -}: AccountDeletionConfirmPageProps) { - // Deep merge - const merged: AccountDeletionConfirmMessages = { - ...defaultAccountDeletionConfirmMessages, - ...messageOverrides, - errors: { ...defaultAccountDeletionConfirmMessages.errors, ...messageOverrides?.errors } - }; - - const router = useRouter(); - - // Track outcome state - const [status, setStatus] = useState('pending'); - const [errorMessage, setErrorMessage] = useState(null); - - // Generated hook — no client bootstrap, no provider - const defaultMutation = useConfirmDeleteAccountMutation({ - selection: { fields: { result: true } } - }); - - // Override pending state - const [overridePending, setOverridePending] = useState(false); - - // The irreversible confirmation request is shared across Strict Mode effect - // replays, while each effect setup owns whether it may commit the result. - const confirmationPromiseRef = useRef | null>(null); - - useEffect(() => { - let active = true; - let redirectTimer: ReturnType | null = null; - - // Guard: missing params → invalid state immediately, no API call - if (!userId || !token) { - setStatus('invalid'); - return () => { - active = false; - if (redirectTimer !== null) clearTimeout(redirectTimer); - }; - } - - async function startConfirmation(): Promise { - if (onSubmitOverride) { - setOverridePending(true); - return onSubmitOverride({ userId, token }); - } - - const data = await defaultMutation.mutateAsync({ input: { userId, token } }); - return data.confirmDeleteAccount?.result ?? null; - } - - confirmationPromiseRef.current ??= startConfirmation(); - const confirmationPromise = confirmationPromiseRef.current; - - void confirmationPromise - .then((deleted) => { - if (!active) return; - if (onSubmitOverride) setOverridePending(false); - - if (deleted) { - setStatus('success'); - onMessage?.({ kind: 'success', key: 'confirmDeleteAccount.success' }); - onSuccess?.({ userId }); - // Redirect after a brief delay so the user sees the success state. - redirectTimer = setTimeout(() => { - if (active) router.push(redirectTo); - }, REDIRECT_DELAY_MS); - } else { - // Server returned false / null without throwing → treat as invalid - setStatus('invalid'); - const msg = merged.errors.UNKNOWN_ERROR; - setErrorMessage(msg); - onMessage?.({ kind: 'error', key: 'UNKNOWN_ERROR', message: msg }); - onInvalid?.(); - onError?.({ message: msg, code: 'UNKNOWN_ERROR' }); - } - }) - .catch((err: unknown) => { - if (!active) return; - if (onSubmitOverride) setOverridePending(false); - // extractRawCode reads the code BEFORE parseGraphQLError may strip it - // for unknown codes (parseGraphQLError returns code: null when the code - // is not in ERROR_CODES, which means TOKEN_EXPIRED etc. are unknown). - const rawCode = extractRawCode(err); - const parsed = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - // Use rawCode for status routing (covers expired/invalid token codes), - // but use parsed code as the notification key (fallback to rawCode if null). - const notifyCode = parsed.code ?? rawCode ?? 'UNKNOWN_ERROR'; - // When there is no recognised code at all, fall back to the UNKNOWN_ERROR - // message from the messages catalog so overrides work end-to-end. - const displayMessage = - parsed.code !== null ? parsed.message : (merged.errors.UNKNOWN_ERROR ?? parsed.message); - const derivedStatus = codeToStatus(rawCode); - setStatus(derivedStatus); - // For expired/invalid, suppress the redundant generic error message - // — the state already renders its own descriptive heading + description. - setErrorMessage(derivedStatus === 'expired' || derivedStatus === 'invalid' ? null : displayMessage); - onMessage?.({ kind: 'error', key: notifyCode, message: displayMessage }); - if (derivedStatus === 'expired') onExpired?.(); - else if (derivedStatus === 'invalid') onInvalid?.(); - onError?.({ message: displayMessage, code: notifyCode }); - }); - - return () => { - active = false; - if (redirectTimer !== null) clearTimeout(redirectTimer); - }; - // Intentionally snapshot the initial token, user, mutation, handlers, messages, - // router, and redirect target: a confirmation link is processed only once. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - return ( -
- - {/* Processing state */} - {(status === 'pending' || isPending) && ( - <> - - {merged.processingTitle} - {merged.processingDescription} - - -
- -
-
- - )} - - {/* Success state */} - {status === 'success' && ( - <> - - {merged.successTitle} - {merged.successDescription} - - - - - - )} - - {/* Expired state */} - {status === 'expired' && ( - <> - - {merged.expiredTitle} - {merged.expiredDescription} - - - - - - - - - )} - - {/* Invalid / error state */} - {(status === 'invalid' || status === 'error') && ( - <> - - {merged.invalidTitle} - {merged.invalidDescription} - - - - - - - - - )} -
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/auth-account-deletion-confirm-page.requires.json b/apps/blocks/src/blocks/auth/account-deletion-confirm-page/auth-account-deletion-confirm-page.requires.json deleted file mode 100644 index fda44ad..0000000 --- a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/auth-account-deletion-confirm-page.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["confirmDeleteAccount"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/messages.ts b/apps/blocks/src/blocks/auth/account-deletion-confirm-page/messages.ts deleted file mode 100644 index ca1875b..0000000 --- a/apps/blocks/src/blocks/auth/account-deletion-confirm-page/messages.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * account-deletion-confirm-page — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type AccountDeletionConfirmMessages = { - processingTitle: string; - processingDescription: string; - successTitle: string; - successDescription: string; - successButton: string; - expiredTitle: string; - expiredDescription: string; - expiredButton: string; - invalidTitle: string; - invalidDescription: string; - invalidButton: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -export const defaultAccountDeletionConfirmMessages: AccountDeletionConfirmMessages = { - processingTitle: 'Deleting your account…', - processingDescription: 'Please wait while we process your request.', - successTitle: 'Account deleted', - successDescription: - 'Your account and all associated data have been permanently deleted. Thank you for using our service.', - successButton: 'Go to sign in', - expiredTitle: 'Link expired', - expiredDescription: - 'This deletion link has expired. Please request a new deletion email from your account settings.', - expiredButton: 'Go to account settings', - invalidTitle: 'Invalid link', - invalidDescription: - 'This deletion link is invalid or has already been used. If you believe this is an error, contact support.', - invalidButton: 'Go to sign in', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-emails-list/account-emails-list.tsx b/apps/blocks/src/blocks/auth/account-emails-list/account-emails-list.tsx deleted file mode 100644 index d2aef65..0000000 --- a/apps/blocks/src/blocks/auth/account-emails-list/account-emails-list.tsx +++ /dev/null @@ -1,594 +0,0 @@ -'use client'; - -/** - * account-emails-list (registry: auth-account-emails-list) - * - * Manages the signed-in user's email addresses. Displays all rows from the - * generated `useEmailsQuery`, lets the user add a new address (via - * `useCreateEmailMutation` + `useSendVerificationEmailMutation`), promote any - * verified address to primary (`useUpdateEmailMutation`), and delete non-primary - * addresses (`useDeleteEmailMutation`). Each row shows verified/unverified - * badges plus a "Verify" CTA for unverified addresses. - * - * ADD-EMAIL PATH: uses `createEmail` first (inserts the row), then - * `sendVerificationEmail` (queues the verification email). This two-step path - * is required because `sendVerificationEmail` only sends to an existing address - * — its `input.email` field is optional and refers to an already-registered row. - * Using `createEmail` alone (without verification send) would leave the row - * permanently unverified with no inbox prompt. - * - * Binding doctrine: - * • All data via generated hooks from `@/generated/auth`. NO fetch, NO GraphQL - * document strings, NO `@constructive-io/data`, NO `configure()`/`getClient()`. - * • Override seams: `onSubmitAdd`, `onSubmitSetPrimary`, `onSubmitDelete` fully - * replace the respective generated-hook call. - * • Error mapping via `parseGraphQLError`; inline `` for form - * errors; per-action errors reported via `onError` / `onMessage`. - * • `onMessage`/`onSuccess`-style seams fire on every operation. - */ - -import { useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Separator } from '@constructive-io/ui/separator'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; - -import { cn } from '@/lib/utils'; -import { - useEmailsQuery, - useCreateEmailMutation, - useSendVerificationEmailMutation, - useUpdateEmailMutation, - useDeleteEmailMutation -} from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { addEmailSchema, type AddEmailFormData } from '@/blocks/lib/schemas'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; - -import { defaultAccountEmailsListMessages, type AccountEmailsListMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type EmailRow = { - id: string; - email: string; - isPrimary: boolean; - isVerified: boolean; - name: string | null; - createdAt: string; -}; - -/** - * Message overrides. Top-level copy is shallow-partial; `errors` is itself - * partial so a host can localize a single error code without restating the map. - */ -export type AccountEmailsListMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type AccountEmailsListProps = { - /** Fires after a new email row is created and verification email queued. */ - onEmailAdded?: (email: EmailRow) => void; - /** Fires after primary is promoted. */ - onPrimaryChanged?: (email: EmailRow) => void; - /** Fires after a non-primary email is deleted. */ - onEmailDeleted?: (emailId: string) => void; - /** Fires after a mapped error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and non-fatal branches. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - /** Override the add-email operation (createEmail + sendVerificationEmail). */ - onSubmitAdd?: (emailAddress: string) => Promise; - /** Override the set-primary operation. */ - onSubmitSetPrimary?: (emailId: string) => Promise; - /** Override the delete operation. */ - onSubmitDelete?: (emailId: string) => Promise; - /** Override the resend-verification operation. */ - onSubmitResendVerification?: (emailAddress: string) => Promise; - messages?: AccountEmailsListMessageOverrides; - /** Disables add/delete/primary operations. Read-only display mode. */ - readOnly?: boolean; - /** Max number of email addresses allowed. Default: 10. */ - maxEmails?: number; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Email field selection — mirrors EmailRow shape -// --------------------------------------------------------------------------- - -const EMAIL_FIELDS = { - id: true, - email: true, - isPrimary: true, - isVerified: true, - name: true, - createdAt: true -} as const; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountEmailsList({ - onEmailAdded, - onPrimaryChanged, - onEmailDeleted, - onError, - onMessage, - onSubmitAdd: onSubmitAddOverride, - onSubmitSetPrimary: onSubmitSetPrimaryOverride, - onSubmitDelete: onSubmitDeleteOverride, - onSubmitResendVerification: onSubmitResendVerificationOverride, - messages: messageOverrides, - readOnly = false, - maxEmails = 10, - className -}: AccountEmailsListProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountEmailsListMessages = { - ...defaultAccountEmailsListMessages, - ...messageOverrides, - errors: { ...defaultAccountEmailsListMessages.errors, ...messageOverrides?.errors } - }; - - // ------------------------------------------------------------------------- - // Query — list emails - // ------------------------------------------------------------------------- - - const emailsQuery = useEmailsQuery({ - selection: { - fields: EMAIL_FIELDS, - orderBy: ['CREATED_AT_DESC'] - } - }); - - const emails: EmailRow[] = (emailsQuery.data?.emails?.nodes ?? []) as EmailRow[]; - - // ------------------------------------------------------------------------- - // Mutations - // ------------------------------------------------------------------------- - - const createEmailMutation = useCreateEmailMutation({ - selection: { fields: EMAIL_FIELDS } - }); - - const sendVerificationMutation = useSendVerificationEmailMutation({ - selection: { fields: { result: true } } - }); - - const updateEmailMutation = useUpdateEmailMutation({ - selection: { fields: EMAIL_FIELDS } - }); - - const deleteEmailMutation = useDeleteEmailMutation({ - selection: { fields: { id: true } } - }); - - // ------------------------------------------------------------------------- - // Dialog state — add email - // ------------------------------------------------------------------------- - - const [addDialogOpen, setAddDialogOpen] = useState(false); - const [addError, setAddError] = useState(null); - const [addOverridePending, setAddOverridePending] = useState(false); - - const isAddPending = onSubmitAddOverride - ? addOverridePending - : createEmailMutation.isPending || sendVerificationMutation.isPending; - - // ------------------------------------------------------------------------- - // Delete confirm state - // ------------------------------------------------------------------------- - - const [deleteTargetId, setDeleteTargetId] = useState(null); - const [deleteOverridePending, setDeleteOverridePending] = useState(false); - const isDeletePending = onSubmitDeleteOverride ? deleteOverridePending : deleteEmailMutation.isPending; - - // ------------------------------------------------------------------------- - // Per-row action pending tracking - // ------------------------------------------------------------------------- - - const [primaryPendingId, setPrimaryPendingId] = useState(null); - const [verifyPendingId, setVerifyPendingId] = useState(null); - const [rowError, setRowError] = useState(null); - - // ------------------------------------------------------------------------- - // Add email form - // ------------------------------------------------------------------------- - - const addForm = useForm({ - defaultValues: { email: '' } as AddEmailFormData, - onSubmit: async ({ value }) => { - await handleAdd(value.email); - } - }); - - // ------------------------------------------------------------------------- - // Handlers - // ------------------------------------------------------------------------- - - async function handleAdd(emailAddress: string) { - setAddError(null); - if (onSubmitAddOverride) setAddOverridePending(true); - try { - addEmailSchema.parse({ email: emailAddress }); - - let newRow: EmailRow; - if (onSubmitAddOverride) { - newRow = await onSubmitAddOverride(emailAddress); - } else { - // Step 1: create the email row - const createData = await createEmailMutation.mutateAsync({ email: emailAddress }); - newRow = createData.createEmail.email as unknown as EmailRow; - // Step 2: queue the verification email - await sendVerificationMutation.mutateAsync({ input: { email: emailAddress } }); - } - - onMessage?.({ kind: 'success', key: 'emailAdded', message: merged.emailAddedMessage }); - onEmailAdded?.(newRow); - setAddDialogOpen(false); - addForm.reset(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setAddError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitAddOverride) setAddOverridePending(false); - } - } - - async function handleSetPrimary(emailId: string) { - setRowError(null); - setPrimaryPendingId(emailId); - try { - let updatedRow: EmailRow; - if (onSubmitSetPrimaryOverride) { - updatedRow = await onSubmitSetPrimaryOverride(emailId); - } else { - const data = await updateEmailMutation.mutateAsync({ id: emailId, emailPatch: { isPrimary: true } }); - updatedRow = data.updateEmail.email as unknown as EmailRow; - } - onMessage?.({ kind: 'success', key: 'primaryChanged', message: merged.primaryChangedMessage }); - onPrimaryChanged?.(updatedRow); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setPrimaryPendingId(null); - } - } - - async function handleResendVerification(row: EmailRow) { - setRowError(null); - setVerifyPendingId(row.id); - try { - if (onSubmitResendVerificationOverride) { - await onSubmitResendVerificationOverride(row.email); - } else { - await sendVerificationMutation.mutateAsync({ input: { email: row.email } }); - } - onMessage?.({ kind: 'info', key: 'verificationSent', message: merged.verificationSentMessage }); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setVerifyPendingId(null); - } - } - - async function handleDeleteConfirm() { - if (!deleteTargetId) return; - setRowError(null); - if (onSubmitDeleteOverride) setDeleteOverridePending(true); - try { - if (onSubmitDeleteOverride) { - await onSubmitDeleteOverride(deleteTargetId); - } else { - await deleteEmailMutation.mutateAsync({ id: deleteTargetId }); - } - const deletedId = deleteTargetId; - setDeleteTargetId(null); - onMessage?.({ kind: 'success', key: 'emailDeleted', message: merged.emailDeletedMessage }); - onEmailDeleted?.(deletedId); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - setDeleteTargetId(null); - } finally { - if (onSubmitDeleteOverride) setDeleteOverridePending(false); - } - } - - // ------------------------------------------------------------------------- - // Derived - // ------------------------------------------------------------------------- - - const atMax = emails.length >= maxEmails; - const deleteTarget = emails.find((e) => e.id === deleteTargetId); - - // ------------------------------------------------------------------------- - // Render - // ------------------------------------------------------------------------- - - return ( - <> - - -
- {merged.title} - {merged.description} -
- {!readOnly && ( - - )} -
- - - {rowError && ( -
- -
- )} - - {emailsQuery.isLoading ? ( -
- Loading… -
- ) : emails.length === 0 ? ( -
- No email addresses found. -
- ) : ( -
    - {emails.map((row, idx) => ( -
  • - {idx > 0 && } -
    - {/* Email + badges */} -
    - - {row.email} - -
    - {row.isPrimary && ( - - {merged.primaryBadge} - - )} - {row.isVerified ? ( - - {merged.verifiedBadge} - - ) : ( - - {merged.unverifiedBadge} - - )} -
    -
    - - {/* Actions */} - {!readOnly && ( -
    - {/* Resend verification */} - {!row.isVerified && ( - - )} - - {/* Set primary — hidden for already-primary rows */} - {!row.isPrimary && row.isVerified && ( - - )} - - {/* Delete — disabled for primary email */} - -
    - )} -
    -
  • - ))} -
- )} -
-
- - {/* --------------------------------------------------------------- - Add email dialog - --------------------------------------------------------------- */} - { - if (!isOpen) { - setAddDialogOpen(false); - setAddError(null); - } - }} - > - - - {merged.addEmailDialogTitle} - - - -
- - -
{ - e.preventDefault(); - e.stopPropagation(); - addForm.handleSubmit(); - }} - > - { - if (!value) return 'Email is required'; - if (!/\S+@\S+\.\S+/.test(value)) return 'Please enter a valid email'; - return undefined; - } - }} - > - {(field) => ( - - )} - - - - - - {merged.addEmailSubmit} - - -
-
-
-
- - {/* --------------------------------------------------------------- - Delete confirm dialog - --------------------------------------------------------------- */} - { - if (!isOpen) setDeleteTargetId(null); - }} - > - - - {merged.deleteConfirmTitle} - {merged.deleteConfirmDescription} - - - - - - {merged.deleteConfirmButton} - - - - - - {/* Invisible element to expose deleteTarget for tests */} - {deleteTarget && ( - - {deleteTarget.email} - - )} - - ); -} diff --git a/apps/blocks/src/blocks/auth/account-emails-list/auth-account-emails-list.requires.json b/apps/blocks/src/blocks/auth/account-emails-list/auth-account-emails-list.requires.json deleted file mode 100644 index 1ace750..0000000 --- a/apps/blocks/src/blocks/auth/account-emails-list/auth-account-emails-list.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["createEmail", "sendVerificationEmail", "updateEmail", "deleteEmail"], - "queries": ["emails"], - "models": ["email"] -} diff --git a/apps/blocks/src/blocks/auth/account-emails-list/messages.ts b/apps/blocks/src/blocks/auth/account-emails-list/messages.ts deleted file mode 100644 index 5de2b2d..0000000 --- a/apps/blocks/src/blocks/auth/account-emails-list/messages.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * account-emails-list — message catalog - * - * Canonical block-messages pattern: top-level camelCase keys are UI copy; - * the nested `errors` map is keyed by backend error CODE (UPPER_SNAKE_CASE) - * and is handed straight to `parseGraphQLError` as `customMessages`, so a - * host localises any code by overriding a single key. - */ - -export type AccountEmailsListMessages = { - title: string; - description: string; - addEmailButton: string; - addEmailDialogTitle: string; - addEmailLabel: string; - addEmailPlaceholder: string; - addEmailSubmit: string; - addEmailSubmitting: string; - primaryBadge: string; - verifiedBadge: string; - unverifiedBadge: string; - verifyButton: string; - setPrimaryButton: string; - deleteButton: string; - deleteConfirmTitle: string; - deleteConfirmDescription: string; - deleteConfirmButton: string; - deleteCancelButton: string; - verificationSentMessage: string; - emailAddedMessage: string; - primaryChangedMessage: string; - emailDeletedMessage: string; - cannotDeletePrimary: string; - errors: { - EMAIL_TAKEN: string; - RATE_LIMITED: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultAccountEmailsListMessages: AccountEmailsListMessages = { - title: 'Email addresses', - description: 'Manage your email addresses. Your primary email is used for sign-in and notifications.', - addEmailButton: 'Add email address', - addEmailDialogTitle: 'Add email address', - addEmailLabel: 'Email address', - addEmailPlaceholder: 'you@example.com', - addEmailSubmit: 'Add address', - addEmailSubmitting: 'Adding…', - primaryBadge: 'Primary', - verifiedBadge: 'Verified', - unverifiedBadge: 'Unverified', - verifyButton: 'Verify', - setPrimaryButton: 'Set as primary', - deleteButton: 'Remove', - deleteConfirmTitle: 'Remove email address?', - deleteConfirmDescription: 'This email address will be removed from your account.', - deleteConfirmButton: 'Remove', - deleteCancelButton: 'Cancel', - verificationSentMessage: 'Verification email sent.', - emailAddedMessage: 'Email address added. Check your inbox to verify it.', - primaryChangedMessage: 'Primary email address updated.', - emailDeletedMessage: 'Email address removed.', - cannotDeletePrimary: 'You cannot remove your primary email address.', - errors: { - EMAIL_TAKEN: 'This email address is already associated with another account.', - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-phones-list/account-phones-list.tsx b/apps/blocks/src/blocks/auth/account-phones-list/account-phones-list.tsx deleted file mode 100644 index 73aeb5e..0000000 --- a/apps/blocks/src/blocks/auth/account-phones-list/account-phones-list.tsx +++ /dev/null @@ -1,1062 +0,0 @@ -'use client'; - -/** - * account-phones-list (registry: auth-account-phones-list) - * - * Multi-phone management card. Lists the signed-in user's phone numbers from the - * generated `usePhoneNumbersQuery`, lets the user add a new number (create row - * via `useCreatePhoneNumberMutation` then trigger OTP send via the - * `onSubmitSendOtp` override seam — SMS procedures are backend-pending), verify - * with an inline 6-digit OTP (`onSubmitVerifyOtp` override seam), set a primary - * number (`useUpdatePhoneNumberMutation`), and delete with confirmation - * (`useDeletePhoneNumberMutation`). - * - * BACKEND-PENDING (CASE b): `send_sms_otp` and `verify_phone_otp` procedures - * are NOT yet deployed in constructive_auth_public, so their generated hooks - * (`useSendSmsOtpMutation`, `useVerifyPhoneOtpMutation`) do NOT exist in the - * SDK. The add/verify flow therefore uses `onSubmitSendOtp` / `onSubmitVerifyOtp` - * as the primary (required) seams for those two operations. Hosts wire the - * generated bindings once they regenerate the SDK after deployment. - * `requires.json` names both pending ops so `check-sdk-fixtures.ts` fails clearly. - * - * Binding doctrine: - * • All list/CRUD data via generated hooks from `@/generated/auth`. NO fetch, - * NO GraphQL document strings, NO `@constructive-io/data`, NO `configure()`. - * • Override seams: `onSubmitAdd`, `onSubmitSendOtp`, `onSubmitVerifyOtp`, - * `onSubmitSetPrimary`, `onSubmitDelete` fully replace respective calls. - * • Error mapping via `parseGraphQLError`; inline `` for form - * errors; per-action errors reported via `onError` / `onMessage`. - * • OTP dialog stays open after phone creation; user enters the code inline. - * • 60-second resend cooldown tracked with a useEffect timeout. - */ - -import { useEffect, useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Separator } from '@constructive-io/ui/separator'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; - -import { cn } from '@/lib/utils'; -import { - usePhoneNumbersQuery, - useCreatePhoneNumberMutation, - useUpdatePhoneNumberMutation, - useDeletePhoneNumberMutation -} from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; - -import { - defaultAccountPhonesListMessages, - type AccountPhonesListMessages, - type AccountPhonesListMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Country codes — minimal list (no libphonenumber dependency) -// --------------------------------------------------------------------------- - -const COUNTRY_CODES = [ - { code: '+1', label: 'US / CA (+1)', value: '+1' }, - { code: '+44', label: 'GB (+44)', value: '+44' }, - { code: '+61', label: 'AU (+61)', value: '+61' }, - { code: '+33', label: 'FR (+33)', value: '+33' }, - { code: '+49', label: 'DE (+49)', value: '+49' }, - { code: '+81', label: 'JP (+81)', value: '+81' }, - { code: '+82', label: 'KR (+82)', value: '+82' }, - { code: '+86', label: 'CN (+86)', value: '+86' }, - { code: '+91', label: 'IN (+91)', value: '+91' }, - { code: '+52', label: 'MX (+52)', value: '+52' }, - { code: '+55', label: 'BR (+55)', value: '+55' }, - { code: '+34', label: 'ES (+34)', value: '+34' }, - { code: '+39', label: 'IT (+39)', value: '+39' }, - { code: '+7', label: 'RU (+7)', value: '+7' }, - { code: '+31', label: 'NL (+31)', value: '+31' }, - { code: '+46', label: 'SE (+46)', value: '+46' }, - { code: '+47', label: 'NO (+47)', value: '+47' }, - { code: '+45', label: 'DK (+45)', value: '+45' }, - { code: '+358', label: 'FI (+358)', value: '+358' }, - { code: '+41', label: 'CH (+41)', value: '+41' } -]; - -const OTP_RESEND_SECONDS = 60; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type PhoneRow = { - id: string; - /** Country calling code, e.g. '+1' */ - cc: string; - /** Phone number without country code */ - number: string; - isPrimary: boolean; - isVerified: boolean; - createdAt: string | null; -}; - -type AddPhoneFormData = { - cc: string; - number: string; -}; - -type OtpFormData = { - otp: string; -}; - -export type AccountPhonesListProps = { - /** Fires after a new phone row is created and OTP sent. */ - onPhoneAdded?: (phone: PhoneRow) => void; - /** Fires after OTP verified successfully. */ - onPhoneVerified?: (phone: PhoneRow) => void; - /** Fires after primary promotion. */ - onPrimaryChanged?: (phone: PhoneRow) => void; - /** Fires after deletion. */ - onPhoneDeleted?: (phoneId: string) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and info events. */ - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - /** - * Override the add-phone + send-OTP operation. - * - * BACKEND-PENDING: `send_sms_otp` is not yet deployed. This seam is the - * PRIMARY path for add+OTP-send until the generated `useSendSmsOtpMutation` - * exists. The host creates the phone row AND sends the OTP within this fn. - * Return the created PhoneRow. - */ - onSubmitAdd?: (cc: string, number: string) => Promise; - /** - * Override the send/resend OTP operation for an existing unverified phone. - * - * BACKEND-PENDING: wraps the pending `send_sms_otp` procedure. - */ - onSubmitSendOtp?: (cc: string, number: string) => Promise; - /** - * Override the OTP verify operation. - * - * BACKEND-PENDING: wraps the pending `verify_phone_otp` procedure. - * Receives the phone number (E.164 = cc+number) and the 6-digit OTP. - * Return the updated PhoneRow on success. - */ - onSubmitVerifyOtp?: (phoneE164: string, otp: string) => Promise; - /** Override the set-primary operation. */ - onSubmitSetPrimary?: (phoneId: string) => Promise; - /** Override the delete operation. */ - onSubmitDelete?: (phoneId: string) => Promise; - messages?: AccountPhonesListMessageOverrides; - /** Default country code for the picker. Default: '+1'. */ - defaultCountry?: string; - /** Disables add/delete/primary operations. Read-only display mode. */ - readOnly?: boolean; - /** Max phone numbers allowed. Default: 5. */ - maxPhones?: number; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Field selection — mirrors PhoneRow shape -// --------------------------------------------------------------------------- - -const PHONE_FIELDS = { - id: true, - cc: true, - number: true, - isPrimary: true, - isVerified: true, - createdAt: true -} as const; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function toE164(cc: string, number: string): string { - // Strip any non-digit chars from the local number, prepend cc - const localDigits = number.replace(/\D/g, ''); - const prefix = cc.startsWith('+') ? cc : `+${cc}`; - return `${prefix}${localDigits}`; -} - -function validatePhone(number: string): boolean { - const digits = number.replace(/\D/g, ''); - return digits.length >= 7 && digits.length <= 15; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountPhonesList({ - onPhoneAdded, - onPhoneVerified, - onPrimaryChanged, - onPhoneDeleted, - onError, - onMessage, - onSubmitAdd: onSubmitAddOverride, - onSubmitSendOtp: onSubmitSendOtpOverride, - onSubmitVerifyOtp: onSubmitVerifyOtpOverride, - onSubmitSetPrimary: onSubmitSetPrimaryOverride, - onSubmitDelete: onSubmitDeleteOverride, - messages: messageOverrides, - defaultCountry = '+1', - readOnly = false, - maxPhones = 5, - className -}: AccountPhonesListProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountPhonesListMessages = { - ...defaultAccountPhonesListMessages, - ...messageOverrides, - errors: { ...defaultAccountPhonesListMessages.errors, ...messageOverrides?.errors } - }; - - // ------------------------------------------------------------------------- - // Query — list phone numbers - // ------------------------------------------------------------------------- - - const phonesQuery = usePhoneNumbersQuery({ - selection: { - fields: PHONE_FIELDS, - orderBy: ['CREATED_AT_DESC'] - } - }); - - const phones: PhoneRow[] = (phonesQuery.data?.phoneNumbers?.nodes ?? []) as PhoneRow[]; - - // ------------------------------------------------------------------------- - // Mutations - // ------------------------------------------------------------------------- - - const createPhoneMutation = useCreatePhoneNumberMutation({ - selection: { fields: PHONE_FIELDS } - }); - - const updatePhoneMutation = useUpdatePhoneNumberMutation({ - selection: { fields: PHONE_FIELDS } - }); - - const deletePhoneMutation = useDeletePhoneNumberMutation({ - selection: { fields: { id: true } } - }); - - // ------------------------------------------------------------------------- - // Dialog state — add phone (two steps: number → OTP) - // Step 0 = closed; Step 1 = enter phone number; Step 2 = enter OTP - // ------------------------------------------------------------------------- - - const [addStep, setAddStep] = useState<0 | 1 | 2>(0); - const [pendingPhone, setPendingPhone] = useState(null); - const [addError, setAddError] = useState(null); - const [addOverridePending, setAddOverridePending] = useState(false); - - const isAddPending = onSubmitAddOverride ? addOverridePending : createPhoneMutation.isPending; - - // OTP step pending (no generated hook — backend-pending CASE b) - const [otpError, setOtpError] = useState(null); - const [otpOverridePending, setOtpOverridePending] = useState(false); - - // Resend cooldown - const [resendCountdown, setResendCountdown] = useState(0); - - function startResendCountdown() { - setResendCountdown(OTP_RESEND_SECONDS); - } - - useEffect(() => { - if (resendCountdown <= 0) return; - const timeout = setTimeout(() => { - setResendCountdown((seconds) => Math.max(0, seconds - 1)); - }, 1000); - return () => clearTimeout(timeout); - }, [resendCountdown]); - - // ------------------------------------------------------------------------- - // Delete confirm state - // ------------------------------------------------------------------------- - - const [deleteTargetId, setDeleteTargetId] = useState(null); - const [deleteOverridePending, setDeleteOverridePending] = useState(false); - const isDeletePending = onSubmitDeleteOverride ? deleteOverridePending : deletePhoneMutation.isPending; - - // ------------------------------------------------------------------------- - // Per-row action pending tracking - // ------------------------------------------------------------------------- - - const [primaryPendingId, setPrimaryPendingId] = useState(null); - const [rowError, setRowError] = useState(null); - - // Inline OTP entry: which phone row is being verified - const [inlineVerifyPhoneId, setInlineVerifyPhoneId] = useState(null); - const [inlineOtpError, setInlineOtpError] = useState(null); - const [inlineOtpPending, setInlineOtpPending] = useState(false); - - // ------------------------------------------------------------------------- - // Add-phone form (step 1) - // ------------------------------------------------------------------------- - - const addForm = useForm({ - defaultValues: { cc: defaultCountry, number: '' } as AddPhoneFormData, - onSubmit: async ({ value }) => { - await handleAdd(value.cc, value.number); - } - }); - - // OTP form (step 2 in dialog) - const otpForm = useForm({ - defaultValues: { otp: '' } as OtpFormData, - onSubmit: async ({ value }) => { - await handleVerifyOtp(value.otp); - } - }); - - // Inline OTP form (for rows shown in the list) - const inlineOtpForm = useForm({ - defaultValues: { otp: '' } as OtpFormData, - onSubmit: async ({ value }) => { - await handleInlineVerifyOtp(value.otp); - } - }); - - // ------------------------------------------------------------------------- - // Handlers - // ------------------------------------------------------------------------- - - async function handleAdd(cc: string, number: string) { - setAddError(null); - - if (!validatePhone(number)) { - setAddError(merged.errors.INVALID_PHONE); - return; - } - - if (onSubmitAddOverride) setAddOverridePending(true); - - try { - let newRow: PhoneRow; - - if (onSubmitAddOverride) { - // Host override: creates row + sends OTP in one call - newRow = await onSubmitAddOverride(cc, number); - setPendingPhone(newRow); - onMessage?.({ kind: 'success', key: 'phoneAdded', message: merged.phoneAddedMessage }); - onPhoneAdded?.(newRow); - startResendCountdown(); - setAddStep(2); - } else { - // Default path: create the phone row (CASE b — no sendSmsOtp generated hook). - // The OTP-send seam requires onSubmitSendOtp override when SMS is needed. - const createData = await createPhoneMutation.mutateAsync({ cc, number }); - newRow = createData.createPhoneNumber.phoneNumber as unknown as PhoneRow; - setPendingPhone(newRow); - onMessage?.({ kind: 'success', key: 'phoneAdded', message: merged.phoneAddedMessage }); - onPhoneAdded?.(newRow); - - // If the host has provided a send-OTP override, call it now - if (onSubmitSendOtpOverride) { - try { - await onSubmitSendOtpOverride(cc, number); - onMessage?.({ kind: 'info', key: 'otpSent', message: merged.otpSentMessage }); - } catch (otpErr) { - const { code, message } = parseGraphQLError(otpErr, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - // Don't block — phone was created, OTP send failed - } - } - startResendCountdown(); - setAddStep(2); - } - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setAddError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitAddOverride) setAddOverridePending(false); - } - } - - async function handleVerifyOtp(otp: string) { - if (!pendingPhone) return; - setOtpError(null); - - if (!otp || otp.length !== 6) { - setOtpError(merged.errors.INVALID_OTP); - return; - } - - setOtpOverridePending(true); - const phoneE164 = toE164(pendingPhone.cc, pendingPhone.number); - - try { - if (onSubmitVerifyOtpOverride) { - const updatedRow = await onSubmitVerifyOtpOverride(phoneE164, otp); - onMessage?.({ kind: 'success', key: 'phoneVerified', message: merged.phoneVerifiedMessage }); - onPhoneVerified?.(updatedRow); - } else { - // CASE b — no generated verify hook. Surface PROCEDURE_NOT_FOUND. - throw Object.assign(new Error('verify_phone_otp not deployed'), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - } - // Close dialog after successful verify - setAddStep(0); - setPendingPhone(null); - otpForm.reset(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setOtpError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setOtpOverridePending(false); - } - } - - async function handleResendOtp() { - if (!pendingPhone || resendCountdown > 0) return; - setOtpError(null); - - try { - if (onSubmitSendOtpOverride) { - await onSubmitSendOtpOverride(pendingPhone.cc, pendingPhone.number); - onMessage?.({ kind: 'info', key: 'otpSent', message: merged.otpSentMessage }); - } else { - throw Object.assign(new Error('send_sms_otp not deployed'), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - } - startResendCountdown(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setOtpError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } - } - - async function handleSetPrimary(phoneId: string) { - setRowError(null); - setPrimaryPendingId(phoneId); - try { - let updatedRow: PhoneRow; - if (onSubmitSetPrimaryOverride) { - updatedRow = await onSubmitSetPrimaryOverride(phoneId); - } else { - const data = await updatePhoneMutation.mutateAsync({ - id: phoneId, - phoneNumberPatch: { isPrimary: true } - }); - updatedRow = data.updatePhoneNumber.phoneNumber as unknown as PhoneRow; - } - onMessage?.({ kind: 'success', key: 'primaryChanged', message: merged.primaryChangedMessage }); - onPrimaryChanged?.(updatedRow); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setPrimaryPendingId(null); - } - } - - async function handleDeleteConfirm() { - if (!deleteTargetId) return; - setRowError(null); - if (onSubmitDeleteOverride) setDeleteOverridePending(true); - try { - if (onSubmitDeleteOverride) { - await onSubmitDeleteOverride(deleteTargetId); - } else { - await deletePhoneMutation.mutateAsync({ id: deleteTargetId }); - } - const deletedId = deleteTargetId; - setDeleteTargetId(null); - onMessage?.({ kind: 'success', key: 'phoneDeleted', message: merged.phoneDeletedMessage }); - onPhoneDeleted?.(deletedId); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - setDeleteTargetId(null); - } finally { - if (onSubmitDeleteOverride) setDeleteOverridePending(false); - } - } - - async function handleInlineSendOtp(phone: PhoneRow) { - setInlineOtpError(null); - setInlineVerifyPhoneId(phone.id); - inlineOtpForm.reset(); - try { - if (onSubmitSendOtpOverride) { - await onSubmitSendOtpOverride(phone.cc, phone.number); - onMessage?.({ kind: 'info', key: 'otpSent', message: merged.otpSentMessage }); - } else { - throw Object.assign(new Error('send_sms_otp not deployed'), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - } - startResendCountdown(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setRowError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } - } - - async function handleInlineVerifyOtp(otp: string) { - const phone = phones.find((p) => p.id === inlineVerifyPhoneId); - if (!phone) return; - setInlineOtpError(null); - - if (!otp || otp.length !== 6) { - setInlineOtpError(merged.errors.INVALID_OTP); - return; - } - - setInlineOtpPending(true); - const phoneE164 = toE164(phone.cc, phone.number); - - try { - if (onSubmitVerifyOtpOverride) { - const updatedRow = await onSubmitVerifyOtpOverride(phoneE164, otp); - onMessage?.({ kind: 'success', key: 'phoneVerified', message: merged.phoneVerifiedMessage }); - onPhoneVerified?.(updatedRow); - } else { - throw Object.assign(new Error('verify_phone_otp not deployed'), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - } - setInlineVerifyPhoneId(null); - inlineOtpForm.reset(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setInlineOtpError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setInlineOtpPending(false); - } - } - - // ------------------------------------------------------------------------- - // Derived - // ------------------------------------------------------------------------- - - const atMax = phones.length >= maxPhones; - const deleteTarget = phones.find((p) => p.id === deleteTargetId); - - function formatPhoneDisplay(phone: PhoneRow): string { - return `${phone.cc} ${phone.number}`; - } - - // ------------------------------------------------------------------------- - // Render - // ------------------------------------------------------------------------- - - return ( - <> - - -
- {merged.title} - {merged.description} -
- {!readOnly && !atMax && ( - - )} -
- - - {rowError && ( -
- -
- )} - - {phonesQuery.isLoading ? ( -
- Loading… -
- ) : phones.length === 0 ? ( -
- No phone numbers found. -
- ) : ( -
    - {phones.map((phone, idx) => ( -
  • - {idx > 0 && } -
    -
    - {/* Phone + badges */} -
    - - {formatPhoneDisplay(phone)} - -
    - {phone.isPrimary && ( - - {merged.primaryBadge} - - )} - - {phone.isVerified ? merged.verifiedBadge : merged.unverifiedBadge} - -
    -
    - - {/* Actions */} - {!readOnly && ( -
    - {/* Verify — for unverified phones */} - {!phone.isVerified && inlineVerifyPhoneId !== phone.id && ( - - )} - - {/* Set primary — only for verified non-primary */} - {!phone.isPrimary && phone.isVerified && ( - - )} - - {/* Delete — disabled for primary */} - -
    - )} -
    - - {/* Inline OTP entry for this row */} - {!readOnly && inlineVerifyPhoneId === phone.id && ( -
    - -
    { - e.preventDefault(); - e.stopPropagation(); - inlineOtpForm.handleSubmit(); - }} - > -
    - { - if (!value) return 'Code is required'; - if (!/^\d{6}$/.test(value)) return 'Enter the 6-digit code'; - return undefined; - } - }} - > - {(field) => ( - - )} - -
    - - {merged.otpSubmit} - -
    - -
    - - -
    -
    - )} -
    -
  • - ))} -
- )} -
-
- - {/* --------------------------------------------------------------- - Add phone dialog — step 1: enter phone number - — step 2: enter OTP - --------------------------------------------------------------- */} - 0} - onOpenChange={(isOpen) => { - if (!isOpen) { - setAddStep(0); - setPendingPhone(null); - setAddError(null); - setOtpError(null); - addForm.reset(); - otpForm.reset(); - } - }} - > - - - {merged.addPhoneDialogTitle} - - - - {addStep === 1 && ( -
- - -
{ - e.preventDefault(); - e.stopPropagation(); - addForm.handleSubmit(); - }} - > - {/* Country code selector */} -
- - - {(field) => ( - - )} - -
- - {/* Phone number input */} - { - if (!value) return 'Phone number is required'; - if (!validatePhone(value)) return merged.errors.INVALID_PHONE; - return undefined; - } - }} - > - {(field) => ( - - )} - - - - - - {merged.addPhoneSubmit} - - -
-
- )} - - {addStep === 2 && ( -
- {pendingPhone && ( -

- {merged.phoneAddedMessage} -

- )} - - - -
{ - e.preventDefault(); - e.stopPropagation(); - otpForm.handleSubmit(); - }} - > - { - if (!value) return 'Code is required'; - if (!/^\d{6}$/.test(value)) return 'Enter the 6-digit code'; - return undefined; - } - }} - > - {(field) => ( - - )} - - - -
-
- - - {merged.otpSubmit} - -
-
- -
-
-
-
-
- )} -
-
- - {/* --------------------------------------------------------------- - Delete confirm dialog - --------------------------------------------------------------- */} - { - if (!isOpen) setDeleteTargetId(null); - }} - > - - - {merged.deleteConfirmTitle} - {merged.deleteConfirmDescription} - - - - - - {merged.deleteConfirmButton} - - - - - - {/* Invisible element to expose deleteTarget for tests */} - {deleteTarget && ( - - {formatPhoneDisplay(deleteTarget)} - - )} - - ); -} diff --git a/apps/blocks/src/blocks/auth/account-phones-list/auth-account-phones-list.requires.json b/apps/blocks/src/blocks/auth/account-phones-list/auth-account-phones-list.requires.json deleted file mode 100644 index 23ee8ea..0000000 --- a/apps/blocks/src/blocks/auth/account-phones-list/auth-account-phones-list.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["createPhoneNumber", "updatePhoneNumber", "deletePhoneNumber", "sendSmsOtp", "verifyPhoneOtp"], - "queries": ["phoneNumbers"], - "models": ["phoneNumber"] -} diff --git a/apps/blocks/src/blocks/auth/account-phones-list/messages.ts b/apps/blocks/src/blocks/auth/account-phones-list/messages.ts deleted file mode 100644 index a5ae673..0000000 --- a/apps/blocks/src/blocks/auth/account-phones-list/messages.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * account-phones-list — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`. - * - * PROCEDURE_NOT_FOUND is included because SMS OTP procedures - * (`send_sms_otp` / `verify_phone_otp`) are backend-pending. When the block - * fires against a deployment that has not yet deployed those procedures, - * PostGraphile returns code PROCEDURE_NOT_FOUND — this key ensures a clear - * user-facing message rather than UNKNOWN_ERROR. - */ - -export type AccountPhonesListMessages = { - title: string; - description: string; - addPhoneButton: string; - addPhoneDialogTitle: string; - countryCodeLabel: string; - phoneLabel: string; - phonePlaceholder: string; - addPhoneSubmit: string; - addPhoneSubmitting: string; - primaryBadge: string; - verifiedBadge: string; - unverifiedBadge: string; - verifyButton: string; - resendButton: string; - setPrimaryButton: string; - deleteButton: string; - deleteConfirmTitle: string; - deleteConfirmDescription: string; - deleteConfirmButton: string; - deleteCancelButton: string; - otpLabel: string; - otpPlaceholder: string; - otpSubmit: string; - otpSubmitting: string; - otpSentMessage: string; - otpResendCooldown: string; - phoneAddedMessage: string; - phoneVerifiedMessage: string; - primaryChangedMessage: string; - phoneDeletedMessage: string; - cannotDeletePrimary: string; - errors: { - INVALID_PHONE: string; - INVALID_OTP: string; - RATE_LIMITED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type — top-level keys are individually optional; - * `errors` is itself partial so a host can override a single code. - */ -export type AccountPhonesListMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAccountPhonesListMessages: AccountPhonesListMessages = { - title: 'Phone numbers', - description: - 'Manage your phone numbers. Your primary number is used for SMS sign-in and notifications.', - addPhoneButton: 'Add phone number', - addPhoneDialogTitle: 'Add phone number', - countryCodeLabel: 'Country', - phoneLabel: 'Phone number', - phonePlaceholder: '(555) 000-0000', - addPhoneSubmit: 'Send verification code', - addPhoneSubmitting: 'Sending…', - primaryBadge: 'Primary', - verifiedBadge: 'Verified', - unverifiedBadge: 'Unverified', - verifyButton: 'Verify', - resendButton: 'Resend code', - setPrimaryButton: 'Set as primary', - deleteButton: 'Remove', - deleteConfirmTitle: 'Remove phone number?', - deleteConfirmDescription: 'This phone number will be removed from your account.', - deleteConfirmButton: 'Remove', - deleteCancelButton: 'Cancel', - otpLabel: 'Verification code', - otpPlaceholder: '000000', - otpSubmit: 'Verify', - otpSubmitting: 'Verifying…', - otpSentMessage: 'Verification code sent.', - otpResendCooldown: 'Resend in {{seconds}}s', - phoneAddedMessage: 'Phone number added. Enter the code we sent to verify it.', - phoneVerifiedMessage: 'Phone number verified.', - primaryChangedMessage: 'Primary phone number updated.', - phoneDeletedMessage: 'Phone number removed.', - cannotDeletePrimary: 'You cannot remove your primary phone number.', - errors: { - INVALID_PHONE: 'Please enter a valid phone number.', - INVALID_OTP: 'Incorrect code. Please try again.', - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - PROCEDURE_NOT_FOUND: - 'SMS verification is not yet available. Please contact support or try again later.', - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-profile-card/account-profile-card.tsx b/apps/blocks/src/blocks/auth/account-profile-card/account-profile-card.tsx deleted file mode 100644 index 97b3118..0000000 --- a/apps/blocks/src/blocks/auth/account-profile-card/account-profile-card.tsx +++ /dev/null @@ -1,503 +0,0 @@ -'use client'; - -/** - * account-profile-card (registry: auth-account-profile-card) - * - * Allows the signed-in user to update their display_name and profile_picture. - * Renders first/last or display name fields for 'person' users and an - * organization name field for 'organization' users. Profile picture changes - * use an optimistic preview before the presigned-URL upload completes. - * - * DATA PATH: - * • Read — `useCurrentUserQuery` from `@/generated/auth` (when `user` prop - * is not supplied by the consumer). - * • Write — `useUpdateUserMutation` from `@/generated/auth`. - * Hook takes flat args `{ id, userPatch }` and returns - * `{ updateUser: { user } }`. - * - * There is NO fetch, NO GraphQL document string, NO configure()/getClient(), - * NO QueryClientProvider in this file. The host mounts blocks-runtime once. - */ - -import { useRef, useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { useCurrentUserQuery, useUpdateUserMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; -import { UserAvatar } from '@/blocks/user/user-avatar/user-avatar'; - -import { - defaultAccountProfileCardMessages, - type AccountProfileCardMessageOverrides, - type AccountProfileCardMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -/** - * Opaque image descriptor stored as jsonb in `users.profile_picture`. - * When the field is a URL string, consumers may read `profilePicture.url` - * or cast to string depending on their upload implementation. - */ -export type ImageJsonb = Record; - -export type UpdateProfileInput = { - /** The user's id — required so the mutation knows which row to update. */ - id: string; - displayName?: string; - /** Set to `null` to remove the current picture. When a new file was selected, - * `profilePictureUpload` carries the raw File; `profilePicture` is omitted. */ - profilePicture?: ImageJsonb | null; - /** Raw File object when the user selected a new picture but no presigned-PUT - * has been performed yet. Present only when `onSubmit` override is used and - * the consumer is responsible for the upload step. */ - profilePictureUpload?: File | null; -}; - -export type UpdateProfileResult = { - user: { - id: string; - type: 'person' | 'organization'; - displayName: string | null; - profilePicture: ImageJsonb | null; - }; -}; - -/** The user shape expected / returned by this block. */ -export type AccountProfileUser = { - id: string; - /** Normalised from the wire Int! (1 → 'person', 2 → 'organization'). */ - type: 'person' | 'organization'; - displayName?: string | null; - /** Raw value from `users.profile_picture` (jsonb image domain or null). */ - profilePicture?: ImageJsonb | null; -}; - -export type AccountProfileCardProps = { - /** Current user. When omitted the block calls `useCurrentUserQuery`. */ - user?: AccountProfileUser; - /** Pre-populate form fields. Falls back to `user` when not set. */ - defaultValues?: { - displayName?: string; - profilePicture?: ImageJsonb | null; - }; - /** Max file size in bytes for profile picture upload. Default: 5_000_000. */ - maxFileSize?: number; - /** Accepted MIME types for profile picture. Default: image/jpeg, image/png, image/webp. */ - acceptedImageTypes?: string[]; - messages?: AccountProfileCardMessageOverrides; - /** Replace the default `useUpdateUserMutation` call. */ - onSubmit?: (input: UpdateProfileInput) => Promise; - /** Fires after a successful save. Always fires. */ - onSuccess?: (result: UpdateProfileResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for all events. Always fires. */ - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Normalize the wire Int! type value to the block's discriminator. */ -function normalizeUserType(raw: number | null | undefined): 'person' | 'organization' { - if (raw === 2) return 'organization'; - return 'person'; -} - -/** - * Best-effort extraction of a display URL from the opaque image jsonb. - * The domain stores objects like `{ url, key, width, height, mimeType }`. - * Falls back to `null` when it cannot resolve a string URL. - */ -function resolveAvatarUrl(pic: ImageJsonb | null | undefined): string | null { - if (!pic) return null; - if (typeof pic === 'string') return pic; - if (typeof (pic as { url?: unknown }).url === 'string') return (pic as { url: string }).url; - return null; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountProfileCard({ - user: userProp, - defaultValues, - maxFileSize = 5_000_000, - acceptedImageTypes = ['image/jpeg', 'image/png', 'image/webp'], - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: AccountProfileCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountProfileCardMessages = { - ...defaultAccountProfileCardMessages, - ...messageOverrides, - errors: { - ...defaultAccountProfileCardMessages.errors, - ...messageOverrides?.errors - } - }; - - // --------------------------------------------------------------------------- - // Data — read current user when the consumer has not supplied a user prop. - // The query is skipped (`enabled: false`) when the consumer supplies `user`. - // --------------------------------------------------------------------------- - const { data: currentUserData } = useCurrentUserQuery({ - selection: { - fields: { - id: true, - type: true, - displayName: true, - profilePicture: true - } - }, - enabled: !userProp - }); - - const rawUser = userProp ?? currentUserData?.currentUser ?? null; - - const resolvedUser: AccountProfileUser | null = rawUser - ? { - id: rawUser.id, - type: userProp - ? userProp.type - : normalizeUserType((rawUser as { type?: number | null }).type as number | null), - displayName: (rawUser as { displayName?: string | null }).displayName, - profilePicture: (rawUser as { profilePicture?: ImageJsonb | null }).profilePicture - } - : null; - - // --------------------------------------------------------------------------- - // Data — update mutation - // --------------------------------------------------------------------------- - const defaultMutation = useUpdateUserMutation({ - selection: { - fields: { - id: true, - type: true, - displayName: true, - profilePicture: true - } - } - }); - - // Hybrid pending: generated hook tracks its own; override path does not. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - // --------------------------------------------------------------------------- - // File / upload state - // --------------------------------------------------------------------------- - const fileInputRef = useRef(null); - const [uploadError, setUploadError] = useState(null); - const [isUploading, setIsUploading] = useState(false); - /** - * Holds the optimistic preview + the raw File so `handleSave` can pass it - * via `userPatch.profilePictureUpload` — the ORM-generated field for - * multipart/binary uploads. `null` means no pending change; `'remove'` means - * clear the current picture. - * - * We intentionally do NOT store `File` inside an `ImageJsonb` (that would be - * non-serializable). The `profilePictureUpload` field on `UserPatch` is the - * correct contract point for raw File uploads. - */ - const [pendingPicture, setPendingPicture] = useState< - { preview: string; file: File } | null | 'remove' - >(null); - - const [formError, setFormError] = useState(null); - - // --------------------------------------------------------------------------- - // Form - // --------------------------------------------------------------------------- - const isOrg = resolvedUser?.type === 'organization'; - const initialDisplayName = - defaultValues?.displayName ?? - resolvedUser?.displayName ?? - ''; - - const form = useForm({ - defaultValues: { displayName: initialDisplayName as string }, - onSubmit: async ({ value }) => { - await handleSave(value.displayName); - } - }); - - // --------------------------------------------------------------------------- - // Handlers - // --------------------------------------------------------------------------- - - function handleFileSelect(e: React.ChangeEvent) { - setUploadError(null); - const file = e.target.files?.[0]; - if (!file) return; - - if (file.size > maxFileSize) { - setUploadError(merged.fileTooLarge); - return; - } - if (!acceptedImageTypes.includes(file.type)) { - setUploadError(merged.fileTypeNotAccepted); - return; - } - - // Optimistic preview — the File is stored on pendingPicture.file so - // handleSave can pass it via `userPatch.profilePictureUpload`, the - // ORM-generated field that accepts a raw File for binary uploads. - // This avoids placing a non-serializable File inside an ImageJsonb, which - // would break GraphQL transport (see B1 fix). - // `isUploading` is set true during the actual mutation in handleSave; here - // we just stage the file and surface an info event for the consumer. - const previewUrl = URL.createObjectURL(file); - onMessage?.({ kind: 'info', key: 'uploading', message: merged.uploadingMessage }); - setPendingPicture({ preview: previewUrl, file }); - } - - function handleRemovePhoto() { - setPendingPicture('remove'); - setUploadError(null); - } - - async function handleSave(displayName: string) { - if (!resolvedUser) return; - setFormError(null); - // Show the avatar upload overlay while the mutation is in-flight with a file. - if (pendingPicture !== null && pendingPicture !== 'remove') { - setIsUploading(true); - } - - // Build the input shape for the onSubmit override (consumer-facing API). - // When the user removed the photo: profilePicture = null. - // When the user selected a new file: profilePictureUpload = File (the - // consumer is responsible for the upload step in the override path). - const input: UpdateProfileInput = { - id: resolvedUser.id, - displayName: displayName || undefined, - ...(pendingPicture === 'remove' - ? { profilePicture: null } - : pendingPicture !== null - ? { profilePictureUpload: pendingPicture.file } - : {}) - }; - - if (onSubmitOverride) setOverridePending(true); - try { - let result: UpdateProfileResult; - - if (onSubmitOverride) { - result = await onSubmitOverride(input); - } else { - // Build the userPatch for the ORM-generated mutation. - // `profilePictureUpload` is the correct field for a raw File object — - // the ORM/transport layer handles serialization. `profilePicture: null` - // clears the existing image. We never put a File inside `profilePicture` - // (which expects `ConstructiveInternalTypeImage = unknown` — a resolved - // jsonb object, not a DOM File). - const userPatch: { - displayName: string | null; - profilePicture?: null; - profilePictureUpload?: File; - } = { displayName: input.displayName ?? null }; - - if (pendingPicture === 'remove') { - userPatch.profilePicture = null; - } else if (pendingPicture !== null) { - userPatch.profilePictureUpload = pendingPicture.file; - } - - const data = await defaultMutation.mutateAsync({ - id: input.id, - userPatch - }); - const updatedUser = data.updateUser?.user; - result = { - user: { - id: updatedUser?.id ?? resolvedUser.id, - type: normalizeUserType((updatedUser as { type?: number | null } | undefined)?.type ?? null), - displayName: (updatedUser as { displayName?: string | null } | undefined)?.displayName ?? null, - profilePicture: - (updatedUser as { profilePicture?: ImageJsonb | null } | undefined)?.profilePicture ?? null - } - }; - } - - setPendingPicture(null); - setIsUploading(false); - onMessage?.({ kind: 'success', key: 'profileUpdated', message: merged.successToast }); - onSuccess?.(result); - } catch (err) { - setIsUploading(false); - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setFormError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - // --------------------------------------------------------------------------- - // Derived avatar state - // --------------------------------------------------------------------------- - const avatarUser = { - id: resolvedUser?.id ?? 'anon', - type: resolvedUser?.type ?? 'person', - displayName: (form.state.values.displayName || resolvedUser?.displayName || '') as string, - username: null, - profilePicture: - pendingPicture === 'remove' - ? null - : pendingPicture !== null - ? pendingPicture.preview - : resolveAvatarUrl(resolvedUser?.profilePicture) - }; - - const isAnyPending = isPending || isUploading; - - // --------------------------------------------------------------------------- - // Render - // --------------------------------------------------------------------------- - return ( - - - {merged.title} - {merged.description} - - - - {/* Profile picture */} - {/* `relative` makes this row the containing block for the sr-only file - input below; otherwise the absolutely-positioned (but visually - hidden) input keeps its in-flow static position at the row's right - edge and is measured against the page body, widening the document - and causing horizontal scroll at narrow viewports. */} -
-
- - {isUploading && ( -
- {merged.uploadingMessage} -
- )} -
- -
-

{merged.profilePictureHint}

-
- - {(resolvedUser?.profilePicture || pendingPicture) && pendingPicture !== 'remove' && ( - - )} -
-
- - {/* Visually-hidden file input. - `sr-only` makes the input `position: absolute` but does NOT set - `left`/`top`, so it would otherwise retain its static-position X - (far right, after the avatar + buttons). Pinning it to `left-0 - top-0` inside the `relative` row keeps its box at the card corner - so it can never extend the page's horizontal scroll bounds. */} - -
- - {/* Upload / form errors */} - - - {/* Display-name / org-name form */} -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - { - if (!value?.trim()) { - return isOrg ? 'Organization name is required' : 'Display name is required'; - } - return undefined; - } - }} - > - {(field) => ( - - )} - - - - {merged.saveButton} - -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-profile-card/auth-account-profile-card.requires.json b/apps/blocks/src/blocks/auth/account-profile-card/auth-account-profile-card.requires.json deleted file mode 100644 index 797a0f9..0000000 --- a/apps/blocks/src/blocks/auth/account-profile-card/auth-account-profile-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["updateUser"], - "queries": ["currentUser"], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-profile-card/messages.ts b/apps/blocks/src/blocks/auth/account-profile-card/messages.ts deleted file mode 100644 index ae10d3f..0000000 --- a/apps/blocks/src/blocks/auth/account-profile-card/messages.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * account-profile-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` - * as `customMessages`, so a host localises any code by overriding a single key. - */ - -export type AccountProfileCardMessages = { - title: string; - description: string; - displayNameLabel: string; - displayNamePlaceholder: string; - orgNameLabel: string; - orgNamePlaceholder: string; - profilePictureLabel: string; - profilePictureHint: string; - changePhotoButton: string; - removePhotoButton: string; - saveButton: string; - savingButton: string; - successToast: string; - uploadingMessage: string; - fileTooLarge: string; - fileTypeNotAccepted: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: top-level keys are each optional, AND the - * `errors` sub-map is itself partial so hosts can override one code at a time. - */ -export type AccountProfileCardMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAccountProfileCardMessages: AccountProfileCardMessages = { - title: 'Profile', - description: 'Update your display name and profile picture.', - displayNameLabel: 'Display name', - displayNamePlaceholder: 'Your name', - orgNameLabel: 'Organization name', - orgNamePlaceholder: 'Your organization name', - profilePictureLabel: 'Profile picture', - profilePictureHint: 'JPG, PNG or WebP. Max 5 MB.', - changePhotoButton: 'Change photo', - removePhotoButton: 'Remove photo', - saveButton: 'Save changes', - savingButton: 'Saving…', - successToast: 'Profile updated.', - uploadingMessage: 'Uploading photo…', - fileTooLarge: 'File exceeds maximum allowed size.', - fileTypeNotAccepted: 'File type not accepted.', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-security-card/account-security-card.tsx b/apps/blocks/src/blocks/auth/account-security-card/account-security-card.tsx deleted file mode 100644 index 07396a5..0000000 --- a/apps/blocks/src/blocks/auth/account-security-card/account-security-card.tsx +++ /dev/null @@ -1,322 +0,0 @@ -'use client'; - -/** - * account-security-card (registry: auth-account-security-card) - * - * At-a-glance security posture summary: password status, TOTP MFA status, and - * passkey count. Display-only — all actions are delegated via callbacks so the - * consumer decides how to navigate to the relevant management blocks. - * - * Data path (sdk-binding-contract.md §5, verified): - * • `useWebauthnCredentialsQuery` from `@/generated/auth` — lists passkeys to - * derive the count. `WebauthnCredentialsConnection` is confirmed in the - * generated SDK (useWebauthnCredentialsQuery.ts). - * • `adapter` prop: fully replaces the network call when provided (static - * value or async function), enabling non-Constructive backends, testing, - * and Storybook without a real QueryClient. - * - * SDK gap note: `totpEnabled` and `hasPassword` are NOT fields on the generated - * `User` type (`UserSelect` in orm/input-types.ts). They only appear on - * `SignInRecord` / session payloads. Until the backend exposes these on a public - * `currentUser()` query, they are read from the `currentUser` query's - * available fields. `totpEnabled` defaults to `false` (safe: prompts user to - * enroll rather than silently hiding the CTA). `hasPassword` defaults to `true` - * (safe: shows "Change password" rather than hiding the action). - * - * No mutations — this block is read-only. No form, no onSubmit override seam. - */ - -import { useEffect, useMemo, useState } from 'react'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Separator } from '@constructive-io/ui/separator'; -import { Skeleton } from '@constructive-io/ui/skeleton'; - -import { cn } from '@/lib/utils'; -import { useWebauthnCredentialsQuery } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; - -import { - defaultAccountSecurityCardMessages, - interpolate, - type AccountSecurityCardMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type AccountSecurityCardMessageOverrides = Partial< - Omit -> & { - errors?: Partial; -}; - -/** Derived security posture values surfaced by the block. */ -export type SecurityStatus = { - hasPassword: boolean; - totpEnabled: boolean; - passkeyCount: number; -}; - -/** - * Query adapter — replaces `useWebauthnCredentialsQuery` when provided. - * Accepts a static result object or an async function that resolves to one. - * Enables non-Constructive backends, unit tests, and Storybook without a - * real QueryClient (sdk-binding-contract.md §4). - */ -export type AccountSecurityCardAdapter = - | { webauthnCredentials: { totalCount: number } } - | (() => Promise<{ webauthnCredentials: { totalCount: number } }>); - -export type AccountSecurityCardProps = { - /** Called when user clicks the change-password / set-password CTA. */ - onChangePassword?: () => void; - /** - * Called when user clicks the MFA enable/disable CTA. - * If undefined the MFA row CTA is hidden (backend-pending; consumer opts in - * only when TOTP enrollment is available). - */ - onManageMfa?: () => void; - /** Called when user clicks the manage passkeys CTA. */ - onManagePasskeys?: () => void; - /** - * Query adapter. When provided, fully replaces `useWebauthnCredentialsQuery`. - * Pass a static `{ webauthnCredentials: { totalCount: number } }` or an async - * factory function for dynamic data. - */ - adapter?: AccountSecurityCardAdapter; - messages?: AccountSecurityCardMessageOverrides; - /** Fires on query errors. Receives the normalised `{ message, code }` shape. */ - onError?: (err: { message: string; code: string }) => void; - /** Fires for any notification event. Always fires. */ - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountSecurityCard({ - onChangePassword, - onManageMfa, - onManagePasskeys, - adapter, - messages: messageOverrides, - onError, - onMessage, - className -}: AccountSecurityCardProps) { - // Deep merge — top-level + errors nested separately. - // Memoized so the useEffect dependency array stays stable across renders. - const merged: AccountSecurityCardMessages = useMemo( - () => ({ - ...defaultAccountSecurityCardMessages, - ...messageOverrides, - errors: { - ...defaultAccountSecurityCardMessages.errors, - ...messageOverrides?.errors - } - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(messageOverrides)] - ); - - // Local error string for inline display. - const [error, setError] = useState(null); - - // ------------------------------------------------------------------ - // Data: passkey count — adapter path - // When `adapter` is provided, resolve it (static object or async fn) - // and skip the generated hook entirely (sdk-binding-contract.md §4). - // Static objects are applied immediately; async functions load once. - // ------------------------------------------------------------------ - const [adapterData, setAdapterData] = useState<{ webauthnCredentials: { totalCount: number } } | null>( - () => adapter && typeof adapter !== 'function' ? adapter : null - ); - const [adapterLoading, setAdapterLoading] = useState(false); - - useEffect(() => { - if (!adapter || typeof adapter !== 'function') return; - setAdapterLoading(true); - adapter() - .then((result) => { - setAdapterData(result); - setAdapterLoading(false); - }) - .catch(() => setAdapterLoading(false)); - // Run once per adapter identity change only. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [adapter]); - - // ------------------------------------------------------------------ - // Data: passkey count — generated hook path (skipped when adapter set) - // useWebauthnCredentialsQuery is confirmed in the generated auth SDK. - // The `totalCount` field on the ConnectionResult gives us the count - // without fetching full credential records (first: 0 is sufficient). - // React Query v5 removed onError from useQuery options; errors are - // handled via the returned `isError` / `error` fields instead. - // ------------------------------------------------------------------ - const credentialsQuery = useWebauthnCredentialsQuery({ - selection: { fields: { id: true }, first: 0 }, - enabled: !adapter - }); - - // Surface query errors via callbacks + inline alert. - useEffect(() => { - if (!credentialsQuery.isError || !credentialsQuery.error) return; - const err = credentialsQuery.error; - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key: `accountSecurity.${key}`, message }); - onError?.({ message, code: key }); - }, [credentialsQuery.isError, credentialsQuery.error, merged.errors, onError, onMessage]); - - const isLoading = adapter ? adapterLoading : credentialsQuery.isLoading; - const passkeyCount = adapter - ? (adapterData?.webauthnCredentials?.totalCount ?? 0) - : (credentialsQuery.data?.webauthnCredentials?.totalCount ?? 0); - - // ------------------------------------------------------------------ - // Security status - // - // SDK gap: `totpEnabled` and `hasPassword` are not on the `User` type - // in the generated auth SDK (they appear only on session/sign-in - // payloads). Until the backend exposes them on `currentUser()`, the - // block defaults both to safe values so display is always meaningful: - // hasPassword = true → shows "Change password" (not "Set password") - // totpEnabled = false → shows "Enable" (not "Manage") - // ------------------------------------------------------------------ - const hasPassword = true; - const totpEnabled = false; - - // ------------------------------------------------------------------ - // Skeleton loader - // ------------------------------------------------------------------ - if (isLoading) { - return ( - - - - - - - {[0, 1, 2].map((i) => ( -
-
- - -
- -
- ))} -
-
- ); - } - - // ------------------------------------------------------------------ - // Render - // ------------------------------------------------------------------ - const passkeysStatus = - passkeyCount > 0 - ? interpolate(merged.passkeysCountStatus, { count: passkeyCount }) - : merged.passkeysNoneStatus; - - return ( - - - {merged.title} - {merged.description} - - - - - - {/* Password row */} -
-
-
-
{merged.passwordLabel}
-
- - {hasPassword ? merged.passwordSetStatus : merged.passwordNotSetStatus} - -
-
- {onChangePassword && ( - - )} -
-
- - - - {/* MFA row */} -
-
-
-
{merged.mfaLabel}
-
- - {totpEnabled ? merged.mfaEnabledStatus : merged.mfaDisabledStatus} - -
-
- {onManageMfa && ( - - )} -
-
- - - - {/* Passkeys row */} -
-
-
-
{merged.passkeysLabel}
-
{passkeysStatus}
-
- {onManagePasskeys && ( - - )} -
-
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-security-card/auth-account-security-card.requires.json b/apps/blocks/src/blocks/auth/account-security-card/auth-account-security-card.requires.json deleted file mode 100644 index 6a76973..0000000 --- a/apps/blocks/src/blocks/auth/account-security-card/auth-account-security-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": [], - "queries": ["webauthnCredentials"], - "models": ["webauthnCredential"] -} diff --git a/apps/blocks/src/blocks/auth/account-security-card/messages.ts b/apps/blocks/src/blocks/auth/account-security-card/messages.ts deleted file mode 100644 index 8e09a90..0000000 --- a/apps/blocks/src/blocks/auth/account-security-card/messages.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * account-security-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * `passkeysCountStatus` uses `{{count}}` interpolation. Use - * `interpolate(messages.passkeysCountStatus, { count })` in the component - * (per i18n-contract §9 and the block spec). - */ - -export type AccountSecurityCardMessages = { - title: string; - description: string; - passwordLabel: string; - passwordSetStatus: string; - passwordNotSetStatus: string; - changePasswordButton: string; - setPasswordButton: string; - mfaLabel: string; - mfaEnabledStatus: string; - mfaDisabledStatus: string; - manageMfaButton: string; - enableMfaButton: string; - passkeysLabel: string; - /** Single interpolated string with {{count}} placeholder. */ - passkeysCountStatus: string; - passkeysNoneStatus: string; - managePasskeysButton: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -export const defaultAccountSecurityCardMessages: AccountSecurityCardMessages = { - title: 'Security', - description: 'Manage your password, two-factor authentication, and passkeys.', - passwordLabel: 'Password', - passwordSetStatus: 'Set', - passwordNotSetStatus: 'Not set', - changePasswordButton: 'Change password', - setPasswordButton: 'Set password', - mfaLabel: 'Two-factor authentication', - mfaEnabledStatus: 'Enabled', - mfaDisabledStatus: 'Disabled', - manageMfaButton: 'Manage', - enableMfaButton: 'Enable', - passkeysLabel: 'Passkeys', - passkeysCountStatus: '{{count}} passkey(s) registered', - passkeysNoneStatus: 'No passkeys registered', - managePasskeysButton: 'Manage passkeys', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; - -/** - * Simple `{{key}}` mustache interpolation. - * Replaces all `{{key}}` occurrences in `template` with the corresponding value - * from `vars`. Values are coerced to string. - */ -export function interpolate(template: string, vars: Record): string { - return Object.entries(vars).reduce( - (acc, [key, value]) => acc.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value)), - template - ); -} diff --git a/apps/blocks/src/blocks/auth/account-sessions-list/account-sessions-list.tsx b/apps/blocks/src/blocks/auth/account-sessions-list/account-sessions-list.tsx deleted file mode 100644 index 27de78e..0000000 --- a/apps/blocks/src/blocks/auth/account-sessions-list/account-sessions-list.tsx +++ /dev/null @@ -1,455 +0,0 @@ -'use client'; - -/** - * account-sessions-list (registry: auth-account-sessions-list) - * - * Displays the signed-in user's active sessions and provides revoke actions. - * Because `user_sessions` is a `constructive_auth_private` view with NO public - * API, there is no generated list hook — the list is supplied by the host via the - * `sessions` adapter prop (default: empty array, renders the empty state). - * Only the `revokeSession` mutation is bindable today via `useRevokeSessionMutation` - * from `@/generated/auth`. - * - * SDK gap: no `useUserSessionsQuery` hook exists (sdk-binding-contract.md §10). - * When a `UserSessionsConnection` ships, add `useUserSessionsQuery` from - * `@/generated/auth` and update `requires.json` with `"queries":["userSessions"]`. - * - * Step-up tiers (step-up-contract.md §6): - * - Single revoke → `tier: 'medium'` (password) - * - Revoke all others → `tier: 'high'` (MFA if enrolled, else password) - * - * Binding doctrine (sdk-binding-contract.md §3, §5): - * • Generated hook imported from `@/generated/auth` — never `@constructive-io/data`. - * • No `configure()`/`getClient()`, no `QueryClientProvider`. Host mounts blocks-runtime. - * • `onRevokeSubmit` override seam replaces the default hook call. - */ - -import { useState } from 'react'; - -import { Badge } from '@constructive-io/ui/badge'; -import { Button } from '@constructive-io/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@constructive-io/ui/dialog'; -import { Separator } from '@constructive-io/ui/separator'; - -import { cn } from '@/lib/utils'; -import { useRevokeSessionMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; - -import { - defaultAccountSessionsListMessages, - type AccountSessionsListMessages, - type AccountSessionsListMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export type ParsedDevice = { - browser: string | null; - os: string | null; - deviceType: 'desktop' | 'mobile' | 'tablet' | 'unknown'; -}; - -export type SessionRow = { - id: string; - isCurrent: boolean; - authMethod: 'password' | 'identity' | 'magic_link' | 'email_otp' | 'sms_otp' | 'anonymous' | string; - userAgent: string | null; - parsedDevice: ParsedDevice | null; - ip: string | null; - origin: string | null; - lastUsedAt: string | null; - createdAt: string; - expiresAt: string; -}; - -/** Variables passed to the `onRevokeSubmit` override. */ -export type RevokeSessionVars = { - sessionId: string; -}; - -/** Result shape; mirrors the `revokeSession` payload fields this block selects. */ -export type RevokeSessionResult = { - result: boolean | null; -}; - -export type AccountSessionsListProps = { - /** - * The list of sessions to display. There is NO generated list hook for - * `user_sessions` (it is in `constructive_auth_private`, no public API → - * no `*Connection` type). The host must supply rows; the default is `[]` - * which renders the empty state. - * - * sdk-binding-contract.md §10 documents this gap. - */ - sessions?: SessionRow[]; - /** Override the `useRevokeSessionMutation` call for a single revoke. */ - onRevokeSubmit?: (vars: RevokeSessionVars) => Promise; - /** Fires after a single session is successfully revoked. */ - onSessionRevoked?: (sessionId: string) => void; - /** Fires after all other sessions are successfully revoked. */ - onAllOtherSessionsRevoked?: () => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and step-up events. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - messages?: AccountSessionsListMessageOverrides; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -/** - * Lightweight user-agent parser. Returns basic device/browser labels without - * shipping a heavy UA-parser library. Good enough for "Chrome on macOS" level. - */ -function parseUserAgent(ua: string | null): ParsedDevice { - if (!ua) return { browser: null, os: null, deviceType: 'unknown' }; - - // Device type - let deviceType: ParsedDevice['deviceType'] = 'desktop'; - if (/mobile/i.test(ua)) deviceType = 'mobile'; - else if (/tablet|ipad/i.test(ua)) deviceType = 'tablet'; - - // Browser - let browser: string | null = null; - if (/edg\//i.test(ua)) browser = 'Edge'; - else if (/chrome\//i.test(ua) && !/chromium/i.test(ua)) browser = 'Chrome'; - else if (/firefox\//i.test(ua)) browser = 'Firefox'; - else if (/safari\//i.test(ua) && !/chrome/i.test(ua)) browser = 'Safari'; - else if (/opr\//i.test(ua)) browser = 'Opera'; - - // OS - let os: string | null = null; - if (/windows nt/i.test(ua)) os = 'Windows'; - else if (/macintosh|mac os x/i.test(ua)) os = 'macOS'; - else if (/iphone|ipad/i.test(ua)) os = 'iOS'; - else if (/android/i.test(ua)) os = 'Android'; - else if (/linux/i.test(ua)) os = 'Linux'; - - return { browser, os, deviceType }; -} - -/** Relative-time label for `lastUsedAt`. Uses Intl.RelativeTimeFormat. */ -function formatRelativeTime(isoDate: string | null): string | null { - if (!isoDate) return null; - const diffMs = Date.now() - new Date(isoDate).getTime(); - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); - const diffSecs = Math.round(diffMs / 1000); - if (Math.abs(diffSecs) < 60) return rtf.format(-diffSecs, 'second'); - const diffMins = Math.round(diffSecs / 60); - if (Math.abs(diffMins) < 60) return rtf.format(-diffMins, 'minute'); - const diffHours = Math.round(diffMins / 60); - if (Math.abs(diffHours) < 24) return rtf.format(-diffHours, 'hour'); - const diffDays = Math.round(diffHours / 24); - return rtf.format(-diffDays, 'day'); -} - -/** Device icon text by device type (placeholder — hosts can style with SVG icons). */ -function deviceLabel(device: ParsedDevice | null): string { - if (!device) return '?'; - if (device.deviceType === 'mobile') return '📱'; - if (device.deviceType === 'tablet') return '📱'; - return '💻'; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function AccountSessionsList({ - sessions = [], - onRevokeSubmit: onRevokeSubmitOverride, - onSessionRevoked, - onAllOtherSessionsRevoked, - onError, - onMessage, - messages: messageOverrides, - className -}: AccountSessionsListProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: AccountSessionsListMessages = { - ...defaultAccountSessionsListMessages, - ...messageOverrides, - errors: { ...defaultAccountSessionsListMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook — `revokeSession` takes `{ input: { sessionId } }`. - // Payload: `{ revokeSession: { result: boolean | null } | null }`. - const defaultRevokeMutation = useRevokeSessionMutation({ - selection: { - fields: { result: true } - } - }); - - // Hybrid pending: override path tracks its own pending state. - const [overridePending, setOverridePending] = useState(false); - const isRevokePending = onRevokeSubmitOverride ? overridePending : defaultRevokeMutation.isPending; - - // Dialog state - const [confirmSession, setConfirmSession] = useState(null); - const [confirmRevokeAll, setConfirmRevokeAll] = useState(false); - const [error, setError] = useState(null); - - // step-up hook - const stepUp = useStepUp(); - - async function runRevoke(sessionId: string): Promise { - if (onRevokeSubmitOverride) return onRevokeSubmitOverride({ sessionId }); - const data = await defaultRevokeMutation.mutateAsync({ input: { sessionId } }); - if (!data.revokeSession) return null; - return { result: data.revokeSession.result ?? null }; - } - - async function handleRevokeSingle(session: SessionRow) { - setError(null); - if (onRevokeSubmitOverride) setOverridePending(true); - try { - // Step-up: medium tier (password) for single revoke. - await stepUp({ tier: 'medium' }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - onMessage?.({ kind: 'warning', key: 'STEP_UP_CANCELLED', message: merged.stepUpCancelled }); - setConfirmSession(null); - if (onRevokeSubmitOverride) setOverridePending(false); - return; - } - // Step-up infrastructure failure — surface as error. - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - if (onRevokeSubmitOverride) setOverridePending(false); - return; - } - - try { - await runRevoke(session.id); - setConfirmSession(null); - onMessage?.({ kind: 'success', key: 'revokeSession.success', message: merged.sessionRevokedMessage }); - onSessionRevoked?.(session.id); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onRevokeSubmitOverride) setOverridePending(false); - } - } - - async function handleRevokeAllOthers() { - setError(null); - if (onRevokeSubmitOverride) setOverridePending(true); - try { - // Step-up: high tier (MFA if enrolled, else password) for revoke-all-others. - await stepUp({ tier: 'high' }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - onMessage?.({ kind: 'warning', key: 'STEP_UP_CANCELLED', message: merged.stepUpCancelled }); - setConfirmRevokeAll(false); - if (onRevokeSubmitOverride) setOverridePending(false); - return; - } - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - if (onRevokeSubmitOverride) setOverridePending(false); - return; - } - - // No bulk procedure: iterate non-current sessions client-side. - // backend-spec/future-procedures.md: bulk_revoke_sessions is backend-pending. - const others = sessions.filter((s) => !s.isCurrent); - try { - await Promise.all(others.map((s) => runRevoke(s.id))); - setConfirmRevokeAll(false); - onMessage?.({ kind: 'success', key: 'revokeAllOthers.success', message: merged.allOtherRevokedMessage }); - onAllOtherSessionsRevoked?.(); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onRevokeSubmitOverride) setOverridePending(false); - } - } - - const otherSessionCount = sessions.filter((s) => !s.isCurrent).length; - - return ( - - - {merged.title} - {merged.description} - - - - - - {sessions.length === 0 ? ( -

{merged.noSessionsDescription}

- ) : ( -
    - {sessions.map((session, idx) => { - const device = session.parsedDevice ?? parseUserAgent(session.userAgent); - const relativeTime = formatRelativeTime(session.lastUsedAt); - const label = [device.browser, device.os].filter(Boolean).join(' on ') || merged.unknownDevice; - - return ( -
  • - {idx > 0 && } -
    -
    -
    - - {label} - {session.isCurrent && ( - - {merged.currentSessionBadge} - - )} -
    -
    - {relativeTime && ( - - {merged.lastUsedLabel}: {relativeTime} - - )} - {session.ip ? ( - - {merged.ipLabel}: {session.ip} - - ) : ( - {merged.unknownLocation} - )} -
    -
    - - -
    - {session.isCurrent && ( -

    - {merged.currentSessionBadge} -

    - )} -
  • - ); - })} -
- )} - - {otherSessionCount > 0 && ( - <> - - - - )} -
- - {/* Single-session revoke confirmation dialog */} - { if (!open) setConfirmSession(null); }}> - - - {merged.revokeConfirmTitle} - {merged.revokeConfirmDescription} - - - - - - { if (confirmSession) handleRevokeSingle(confirmSession); }} - data-testid="revoke-confirm-button" - > - {merged.revokeConfirmButton} - - - - - - {/* Revoke-all-others confirmation dialog */} - { if (!open) setConfirmRevokeAll(false); }}> - - - {merged.revokeAllConfirmTitle} - {merged.revokeAllConfirmDescription} - - - - - - - {merged.revokeAllConfirmButton} - - - - -
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-sessions-list/auth-account-sessions-list.requires.json b/apps/blocks/src/blocks/auth/account-sessions-list/auth-account-sessions-list.requires.json deleted file mode 100644 index fa3a2a9..0000000 --- a/apps/blocks/src/blocks/auth/account-sessions-list/auth-account-sessions-list.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["revokeSession"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-sessions-list/messages.ts b/apps/blocks/src/blocks/auth/account-sessions-list/messages.ts deleted file mode 100644 index d2ac9b1..0000000 --- a/apps/blocks/src/blocks/auth/account-sessions-list/messages.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * account-sessions-list — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type AccountSessionsListMessages = { - title: string; - description: string; - currentSessionBadge: string; - revokeButton: string; - revokeConfirmTitle: string; - revokeConfirmDescription: string; - revokeConfirmButton: string; - revokeCancelButton: string; - revokeAllOtherButton: string; - revokeAllConfirmTitle: string; - revokeAllConfirmDescription: string; - revokeAllConfirmButton: string; - revokeAllCancelButton: string; - sessionRevokedMessage: string; - allOtherRevokedMessage: string; - lastUsedLabel: string; - createdLabel: string; - ipLabel: string; - unknownDevice: string; - unknownLocation: string; - stepUpCancelled: string; - noSessionsDescription: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: top-level copy is shallow-partial; `errors` is - * itself partial so a host can localize a single error code without restating - * the whole map. - */ -export type AccountSessionsListMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAccountSessionsListMessages: AccountSessionsListMessages = { - title: 'Active sessions', - description: 'These are the devices currently signed in to your account. Revoke any session you do not recognise.', - currentSessionBadge: 'This device', - revokeButton: 'Revoke', - revokeConfirmTitle: 'Revoke session?', - revokeConfirmDescription: 'This device will be signed out immediately.', - revokeConfirmButton: 'Revoke', - revokeCancelButton: 'Cancel', - revokeAllOtherButton: 'Revoke all other sessions', - revokeAllConfirmTitle: 'Revoke all other sessions?', - revokeAllConfirmDescription: - 'All sessions except the current one will be signed out. You will remain signed in on this device.', - revokeAllConfirmButton: 'Revoke all', - revokeAllCancelButton: 'Cancel', - sessionRevokedMessage: 'Session revoked.', - allOtherRevokedMessage: 'All other sessions revoked.', - lastUsedLabel: 'Last active', - createdLabel: 'Signed in', - ipLabel: 'IP', - unknownDevice: 'Unknown device', - unknownLocation: 'Unknown location', - stepUpCancelled: 'Step-up verification cancelled.', - noSessionsDescription: 'No active sessions found.', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/account-settings-page/account-settings-page.tsx b/apps/blocks/src/blocks/auth/account-settings-page/account-settings-page.tsx deleted file mode 100644 index 7aa9167..0000000 --- a/apps/blocks/src/blocks/auth/account-settings-page/account-settings-page.tsx +++ /dev/null @@ -1,330 +0,0 @@ -'use client'; - -/** - * account-settings-page (registry: auth-account-settings-page) - * - * THE CONVERGENCE PAGE. Composes all account-settings section cards into a - * single tabbed layout. Installing this page automatically pulls in every - * section card via registryDependencies. - * - * DATA PATH: calls `useCurrentUserQuery` from `@/generated/auth` once at - * mount to read the current user's `id` and `type`. The result is used to - * gate the api-keys tab behind the `allowApiKeys` prop (feature flag) and - * provide a loading skeleton while the query resolves. - * - * Tab routing: reads `?tab=` from the URL via `useSearchParams()` and - * activates the matching tab. Changing tabs updates the URL for deep-linking. - * - * Pages MAY use `next/navigation`; Cards MUST NOT (block-contract.md §6). - * Ships `auth-account-settings-page.requires.json` per sdk-binding-contract §7. - * - * The Suspense boundary is required by Next.js 15 when `useSearchParams` is - * used anywhere in the component tree below a Client Component boundary. - * - * section cards composed: - * auth-account-profile-card, auth-account-emails-list, - * auth-account-security-card, auth-account-sessions-list, - * auth-account-api-keys-list, auth-account-connected-accounts, - * auth-account-phones-list, auth-account-danger-card - */ - -import { Suspense, useCallback } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; - -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@constructive-io/ui/tabs'; - -import { cn } from '@/lib/utils'; -import { useCurrentUserQuery } from '@/generated/auth'; - -import { AccountProfileCard } from '@/blocks/auth/account-profile-card/account-profile-card'; -import { AccountEmailsList } from '@/blocks/auth/account-emails-list/account-emails-list'; -import { AccountSecurityCard } from '@/blocks/auth/account-security-card/account-security-card'; -import { AccountSessionsList } from '@/blocks/auth/account-sessions-list/account-sessions-list'; -import { AccountApiKeysList } from '@/blocks/auth/account-api-keys-list/account-api-keys-list'; -import { AccountConnectedAccounts } from '@/blocks/auth/account-connected-accounts/account-connected-accounts'; -import { AccountPhonesList } from '@/blocks/auth/account-phones-list/account-phones-list'; -import { AccountDangerCard } from '@/blocks/auth/account-danger-card/account-danger-card'; - -import { - defaultAccountSettingsPageMessages, - type AccountSettingsPageMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type AccountSettingsSection = - | 'profile' - | 'emails' - | 'security' - | 'sessions' - | 'api-keys' - | 'connected-accounts' - | 'phones' - | 'danger'; - -const ALL_SECTIONS: AccountSettingsSection[] = [ - 'profile', - 'emails', - 'security', - 'sessions', - 'api-keys', - 'connected-accounts', - 'phones', - 'danger' -]; - -const DEFAULT_TAB: AccountSettingsSection = 'profile'; - -export type AccountSettingsPageMessageOverrides = Partial; - -export type AccountSettingsPageProps = { - /** - * Which sections to render. Defaults to all sections. - * Consumers can hide sections they don't need without forking the page. - */ - sections?: AccountSettingsSection[]; - messages?: AccountSettingsPageMessageOverrides; - /** - * Route to push after account deletion email is sent. - * Passed through to `auth-account-danger-card`. - */ - onDeletionEmailSent?: () => void; - /** - * Route to use for change-password action. - * Passed through to `auth-account-security-card`. - */ - onChangePassword?: () => void; - /** - * Route to use for manage-passkeys action. - * Passed through to `auth-account-security-card`. - */ - onManagePasskeys?: () => void; - /** - * Route to use for manage-MFA action. When undefined, the security card - * will hide the MFA management CTA (backend-pending in v1). - * Passed through to `auth-account-security-card`. - */ - onManageMfa?: () => void; - /** - * Feature flag: whether the host app has API keys enabled - * (`app_settings_auth.allow_api_keys`). Defaults to `true`. - * When `false`, the API keys tab is omitted from the tab list. - */ - allowApiKeys?: boolean; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Inner content component — must be inside because it calls -// useSearchParams() (Next.js 15 requirement). -// --------------------------------------------------------------------------- - -type AccountSettingsPageContentProps = Omit & { - merged: AccountSettingsPageMessages; - effectiveSections: AccountSettingsSection[]; -}; - -function AccountSettingsPageContent({ - merged, - effectiveSections, - onDeletionEmailSent, - onChangePassword, - onManagePasskeys, - onManageMfa -}: AccountSettingsPageContentProps) { - const router = useRouter(); - const searchParams = useSearchParams(); - - // Active tab from URL `?tab=`, falling back to the first visible section. - const rawTab = searchParams.get('tab') as AccountSettingsSection | null; - const firstSection = effectiveSections[0] ?? DEFAULT_TAB; - const activeTab: AccountSettingsSection = - rawTab && effectiveSections.includes(rawTab) ? rawTab : firstSection; - - const handleTabChange = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (value: any) => { - if (!value || typeof value !== 'string') return; - const params = new URLSearchParams(searchParams.toString()); - params.set('tab', value); - router.replace(`?${params.toString()}`, { scroll: false }); - }, - [router, searchParams] - ); - - const show = (section: AccountSettingsSection) => effectiveSections.includes(section); - - return ( - - - {show('profile') && ( - {merged.profileTabLabel} - )} - {show('emails') && ( - {merged.emailsTabLabel} - )} - {show('security') && ( - {merged.securityTabLabel} - )} - {show('sessions') && ( - {merged.sessionsTabLabel} - )} - {show('api-keys') && ( - {merged.apiKeysTabLabel} - )} - {show('connected-accounts') && ( - - {merged.connectedAccountsTabLabel} - - )} - {show('phones') && ( - {merged.phonesTabLabel} - )} - {show('danger') && ( - {merged.dangerTabLabel} - )} - - - {show('profile') && ( - - - - )} - - {show('emails') && ( - - - - )} - - {show('security') && ( - - - - )} - - {show('sessions') && ( - - - - )} - - {show('api-keys') && ( - - - - )} - - {show('connected-accounts') && ( - - - - )} - - {show('phones') && ( - - - - )} - - {show('danger') && ( - - - - )} - - ); -} - -// --------------------------------------------------------------------------- -// Page -// --------------------------------------------------------------------------- - -/** - * Default export — drop this file at `app/auth/account/page.tsx`. - * The `Suspense` boundary is required by Next.js 15 when `useSearchParams` is - * used anywhere in the component tree below a Client Component boundary. - * - * Calls `useCurrentUserQuery` once to read the current user. Uses the result - * to gate the API keys tab behind the `allowApiKeys` prop. - */ -export default function AccountSettingsPage({ - sections = ALL_SECTIONS, - messages: messageOverrides, - onDeletionEmailSent, - onChangePassword, - onManagePasskeys, - onManageMfa, - allowApiKeys = true, - className -}: AccountSettingsPageProps) { - const merged: AccountSettingsPageMessages = { - ...defaultAccountSettingsPageMessages, - ...messageOverrides - }; - - // Single top-level query — avoids N+1 loading on mount. - // Reads id + type; totpEnabled is not yet on UserSelect (backend pending). - const { isLoading: currentUserLoading } = useCurrentUserQuery({ - selection: { fields: { id: true, type: true } } - }); - - // Gate api-keys tab: omit when allowApiKeys flag is off. - const effectiveSections: AccountSettingsSection[] = allowApiKeys - ? sections - : sections.filter((s) => s !== 'api-keys'); - - return ( -
- {/* Skip-to-content for screen readers. - `sr-only` makes the anchor `position: absolute` without setting - `left`/`top`, so it would keep its static position and can extend the - page's horizontal scroll bounds at narrow viewports; `relative` on the - page wrapper + `left-0 top-0` pin it to the block's corner (which is - also where it should surface when focus reveals it). */} - - {merged.skipToContentLabel} - - -

{merged.pageTitle}

- - {currentUserLoading ? ( -
-
-
-
- ) : ( - - - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/account-settings-page/auth-account-settings-page.requires.json b/apps/blocks/src/blocks/auth/account-settings-page/auth-account-settings-page.requires.json deleted file mode 100644 index b61c8d3..0000000 --- a/apps/blocks/src/blocks/auth/account-settings-page/auth-account-settings-page.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": [], - "queries": ["currentUser"], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/account-settings-page/messages.ts b/apps/blocks/src/blocks/auth/account-settings-page/messages.ts deleted file mode 100644 index ca75c2c..0000000 --- a/apps/blocks/src/blocks/auth/account-settings-page/messages.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * account-settings-page — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy for the page-level chrome (tab labels, page title, - * skip-to-content). Section-level messages belong to each composed card. - */ - -export type AccountSettingsPageMessages = { - pageTitle: string; - skipToContentLabel: string; - profileTabLabel: string; - emailsTabLabel: string; - securityTabLabel: string; - sessionsTabLabel: string; - apiKeysTabLabel: string; - connectedAccountsTabLabel: string; - phonesTabLabel: string; - dangerTabLabel: string; -}; - -export const defaultAccountSettingsPageMessages: AccountSettingsPageMessages = { - pageTitle: 'Account settings', - skipToContentLabel: 'Skip to main content', - profileTabLabel: 'Profile', - emailsTabLabel: 'Emails', - securityTabLabel: 'Security', - sessionsTabLabel: 'Sessions', - apiKeysTabLabel: 'API keys', - connectedAccountsTabLabel: 'Connected accounts', - phonesTabLabel: 'Phones', - dangerTabLabel: 'Account' -}; diff --git a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/anonymous-sign-in-button.tsx b/apps/blocks/src/blocks/auth/anonymous-sign-in-button/anonymous-sign-in-button.tsx deleted file mode 100644 index 0ac3b92..0000000 --- a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/anonymous-sign-in-button.tsx +++ /dev/null @@ -1,177 +0,0 @@ -'use client'; - -/** - * anonymous-sign-in-button (registry: auth-anonymous-sign-in-button) - * - * Single-click guest session button. Creates an anonymous session - * (sessions.is_anonymous=true) without requiring any credentials. - * - * BACKEND-PENDING — CASE (b): - * The `anonymous_sign_in` procedure is not yet deployed in - * `constructive_auth_public`, so `useAnonymousSignInMutation` does NOT exist - * in the generated `@/generated/auth` SDK yet. To keep tsc clean, the - * @/generated/auth import is omitted until the proc ships and codegen is re-run. - * - * The `onSubmit` override seam is therefore the PRIMARY path (required for - * production use until backend ships). Once the backend procedure is deployed: - * 1. Re-run `cnc codegen --api-names auth --react-query --orm -o src/generated` - * 2. Uncomment the `useAnonymousSignInMutation` import below - * 3. Remove the compile-time guard block - * - * Data-binding doctrine: sdk-binding-contract.md §5, §10 (gap honesty). - * No fetch, no GraphQL document string, no configure()/getClient(), no - * QueryClientProvider — all wiring is done by @constructive/blocks-runtime. - * - * Anonymous session upgrade (convert to a real account) is a separate flow - * not provided by this block. Consumers are responsible for hiding this button - * when app_settings_auth.allow_anonymous_sessions is false. - */ - -import { useState } from 'react'; - -// BACKEND-PENDING: Uncomment this import once the anonymous_sign_in proc ships -// and cnc codegen has been re-run. The hook name (verified by contract) is: -// useAnonymousSignInMutation (from @/generated/auth) -// import { useAnonymousSignInMutation } from '@/generated/auth'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; - -import { - defaultAnonymousSignInButtonMessages, - type AnonymousSignInButtonMessageOverrides -} from './messages'; - -/** The result shape returned from the anonymous sign-in call. */ -export type AnonymousSignInResult = { - id: string; - userId: string; - accessToken: string; - accessTokenExpiresAt: string; - isAnonymous: true; -}; - -export type AnonymousSignInButtonProps = { - /** Button text override (uses messages.buttonText by default). */ - children?: React.ReactNode; - /** Credential kind sent to the API (default `'bearer'`). */ - credentialKind?: 'bearer' | 'cookie'; - /** Whether to create a persistent session (default false for guest). */ - rememberMe?: boolean; - messages?: AnonymousSignInButtonMessageOverrides; - /** Button visual variant (passed to the underlying Button). */ - variant?: 'default' | 'outline' | 'ghost' | 'link'; - /** - * Replace the default `useAnonymousSignInMutation` call. - * REQUIRED until `anonymous_sign_in` backend procedure ships. - * The host wires the generated binding after regenerating the SDK. - */ - onSubmit?: () => Promise; - /** Fires after a resolved anonymous sign-in. Always fires. */ - onSuccess?: (result: AnonymousSignInResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -export function AnonymousSignInButton({ - children, - credentialKind = 'bearer', - rememberMe = false, - messages: messageOverrides, - variant = 'outline', - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: AnonymousSignInButtonProps) { - // Deep merge: top-level copy + the errors map merged separately. - const merged = { - ...defaultAnonymousSignInButtonMessages, - ...messageOverrides, - errors: { ...defaultAnonymousSignInButtonMessages.errors, ...messageOverrides?.errors } - }; - - // BACKEND-PENDING (Case b): the generated hook is not yet in the SDK, so we - // cannot instantiate useAnonymousSignInMutation here. Once the proc ships, - // replace the stub below with: - // - // const defaultMutation = useAnonymousSignInMutation({ - // selection: { - // fields: { - // id: true, - // userId: true, - // accessToken: true, - // accessTokenExpiresAt: true, - // } - // } - // }); - // - // And update runAnonymousSignIn to: - // const data = await defaultMutation.mutateAsync({ input: { rememberMe, credentialKind } }); - // return data.anonymousSignIn as AnonymousSignInResult; - // - // Hybrid pending: onSubmitOverride ? overridePending : defaultMutation.isPending - - const [overridePending, setOverridePending] = useState(false); - const [error, setError] = useState(null); - - // While the backend is pending, isPending tracks only the override path. - // When the default mutation is restored, swap to the hybrid pattern. - const isPending = overridePending; - - async function handleClick() { - setError(null); - - if (!onSubmitOverride) { - // Backend pending — surface PROCEDURE_NOT_FOUND as a clear message. - const msg = merged.errors.PROCEDURE_NOT_FOUND; - const code = 'PROCEDURE_NOT_FOUND'; - setError(msg); - onMessage?.({ kind: 'error', key: code, message: msg }); - onError?.({ message: msg, code }); - return; - } - - setOverridePending(true); - try { - const result = await onSubmitOverride(); - onMessage?.({ kind: 'success', key: 'anonymousSignIn.success', message: merged.successMessage }); - onSuccess?.(result); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setOverridePending(false); - } - } - - return ( -
- - - {children ?? merged.buttonText} - -
- ); -} diff --git a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/auth-anonymous-sign-in-button.requires.json b/apps/blocks/src/blocks/auth/anonymous-sign-in-button/auth-anonymous-sign-in-button.requires.json deleted file mode 100644 index 3092db7..0000000 --- a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/auth-anonymous-sign-in-button.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["anonymousSignIn"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/messages.ts b/apps/blocks/src/blocks/auth/anonymous-sign-in-button/messages.ts deleted file mode 100644 index 553346d..0000000 --- a/apps/blocks/src/blocks/auth/anonymous-sign-in-button/messages.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * anonymous-sign-in-button — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` - * as `customMessages`. - * - * PROCEDURE_NOT_FOUND is required because `anonymous_sign_in` is a - * backend-pending procedure. It will surface as a GraphQL error until the - * proc ships and codegen is re-run. - */ - -export type AnonymousSignInButtonMessages = { - buttonText: string; - buttonPending: string; - successMessage: string; - /** Error messages keyed by UPPER_SNAKE_CASE backend error code */ - errors: { - PROCEDURE_NOT_FOUND: string; - ANONYMOUS_DISABLED: string; - RATE_LIMITED: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: hosts can localize a single error code without - * restating the full map (block-contract.md §10). - */ -export type AnonymousSignInButtonMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultAnonymousSignInButtonMessages: AnonymousSignInButtonMessages = { - buttonText: 'Continue as guest', - buttonPending: 'Starting session…', - successMessage: 'Guest session started.', - errors: { - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - ANONYMOUS_DISABLED: 'Guest access is not available.', - RATE_LIMITED: 'Too many requests. Please wait.', - UNKNOWN_ERROR: 'Failed to start guest session. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/api-key-create-dialog/api-key-create-dialog.tsx b/apps/blocks/src/blocks/auth/api-key-create-dialog/api-key-create-dialog.tsx deleted file mode 100644 index f5d13d6..0000000 --- a/apps/blocks/src/blocks/auth/api-key-create-dialog/api-key-create-dialog.tsx +++ /dev/null @@ -1,514 +0,0 @@ -'use client'; - -/** - * api-key-create-dialog (registry: auth-api-key-create-dialog) - * - * Modal dialog form for creating a new user-scoped API key. - * Enforces high-severity step-up (`tier: 'high'`) before calling - * `createApiKey`. On success, delivers the raw key + metadata to - * `onSuccess`; the parent (auth-account-api-keys-list) is responsible - * for opening auth-api-key-created-modal to show the one-time key value. - * - * Data path — GENERATED hook only: - * `useCreateApiKeyMutation` imported from `@/generated/auth`. - * No fetch, no GraphQL document string, no @constructive-io/data. - * No client bootstrap — blocks-runtime does all wiring. - * - * Hook signature (verified against reference SDK): - * variables: { input: { keyName?, accessLevel?, mfaLevel?, expiresIn?: IntervalInput } } - * payload wrapper: data.createApiKey → CreateApiKeyRecord { apiKey, keyId, expiresAt } - * i.e. result is nested under `result` field → data.createApiKey.result - * - * Step-up: `await stepUp({ tier: 'high' })` gates the mutation. If step-up is - * cancelled (`StepUpError.reason === 'cancelled'`) the dialog stays open without - * firing error callbacks (silent return per step-up-contract.md §3). - * - * (sdk-binding-contract.md §5–§7, block-contract.md §10) - */ - -import { useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter -} from '@constructive-io/ui/dialog'; -import { Button } from '@constructive-io/ui/button'; -import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem -} from '@constructive-io/ui/select'; - -import { cn } from '@/lib/utils'; -import { useCreateApiKeyMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; - -import { - defaultApiKeyCreateDialogMessages, - type ApiKeyCreateDialogMessages, - type ApiKeyCreateDialogMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -/** Variables the create-api-key call receives. The override `onSubmit` gets these verbatim. */ -export type ApiKeyCreateInput = { - /** Key name. Non-empty, trimmed, max 100 chars. */ - name: string; - accessLevel: string; - mfaLevel: string; - /** - * Postgres interval string (e.g. "30 days") or null for no expiry. - * NOTE: The backend expects an `IntervalInput` object. This block converts - * the preset string values to the correct { days: N } shape internally. - * When onSubmit override is provided it receives the raw preset value. - */ - expiresIn: string | null; -}; - -export type ApiKeyCreatedResult = { - keyId: string; - rawKey: string; - name: string; - expiresAt: string | null; -}; - -export type AccessLevelOption = { value: string; label: string }; -export type MfaLevelOption = { value: string; label: string }; - -export type ApiKeyCreateDialogProps = { - /** Controlled open state. */ - open: boolean; - onOpenChange: (open: boolean) => void; - /** - * Available access levels. Default: ['read_only', 'full_access']. - * The deployed `create_api_key` proc ONLY accepts `read_only` | `full_access`. - */ - accessLevelOptions?: AccessLevelOption[]; - /** - * Available MFA levels. Default: ['none', 'verified']. - * The deployed `create_api_key` proc ONLY accepts `none` | `verified`. - */ - mfaLevelOptions?: MfaLevelOption[]; - messages?: ApiKeyCreateDialogMessageOverrides; - /** - * Replace the default `useCreateApiKeyMutation` call. - * Receives the raw form values after step-up succeeds. - */ - onSubmit?: (input: ApiKeyCreateInput) => Promise; - /** - * Fires on successful creation with the raw key and metadata. - * Parent should use this to open auth-api-key-created-modal. - * Always fires. - */ - onSuccess: (result: ApiKeyCreatedResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for step-up events and errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Interval conversion -// --------------------------------------------------------------------------- - -/** Convert preset expiry strings to IntervalInput for the backend. */ -function expiresInToInterval(value: string | null): { days: number } | undefined { - if (!value) return undefined; - const map: Record = { - '30 days': 30, - '90 days': 90, - '180 days': 180, - '365 days': 365 - }; - const days = map[value]; - if (days) return { days }; - return undefined; -} - -// --------------------------------------------------------------------------- -// Default option sets -// --------------------------------------------------------------------------- - -// The deployed `create_api_key` proc only accepts these enum values: -// accessLevel ∈ { read_only, full_access } mfaLevel ∈ { none, verified } -// Any other value (e.g. read/write/admin, required) -> INVALID_ACCESS_LEVEL. -const DEFAULT_ACCESS_LEVEL_OPTIONS: AccessLevelOption[] = [ - { value: 'read_only', label: 'Read only' }, - { value: 'full_access', label: 'Full access' } -]; - -const DEFAULT_MFA_LEVEL_OPTIONS: MfaLevelOption[] = [ - { value: 'none', label: 'None' }, - { value: 'verified', label: 'Verified' } -]; - -// --------------------------------------------------------------------------- -// Form data -// --------------------------------------------------------------------------- - -interface ApiKeyFormData { - name: string; - accessLevel: string; - mfaLevel: string; - expiresIn: string; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function ApiKeyCreateDialog({ - open, - onOpenChange, - accessLevelOptions = DEFAULT_ACCESS_LEVEL_OPTIONS, - mfaLevelOptions = DEFAULT_MFA_LEVEL_OPTIONS, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: ApiKeyCreateDialogProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: ApiKeyCreateDialogMessages = { - ...defaultApiKeyCreateDialogMessages, - ...messageOverrides, - expiresInOptions: { - ...defaultApiKeyCreateDialogMessages.expiresInOptions, - ...messageOverrides?.expiresInOptions - }, - errors: { - ...defaultApiKeyCreateDialogMessages.errors, - ...messageOverrides?.errors - } - }; - - // Generated hook from the host's `auth` SDK (sdk-binding-contract.md §5). - // Payload: data.createApiKey.result -> { apiKey, keyId, expiresAt } - const defaultMutation = useCreateApiKeyMutation({ - selection: { - fields: { - result: { - select: { - apiKey: true, - keyId: true, - expiresAt: true - } - } - } - } - }); - - const stepUp = useStepUp(); - - // Hybrid pending: generated hook tracks its own; override path uses local state. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - const [error, setError] = useState(null); - - async function runCreate(input: ApiKeyCreateInput): Promise { - if (onSubmitOverride) return onSubmitOverride(input); - // Build the interval input from the preset string. - const interval = expiresInToInterval(input.expiresIn); - const data = await defaultMutation.mutateAsync({ - input: { - keyName: input.name, - accessLevel: input.accessLevel, - mfaLevel: input.mfaLevel, - ...(interval ? { expiresIn: interval } : {}) - } - }); - const rec = data.createApiKey?.result; - if (!rec?.keyId || !rec?.apiKey) { - throw Object.assign(new Error('No key returned'), { extensions: { code: 'UNKNOWN_ERROR' } }); - } - return { - keyId: rec.keyId, - rawKey: rec.apiKey, - name: input.name, - expiresAt: rec.expiresAt ?? null - }; - } - - async function handleSubmit(values: ApiKeyFormData) { - setError(null); - - // Step 1: step-up BEFORE mutation (step-up-contract.md §5, tier: 'high'). - try { - await stepUp({ tier: 'high' }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - // Silent return — dialog stays open, no error fired (step-up-contract.md §3). - onMessage?.({ kind: 'info', key: 'stepUpCancelled', message: merged.stepUpCancelled }); - return; - } - // Step-up failed (non-cancel) — treat as error. - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - return; - } - - // Step 2: create the API key. - if (onSubmitOverride) setOverridePending(true); - try { - const input: ApiKeyCreateInput = { - name: values.name.trim(), - accessLevel: values.accessLevel, - mfaLevel: values.mfaLevel, - expiresIn: values.expiresIn === '__none__' ? null : values.expiresIn - }; - const result = await runCreate(input); - - // Do NOT auto-close here — parent closes and opens created-modal. - // Prevents a flash where both dialog and modal try to render simultaneously - // (spec §Notes). - onSuccess(result); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - const form = useForm({ - defaultValues: { - name: '', - accessLevel: accessLevelOptions[0]?.value ?? 'read_only', - mfaLevel: mfaLevelOptions[0]?.value ?? 'none', - expiresIn: '__none__' - } as ApiKeyFormData, - onSubmit: async ({ value }) => { - await handleSubmit(value); - } - }); - - function handleCancel() { - form.reset(); - setError(null); - onOpenChange(false); - } - - // Build expiry options from messages catalog. - const expiryOptions = [ - { value: '__none__', label: merged.expiresInOptions.noExpiry }, - { value: '30 days', label: merged.expiresInOptions.days30 }, - { value: '90 days', label: merged.expiresInOptions.days90 }, - { value: '180 days', label: merged.expiresInOptions.days180 }, - { value: '365 days', label: merged.expiresInOptions.days365 } - ]; - - return ( - - - - {merged.title} - {merged.description} - - - - -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - {/* Key name */} - { - if (!value || !value.trim()) return 'Key name is required'; - if (value.trim().length > 100) return 'Key name must be 100 characters or fewer'; - return undefined; - } - }} - > - {(field) => ( - - )} - - - {/* Access level */} - (!value ? 'Access level is required' : undefined) - }} - > - {(field) => ( -
- - - {field.state.meta.errors.length > 0 && ( -

{field.state.meta.errors[0]}

- )} -
- )} -
- - {/* MFA level */} - (!value ? 'MFA level is required' : undefined) - }} - > - {(field) => ( -
- - - {field.state.meta.errors.length > 0 && ( -

{field.state.meta.errors[0]}

- )} -
- )} -
- - {/* Expiry */} - - {(field) => ( -
- - -
- )} -
- - - - - {merged.createButton} - - -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/api-key-create-dialog/auth-api-key-create-dialog.requires.json b/apps/blocks/src/blocks/auth/api-key-create-dialog/auth-api-key-create-dialog.requires.json deleted file mode 100644 index fb1e2af..0000000 --- a/apps/blocks/src/blocks/auth/api-key-create-dialog/auth-api-key-create-dialog.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["createApiKey"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/api-key-create-dialog/messages.ts b/apps/blocks/src/blocks/auth/api-key-create-dialog/messages.ts deleted file mode 100644 index c99290b..0000000 --- a/apps/blocks/src/blocks/auth/api-key-create-dialog/messages.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * api-key-create-dialog — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type ApiKeyCreateDialogMessages = { - title: string; - description: string; - nameLabel: string; - namePlaceholder: string; - accessLevelLabel: string; - mfaLevelLabel: string; - expiresInLabel: string; - expiresInOptions: { - noExpiry: string; - days30: string; - days90: string; - days180: string; - days365: string; - }; - createButton: string; - creatingButton: string; - cancelButton: string; - stepUpPrompt: string; - stepUpCancelled: string; - errors: { - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: top-level copy is shallow-partial; `errors` is - * itself partial so a host can localize a single error code without restating - * the full catalog. - */ -export type ApiKeyCreateDialogMessageOverrides = Partial> & { - errors?: Partial; - expiresInOptions?: Partial; -}; - -export const defaultApiKeyCreateDialogMessages: ApiKeyCreateDialogMessages = { - title: 'Create API key', - description: 'API keys provide programmatic access to your account.', - nameLabel: 'Key name', - namePlaceholder: 'e.g. CI deploy key', - accessLevelLabel: 'Access level', - mfaLevelLabel: 'MFA requirement', - expiresInLabel: 'Expiry', - expiresInOptions: { - noExpiry: 'No expiry', - days30: '30 days', - days90: '90 days', - days180: '180 days', - days365: '1 year' - }, - createButton: 'Create key', - creatingButton: 'Creating…', - cancelButton: 'Cancel', - stepUpPrompt: 'Confirm your identity before creating an API key.', - stepUpCancelled: 'Step-up verification cancelled.', - errors: { - UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/api-key-created-modal/api-key-created-modal.tsx b/apps/blocks/src/blocks/auth/api-key-created-modal/api-key-created-modal.tsx deleted file mode 100644 index 95bfbb5..0000000 --- a/apps/blocks/src/blocks/auth/api-key-created-modal/api-key-created-modal.tsx +++ /dev/null @@ -1,317 +0,0 @@ -'use client'; - -/** - * api-key-created-modal (registry: auth-api-key-created-modal) - * - * Presentational block — one-time display of a freshly-created raw API key - * (cnc_live_sk_...). The raw key is unrecoverable after this view; the DB - * stores only the SHA-256 hash. The modal enforces an explicit - * "I have saved this key" acknowledgement before allowing dismiss. - * - * NO data operation, NO generated hook, NO blocks-runtime dependency. - * The raw key is passed in as a prop by auth-account-api-keys-list after - * auth-api-key-create-dialog succeeds (spec §Pairing). - * - * Safety rails (Base UI Dialog API): - * • `disablePointerDismissal` on Dialog root: blocks overlay click when unacknowledged. - * • `onOpenChange` intercept: blocks Escape close (reason === 'escapeKey') when unacknowledged. - * • "Done" button uses aria-disabled (not native disabled) to remain in tab order. - * • Copy feedback reverts automatically after 2 s. - * - * (sdk-binding-contract.md §7 — presentational blocks ship no requires.json.) - */ - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { TriangleAlertIcon, CopyIcon, CheckIcon } from 'lucide-react'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; - -import { cn } from '@/lib/utils'; - -import { - defaultApiKeyCreatedModalMessages, - type ApiKeyCreatedModalMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export type ApiKeyCreatedModalMessageOverrides = Partial; - -export type ApiKeyCreatedModalProps = { - /** Whether the dialog is open. */ - open: boolean; - /** Called when the dialog requests a state change (open/close). */ - onOpenChange: (open: boolean) => void; - /** The raw API key (cnc_live_sk_...) — exists only in React state, never stored here. */ - apiKey: string; - /** Human-readable name of the key, shown in the modal title. */ - keyName: string; - /** Optional ISO-8601 expiry timestamp for display only. */ - expiresAt?: string | null; - /** BCP 47 locale used to format the expiry date. Default: en-US. */ - locale?: string; - /** IANA time-zone name used to format the expiry date. Default: UTC. */ - timeZone?: string; - /** Called when the user checks the acknowledgement and clicks "Done". */ - onDismissed?: () => void; - messages?: ApiKeyCreatedModalMessageOverrides; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function ApiKeyCreatedModal({ - open, - onOpenChange, - apiKey, - keyName, - expiresAt, - locale = 'en-US', - timeZone = 'UTC', - onDismissed, - messages: messageOverrides, - className -}: ApiKeyCreatedModalProps) { - // Deep merge (flat catalog — no nested errors map for this block). - const merged: ApiKeyCreatedModalMessages = { - ...defaultApiKeyCreatedModalMessages, - ...messageOverrides - }; - - const [hasCopied, setHasCopied] = useState(false); - const [copyError, setCopyError] = useState(null); - const [hasAcknowledged, setHasAcknowledged] = useState(false); - - // Use a ref so the onOpenChange interceptor always has the current value - // without a stale closure (avoids re-creating the handler on every state tick). - const acknowledgedRef = useRef(hasAcknowledged); - useEffect(() => { - acknowledgedRef.current = hasAcknowledged; - }, [hasAcknowledged]); - - // Reset local state when the modal opens. - useEffect(() => { - if (open) { - setHasCopied(false); - setCopyError(null); - setHasAcknowledged(false); - acknowledgedRef.current = false; - } - }, [open]); - - // Copy-button timeout ref for cleanup on unmount. - const copyTimeoutRef = useRef | null>(null); - useEffect(() => { - return () => { - if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); - }; - }, []); - - const copyErrorMessage = merged.copyErrorMessage; - const handleCopy = useCallback(async () => { - setCopyError(null); - try { - await navigator.clipboard.writeText(apiKey); - setHasCopied(true); - copyTimeoutRef.current = setTimeout(() => setHasCopied(false), 2000); - } catch { - setCopyError(copyErrorMessage); - } - }, [apiKey, copyErrorMessage]); - - // Base UI onOpenChange intercept — block Escape + any programmatic close - // when the user has not acknowledged they saved the key. - // The signature is (open: boolean, eventDetails: DialogRoot.ChangeEventDetails). - const handleOpenChange = useCallback( - (nextOpen: boolean, eventDetails?: { reason?: string }) => { - if (!nextOpen && !acknowledgedRef.current) { - // Block Escape key and outside-press dismissal; Done button is the only exit. - // Base UI REASONS constants use hyphenated strings: 'escape-key' and 'outside-press'. - if ( - eventDetails?.reason === 'escape-key' || - eventDetails?.reason === 'outside-press' - ) { - return; // Do not propagate — keep the dialog open. - } - } - onOpenChange(nextOpen); - }, - [onOpenChange] - ); - - function handleDone() { - if (!hasAcknowledged) return; - onDismissed?.(); - onOpenChange(false); - } - - // Format expiry for display. - const expiryDisplay = useMemo( - () => - expiresAt - ? new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'short', - day: 'numeric', - timeZone - }).format(new Date(expiresAt)) - : merged.noExpiry, - [expiresAt, locale, merged.noExpiry, timeZone] - ); - - return ( - void} - disablePointerDismissal={!hasAcknowledged} - > - - - {merged.title} - - One-time display of your new API key for {keyName}. Save it before closing. - - - -
- {/* Warning banner */} -
-
- - {/* Key display area */} -
-

{merged.keyLabel}

-
- - {apiKey} - -
- -
-
- {copyError && ( -

- {copyError} -

- )} -
- - {/* Expiry line */} -
- {merged.expiresLabel}: - {expiresAt ? ( - - {expiryDisplay} - - ) : ( - - {expiryDisplay} - - )} -
- - {/* Separator */} -
- - {/* Acknowledgement checkbox */} -
- setHasAcknowledged(e.target.checked)} - aria-required="true" - className="mt-0.5 size-4 accent-primary cursor-pointer" - data-testid="acknowledge-checkbox" - /> - -
- - {/* Dismiss button */} - -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/api-key-created-modal/messages.ts b/apps/blocks/src/blocks/auth/api-key-created-modal/messages.ts deleted file mode 100644 index 1b523b8..0000000 --- a/apps/blocks/src/blocks/auth/api-key-created-modal/messages.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * api-key-created-modal — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy. No `errors` map — this is a presentational block - * with no data operations and no backend error codes to surface. - */ - -export type ApiKeyCreatedModalMessages = { - title: string; - warningHeading: string; - warningBody: string; - keyLabel: string; - expiresLabel: string; - noExpiry: string; - copyButton: string; - copiedButton: string; - copyErrorMessage: string; - acknowledgementLabel: string; - dismissButton: string; -}; - -export const defaultApiKeyCreatedModalMessages: ApiKeyCreatedModalMessages = { - title: 'API key created', - warningHeading: 'Save this key now', - warningBody: - 'This is the only time you will see this key. It cannot be recovered once you close this window.', - keyLabel: 'Your new API key', - expiresLabel: 'Expires', - noExpiry: 'Never', - copyButton: 'Copy', - copiedButton: 'Copied!', - copyErrorMessage: 'Could not copy to clipboard. Please select and copy the key manually.', - acknowledgementLabel: 'I have copied and saved this API key in a secure location.', - dismissButton: 'Done' -}; diff --git a/apps/blocks/src/blocks/auth/change-password-form/auth-change-password-form.requires.json b/apps/blocks/src/blocks/auth/change-password-form/auth-change-password-form.requires.json deleted file mode 100644 index 9b71169..0000000 --- a/apps/blocks/src/blocks/auth/change-password-form/auth-change-password-form.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["setPassword"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/change-password-form/change-password-form.tsx b/apps/blocks/src/blocks/auth/change-password-form/change-password-form.tsx deleted file mode 100644 index 358c58a..0000000 --- a/apps/blocks/src/blocks/auth/change-password-form/change-password-form.tsx +++ /dev/null @@ -1,320 +0,0 @@ -'use client'; - -/** - * change-password-form (registry: auth-change-password-form) - * - * Inline form for authenticated users to update their password. Fields: current - * password + new password + confirm new password. Before submitting, gates behind - * `await stepUp({ tier: 'medium' })` (password re-verification). Provides inline - * strength feedback for the new password via the `password-strength` foundation lib. - * - * Binding doctrine (sdk-binding-contract.md §3, MASTER-PROMPT §5): - * • Data path = `useSetPasswordMutation` from `@/generated/auth` with a - * `selection` field-picker. No fetch, no GraphQL document, no hardcoded URL. - * • NO client bootstrap: never calls `configure()`/`getClient()`, never mounts - * ``. The host's `@constructive/blocks-runtime` does that. - * • Override seam: `onSubmit` fully replaces the generated-hook call. - * • Error mapping via `parseGraphQLError` from the `auth-errors` foundation lib. - */ - -import { useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { cn } from '@/lib/utils'; -import { useSetPasswordMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { estimatePasswordStrength } from '@/blocks/lib/password-strength'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; -import { Progress } from '@constructive-io/ui/progress'; - -import { defaultChangePasswordFormMessages, type ChangePasswordFormMessageOverrides, type ChangePasswordFormMessages } from './messages'; - -/** Input variables the change-password call receives. The override `onSubmit` gets these. */ -export type ChangePasswordInput = { - currentPassword: string; - newPassword: string; -}; - -export type ChangePasswordResult = { - success: boolean; -}; - -export type ChangePasswordFormProps = { - /** Show new password strength meter (default: true). */ - showPasswordStrength?: boolean; - /** - * Whether to check step-up before submission. - * Default: true. Set to false to skip when sign-in already verified recently. - */ - requireStepUp?: boolean; - messages?: ChangePasswordFormMessageOverrides; - /** Replace the default `useSetPasswordMutation` call. Receives the same vars. */ - onSubmit?: (input: ChangePasswordInput) => Promise; - /** Fires after a successful password update. Always fires. */ - onSuccess?: (result: ChangePasswordResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -type FormData = { - currentPassword: string; - newPassword: string; - confirmPassword: string; -}; - -function strengthLabel(merged: ChangePasswordFormMessages, label: 'weak' | 'fair' | 'good' | 'strong'): string { - const map = { - weak: merged.passwordStrengthWeak, - fair: merged.passwordStrengthFair, - good: merged.passwordStrengthGood, - strong: merged.passwordStrengthStrong - }; - return map[label]; -} - -function strengthColorClass(label: 'weak' | 'fair' | 'good' | 'strong'): string { - const map = { - weak: '[&_[data-slot=progress-indicator]]:bg-destructive', - fair: '[&_[data-slot=progress-indicator]]:bg-yellow-500', - good: '[&_[data-slot=progress-indicator]]:bg-blue-500', - strong: '[&_[data-slot=progress-indicator]]:bg-green-500' - }; - return map[label]; -} - -export function ChangePasswordForm({ - showPasswordStrength = true, - requireStepUp: requireStepUpProp = true, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: ChangePasswordFormProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: ChangePasswordFormMessages = { - ...defaultChangePasswordFormMessages, - ...messageOverrides, - errors: { ...defaultChangePasswordFormMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook from the host's `auth` SDK. The payload wraps the result - // under `setPassword.result` (boolean). Verified against generated types. - const defaultMutation = useSetPasswordMutation({ - selection: { - fields: { - result: true - } - } - }); - - // Step-up hook — imperative promise-based API (step-up-contract.md §3). - const stepUp = useStepUp(); - - // Hybrid pending: the generated hook tracks its own; the override path does not. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - const [error, setError] = useState(null); - const [newPasswordValue, setNewPasswordValue] = useState(''); - - async function handleChangePassword(values: FormData): Promise { - setError(null); - - // Confirm password mismatch guard. - if (values.newPassword !== values.confirmPassword) { - setError(merged.passwordMismatch); - return; - } - - // Step-up gate (tier: 'medium' → password re-verification per step-up-contract §6). - if (requireStepUpProp) { - try { - await stepUp({ tier: 'medium' }); - } catch (err) { - if (err instanceof StepUpError && err.reason === 'cancelled') { - // User cancelled the dialog — surface as STEP_UP_CANCELLED. - const message = merged.errors.STEP_UP_CANCELLED; - setError(message); - onMessage?.({ kind: 'error', key: 'STEP_UP_CANCELLED', message }); - onError?.({ message, code: 'STEP_UP_CANCELLED' }); - return; - } - // Other step-up failure. - const { code, message } = parseGraphQLError(err, { customMessages: merged.errors, defaultMessage: merged.errors.UNKNOWN_ERROR }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - return; - } - } - - if (onSubmitOverride) setOverridePending(true); - try { - const input: ChangePasswordInput = { - currentPassword: values.currentPassword, - newPassword: values.newPassword - }; - - let success: boolean; - if (onSubmitOverride) { - success = await onSubmitOverride(input); - } else { - const data = await defaultMutation.mutateAsync({ input: { currentPassword: values.currentPassword, newPassword: values.newPassword } }); - success = data.setPassword?.result ?? false; - } - - if (!success) { - // A resolved mutation returning false means current password was wrong. - const message = merged.errors.INVALID_CREDENTIALS; - setError(message); - onMessage?.({ kind: 'error', key: 'INVALID_CREDENTIALS', message }); - onError?.({ message, code: 'INVALID_CREDENTIALS' }); - return; - } - - onMessage?.({ kind: 'success', key: 'changePassword.success', message: merged.successMessage }); - onSuccess?.({ success: true }); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - const form = useForm({ - defaultValues: { - currentPassword: '', - newPassword: '', - confirmPassword: '' - } as FormData, - onSubmit: async ({ value }) => { - await handleChangePassword(value); - } - }); - - const strength = showPasswordStrength && newPasswordValue ? estimatePasswordStrength(newPasswordValue) : null; - const strengthPct = strength ? (strength.score / 4) * 100 : 0; - - return ( -
-

{merged.title}

- - - -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - (!value ? 'Current password is required' : undefined) - }} - > - {(field) => ( - - )} - - - { - setNewPasswordValue(value ?? ''); - if (!value) return 'New password is required'; - if (value.length < 8) return 'Password must be at least 8 characters'; - if (value.length > 63) return 'Password must be at most 63 characters'; - return undefined; - } - }} - > - {(field) => ( -
- - {showPasswordStrength && newPasswordValue && strength && ( -
- -

{strengthLabel(merged, strength.label)}

-
- )} -
- )} -
- - (!value ? 'Please confirm your new password' : undefined) - }} - > - {(field) => ( - - )} - - -
- - {merged.submitButton} - -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/change-password-form/messages.ts b/apps/blocks/src/blocks/auth/change-password-form/messages.ts deleted file mode 100644 index 9b20fce..0000000 --- a/apps/blocks/src/blocks/auth/change-password-form/messages.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * change-password-form — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type ChangePasswordFormMessages = { - title: string; - currentPasswordLabel: string; - currentPasswordPlaceholder: string; - newPasswordLabel: string; - newPasswordPlaceholder: string; - confirmPasswordLabel: string; - confirmPasswordPlaceholder: string; - submitButton: string; - submitButtonPending: string; - passwordMismatch: string; - passwordStrengthWeak: string; - passwordStrengthFair: string; - passwordStrengthGood: string; - passwordStrengthStrong: string; - successMessage: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - INVALID_CREDENTIALS: string; - INCORRECT_PASSWORD: string; - WEAK_PASSWORD: string; - STEP_UP_REQUIRED: string; - STEP_UP_CANCELLED: string; - UNKNOWN_ERROR: string; - }; -}; - -export type ChangePasswordFormMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultChangePasswordFormMessages: ChangePasswordFormMessages = { - title: 'Change password', - currentPasswordLabel: 'Current password', - currentPasswordPlaceholder: '••••••••', - newPasswordLabel: 'New password', - newPasswordPlaceholder: '••••••••', - confirmPasswordLabel: 'Confirm new password', - confirmPasswordPlaceholder: '••••••••', - submitButton: 'Update password', - submitButtonPending: 'Updating…', - passwordMismatch: 'Passwords do not match.', - passwordStrengthWeak: 'Weak', - passwordStrengthFair: 'Fair', - passwordStrengthGood: 'Good', - passwordStrengthStrong: 'Strong', - successMessage: 'Password updated successfully.', - errors: { - INVALID_CREDENTIALS: 'Current password is incorrect.', - INCORRECT_PASSWORD: 'Current password is incorrect.', - WEAK_PASSWORD: 'New password does not meet minimum requirements.', - STEP_UP_REQUIRED: 'Please verify your identity to continue.', - STEP_UP_CANCELLED: 'Identity verification was cancelled.', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/cross-origin-link/auth-cross-origin-link.requires.json b/apps/blocks/src/blocks/auth/cross-origin-link/auth-cross-origin-link.requires.json deleted file mode 100644 index 76b4641..0000000 --- a/apps/blocks/src/blocks/auth/cross-origin-link/auth-cross-origin-link.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["requestCrossOriginToken"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/cross-origin-link/cross-origin-link.tsx b/apps/blocks/src/blocks/auth/cross-origin-link/cross-origin-link.tsx deleted file mode 100644 index 43636f3..0000000 --- a/apps/blocks/src/blocks/auth/cross-origin-link/cross-origin-link.tsx +++ /dev/null @@ -1,207 +0,0 @@ -'use client'; - -/** - * cross-origin-link (registry: auth-cross-origin-link) - * - * Generates a one-time cross-origin authentication token by calling - * `constructive_auth_public.request_cross_origin_token(...)`, then navigates - * to `${destinationOrigin}${destinationPath}?token=` via - * `window.location.href` (intentional cross-origin redirect). - * - * Binding doctrine (sdk-binding-contract.md §5): - * • Data path = `useRequestCrossOriginTokenMutation` from `@/generated/auth`. - * No fetch, no GraphQL document string, no `@constructive-io/data` import. - * • No client bootstrap: never calls `configure()`/`getClient()`, never mounts - * a `QueryClientProvider`. The host mounts `blocks-runtime` once at app root. - * • Override seam: `onSubmit` fully replaces the generated-hook call. - * • Error mapping via `auth-errors` foundation lib; inline alert via - * `auth-error-alert` primitive. - * - * The block does NOT ship a form with email/password inputs — it receives them - * as props from a parent form context where the user already typed credentials. - */ - -import { useState } from 'react'; - -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { useRequestCrossOriginTokenMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; - -import { defaultCrossOriginLinkMessages, type CrossOriginLinkMessages } from './messages'; - -/** Variables sent to the cross-origin token mutation. */ -export type CrossOriginLinkInput = { - email: string; - password: string; - origin: string; - rememberMe: boolean; -}; - -/** - * Deep-partial message overrides: top-level keys are shallow-partial; - * `errors` is itself partial so a host can override a single error code. - */ -export type CrossOriginLinkMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type CrossOriginLinkProps = { - /** Email for credential verification (passed from the parent form). */ - email: string; - /** Password for credential verification (passed from the parent form). */ - password: string; - /** Target origin, e.g. 'https://app.example.com'. Must be allowlisted server-side. */ - destinationOrigin: string; - /** - * Path on the destination to redirect to after token exchange. - * The token is appended as ?token=. Default: '/auth/cross-origin'. - */ - destinationPath?: string; - rememberMe?: boolean; - /** Render as a button (default) or an anchor link. */ - renderAs?: 'button' | 'link'; - /** Content rendered inside the button/link. Falls back to messages.defaultButtonText. */ - children?: React.ReactNode; - /** Visual variant passed through to the underlying Button. Default: 'default'. */ - variant?: 'default' | 'outline' | 'ghost' | 'link'; - messages?: CrossOriginLinkMessageOverrides; - /** Replace the default `useRequestCrossOriginTokenMutation` call. Must return the token string. */ - onSubmit?: (input: CrossOriginLinkInput) => Promise; - /** Fires after token generation, before redirect. Always fires on success. */ - onSuccess?: (token: string, url: string) => void; - /** Fires after a mapped error. Always fires on error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and all errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -export function CrossOriginLink({ - email, - password, - destinationOrigin, - destinationPath = '/auth/cross-origin', - rememberMe = false, - renderAs = 'button', - children, - variant = 'default', - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: CrossOriginLinkProps) { - // Deep merge: top-level keys + the errors map merged separately. - const merged: CrossOriginLinkMessages = { - ...defaultCrossOriginLinkMessages, - ...messageOverrides, - errors: { ...defaultCrossOriginLinkMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hook from the host's `auth` SDK (sdk-binding-contract.md §5). - // `RequestCrossOriginTokenPayload.result` is a plain string (the token), - // so the selection uses `{ result: true }` — a scalar boolean selector. - const defaultMutation = useRequestCrossOriginTokenMutation({ - selection: { - fields: { - result: true - } - } - }); - - // Hybrid pending: the generated hook tracks its own; the override path does not. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - const [error, setError] = useState(null); - - async function handleClick() { - setError(null); - - const vars: CrossOriginLinkInput = { - email, - password, - origin: destinationOrigin, - rememberMe - }; - - if (onSubmitOverride) setOverridePending(true); - try { - let token: string; - - if (onSubmitOverride) { - token = await onSubmitOverride(vars); - } else { - // The generated hook takes `{ input }` and returns - // `{ requestCrossOriginToken: { result: string | null } | null }`. - const data = await defaultMutation.mutateAsync({ input: vars }); - const result = data.requestCrossOriginToken?.result ?? null; - if (!result) { - // A resolved mutation with no token is treated as a credential failure. - throw Object.assign(new Error('Invalid email or password.'), { - extensions: { code: 'INVALID_CREDENTIALS' } - }); - } - token = result; - } - - const url = `${destinationOrigin}${destinationPath}?token=${encodeURIComponent(token)}`; - - onMessage?.({ kind: 'success', key: 'crossOriginLink.success', message: merged.successMessage }); - onSuccess?.(token, url); - - // Cross-origin navigation — cannot use router.push (block-contract.md §6). - window.location.href = url; - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - const label = children ?? merged.defaultButtonText; - const loadingLabel = merged.pendingText; - - return ( -
- - - {renderAs === 'link' ? ( - - ) : ( - - {label} - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/cross-origin-link/messages.ts b/apps/blocks/src/blocks/auth/cross-origin-link/messages.ts deleted file mode 100644 index 8dab115..0000000 --- a/apps/blocks/src/blocks/auth/cross-origin-link/messages.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * cross-origin-link — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type CrossOriginLinkMessages = { - defaultButtonText: string; - pendingText: string; - /** Shown when token is generated (before navigation) */ - successMessage: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - INVALID_CREDENTIALS: string; - CROSS_ORIGIN_DISABLED: string; - RATE_LIMITED: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultCrossOriginLinkMessages: CrossOriginLinkMessages = { - defaultButtonText: 'Continue to app', - pendingText: 'Connecting…', - successMessage: 'Redirecting to app…', - errors: { - INVALID_CREDENTIALS: 'Invalid email or password.', - CROSS_ORIGIN_DISABLED: 'Cross-origin authentication is not enabled.', - RATE_LIMITED: 'Too many attempts. Please wait.', - UNKNOWN_ERROR: 'Failed to generate link. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/domain-verification-step/domain-verification-step.tsx b/apps/blocks/src/blocks/auth/domain-verification-step/domain-verification-step.tsx deleted file mode 100644 index 5599ebe..0000000 --- a/apps/blocks/src/blocks/auth/domain-verification-step/domain-verification-step.tsx +++ /dev/null @@ -1,216 +0,0 @@ -'use client'; - -/** - * domain-verification-step (registry: auth-domain-verification-step) - * - * v2 STUB — Phase 3 (deferred SSO backend). - * - * Displays the DNS TXT record an admin must add to prove domain ownership for - * an SSO provider configuration. In production it will poll the server until - * the record is detected or a timeout is reached. The stub renders the static - * UI skeleton and surfaces a clear "deferred" notice so operators understand - * what must be deployed before the block becomes functional. - * - * No generated hook, no @/generated import, no requires.json, no blocks-runtime - * dependency — this block is purely presentational at this stage. - * - * When the SSO backend ships (`constructive_auth_public.get_domain_verification_record` - * + `check_domain_verification`), replace the stub states with live hook calls - * imported from `@/generated/auth` and add `auth-domain-verification-step.requires.json`. - * - * Spec: planning/blocks/auth/auth-domain-verification-step.md - * SDK prerequisite (future): constructive_auth_public.get_domain_verification_record + - * check_domain_verification (backend-spec/v2-sso-scim.md) - */ - -import { useState } from 'react'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Alert, AlertDescription } from '@constructive-io/ui/alert'; - -import { cn } from '@/lib/utils'; - -import { - defaultAuthDomainVerificationStepMessages, - type AuthDomainVerificationStepMessages -} from './messages'; - -// ─── Types ─────────────────────────────────────────────────────────────────── - -/** Verification state machine — mirrors what the live poll implementation will use. */ -export type DomainVerificationStatus = 'waiting' | 'verified' | 'timeout' | 'error'; - -export type AuthDomainVerificationStepMessageOverrides = Partial; - -export type AuthDomainVerificationStepProps = { - /** The SSO provider UUID this domain is being claimed for (required). */ - ssoProviderId: string; - /** The domain being verified (e.g. "acme.com"). */ - domain: string; - /** Polling interval in ms (default 5000 — unused in stub). */ - pollIntervalMs?: number; - /** Max poll duration in ms (default 300_000 — unused in stub). */ - pollTimeoutMs?: number; - messages?: AuthDomainVerificationStepMessageOverrides; - /** Fires when the domain is verified. */ - onVerified?: (ssoProviderId: string) => void; - /** Fires when polling exceeds `pollTimeoutMs`. */ - onTimeout?: () => void; - /** Fires after an error during the verification check. */ - onError?: (err: { message: string; code: string }) => void; - /** Fires to surface a notification to the host application. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// ─── Component ─────────────────────────────────────────────────────────────── - -/** - * AuthDomainVerificationStep - * - * v2 stub — renders the DNS TXT-record verification UI skeleton with a - * "backend deferred" notice. Drop the notice and wire the two generated hooks - * once the SSO procedures ship. - */ -export function AuthDomainVerificationStep({ - ssoProviderId, - domain, - messages: messageOverrides, - onVerified: _onVerified, - onTimeout: _onTimeout, - onError: _onError, - onMessage: _onMessage, - className -}: AuthDomainVerificationStepProps) { - // Deep-merge messages (same pattern as sign-in-card). - const merged: AuthDomainVerificationStepMessages = { - ...defaultAuthDomainVerificationStepMessages, - ...messageOverrides - }; - - // In the live implementation these will be driven by the poll hook. - // The stub always starts in 'waiting' so the skeleton is visible. - const [status] = useState('waiting'); - const [copiedField, setCopiedField] = useState<'name' | 'value' | null>(null); - - // Stable placeholder TXT values — replaced by the real hook response in v2. - const txtRecordName = `_constructive-verify.${domain}`; - const txtRecordValue = `constructive-domain-verification=${ssoProviderId}`; - - function handleCopy(field: 'name' | 'value', value: string) { - navigator.clipboard.writeText(value).then(() => { - setCopiedField(field); - setTimeout(() => setCopiedField(null), 2000); - }); - } - - // The "Check now" button will invoke the check_domain_verification hook in v2. - // In the stub it does nothing (the button is present for layout fidelity). - function handleCheckNow() { - // TODO (v2): call `useCheckDomainVerificationMutation` from `@/generated/auth` - } - - return ( - - -
- {merged.title} - -
- {merged.description} -
- - - {/* Deferred-backend notice — remove once procedures ship */} - - - {merged.deferredNotice} - - - - {/* TXT record name */} - handleCopy('name', txtRecordName)} - /> - - {/* TXT record value */} - handleCopy('value', txtRecordValue)} - /> - - {/* Propagation note */} -

{merged.propagationNote}

- - {/* Manual check trigger */} - -
-
- ); -} - -// ─── Sub-components ─────────────────────────────────────────────────────────── - -function StatusBadge({ - status, - merged -}: { - status: DomainVerificationStatus; - merged: AuthDomainVerificationStepMessages; -}) { - if (status === 'verified') return {merged.statusVerified}; - if (status === 'timeout') return {merged.statusTimeout}; - if (status === 'error') return {merged.statusError}; - return ( - - {merged.statusWaiting} - - ); -} - -function TxtRecordField({ - label, - value, - copyLabel, - onCopy -}: { - label: string; - value: string; - copyLabel: string; - onCopy: () => void; -}) { - return ( -
-

{label}

-
- {value} - -
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/domain-verification-step/messages.ts b/apps/blocks/src/blocks/auth/domain-verification-step/messages.ts deleted file mode 100644 index d9edbe0..0000000 --- a/apps/blocks/src/blocks/auth/domain-verification-step/messages.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * domain-verification-step — message catalog - * - * v2 STUB — no data binding yet (sdk-binding-contract.md: deferred SSO backend). - * Top-level camelCase keys are UI copy. There are no backend error codes in v1 - * because the block performs no network operations. - */ - -export type AuthDomainVerificationStepMessages = { - title: string; - description: string; - txtRecordNameLabel: string; - txtRecordValueLabel: string; - copyLabel: string; - copiedLabel: string; - checkNowLabel: string; - statusWaiting: string; - statusVerified: string; - statusTimeout: string; - statusError: string; - deferredNotice: string; - propagationNote: string; -}; - -export const defaultAuthDomainVerificationStepMessages: AuthDomainVerificationStepMessages = { - title: 'Verify domain ownership', - description: - 'Add the DNS TXT record below to your domain to prove ownership. DNS changes can take up to 48 hours to propagate.', - txtRecordNameLabel: 'TXT record name', - txtRecordValueLabel: 'TXT record value', - copyLabel: 'Copy', - copiedLabel: 'Copied!', - checkNowLabel: 'Check now', - statusWaiting: 'Waiting for DNS propagation…', - statusVerified: 'Domain verified.', - statusTimeout: 'Verification timed out. Check that the TXT record was added correctly.', - statusError: 'Verification error. Please try again.', - deferredNotice: 'Domain verification requires a server-side DNS backend that has not been deployed yet.', - propagationNote: 'DNS propagation can take up to 48 hours. The check button lets you verify manually at any time.' -}; diff --git a/apps/blocks/src/blocks/auth/email-otp-input/auth-email-otp-input.requires.json b/apps/blocks/src/blocks/auth/email-otp-input/auth-email-otp-input.requires.json deleted file mode 100644 index f488b86..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-input/auth-email-otp-input.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["signInEmailOtp", "sendEmailOtp"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/email-otp-input/email-otp-input.tsx b/apps/blocks/src/blocks/auth/email-otp-input/email-otp-input.tsx deleted file mode 100644 index 7ffb977..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-input/email-otp-input.tsx +++ /dev/null @@ -1,466 +0,0 @@ -'use client'; - -/** - * email-otp-input (registry: auth-email-otp-input) - * - * Reusable 6-segment OTP code input with countdown timer, resend CTA, and - * attempt feedback. Designed to be rendered inline by [[auth-email-otp-request-card]] - * or as a standalone block. - * - * BACKEND-PENDING — CASE (b): - * `sign_in_email_otp` and `send_email_otp` are not yet deployed in - * `constructive_auth_public`. `useSignInEmailOtpMutation` and - * `useSendEmailOtpMutation` do NOT exist in the reference SDK (confirmed in - * apps/admin/src/graphql/auth-sdk/api/hooks/mutations/). This block therefore: - * - * • Does NOT import from `@/generated/auth` (no hooks to import — tsc would fail). - * • Makes `onVerify` the primary/recommended network path: the host wires the - * generated binding after running `cnc codegen --api-names auth ...`. - * • `onResend` is similarly the primary resend path until `send_email_otp` ships. - * • Stub default paths throw typed PROCEDURE_NOT_FOUND errors so the block - * behaves gracefully (shows the error message) if mounted without an override. - * • `requires.json` names the pending ops so `check-sdk-fixtures.ts` fails clearly. - * • `PROCEDURE_NOT_FOUND` is in `messages.errors`. - * - * When the backend ships and the host regenerates the SDK, replace the stubs with: - * import { useSignInEmailOtpMutation, useSendEmailOtpMutation } from '@/generated/auth'; - * const defaultMutation = useSignInEmailOtpMutation({ - * selection: { - * fields: { - * id: true, userId: true, accessToken: true, accessTokenExpiresAt: true, - * isVerified: true, mfaRequired: true, mfaChallengeToken: true, - * }, - * }, - * }); - * const sendMutation = useSendEmailOtpMutation({ selection: {} }); - * // Submit: const result = await defaultMutation.mutateAsync({ email, code }).then((d) => d.signInEmailOtp); - * // Resend: await sendMutation.mutateAsync({ email, type: 'sign_in' }); - * and add the hybrid-isPending pattern (sdk-binding-contract.md §5). - * - * Pairing: No page block — used as an inline code-entry step rendered by - * [[auth-email-otp-request-card]] or embedded in a custom page. - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; - -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; - -import { defaultEmailOtpInputMessages, type EmailOtpInputMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const DEFAULT_LENGTH = 6; -const DEFAULT_RESEND_COOLDOWN_SECONDS = 60; - -// --------------------------------------------------------------------------- -// Simple {{key}} mustache interpolation (no dep needed) -// --------------------------------------------------------------------------- - -function interpolate(template: string, vars: Record): string { - return template.replace(/\{\{(\w+)\}\}/g, (_, key) => String(vars[key] ?? '')); -} - -// --------------------------------------------------------------------------- -// Backend-pending stubs -// Throw PROCEDURE_NOT_FOUND so the error message surfaces in the UI. -// Replace these with the generated hooks once the procedures ship. -// --------------------------------------------------------------------------- - -class ProcedureNotFoundError extends Error { - public readonly extensions = { code: 'PROCEDURE_NOT_FOUND' }; - constructor() { - super('PROCEDURE_NOT_FOUND'); - this.name = 'ProcedureNotFoundError'; - } -} - -async function stubVerify(_email: string, _code: string): Promise { - throw new ProcedureNotFoundError(); -} - -async function stubResend(_email: string): Promise { - throw new ProcedureNotFoundError(); -} - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -/** - * Result of an OTP verification. Mirrors the `signInEmailOtp` payload shape - * (the fields this block selects); declared here so the public surface and - * the `onVerify` override do not depend on a generated type name. - */ -export type EmailOtpVerifyResult = { - id?: string | null; - userId?: string | null; - accessToken?: string | null; - accessTokenExpiresAt?: string | null; - isVerified?: boolean | null; - mfaRequired?: boolean | null; - mfaChallengeToken?: string | null; - /** For non-sign-in flows: simple success boolean. */ - success?: boolean; -}; - -/** - * Message overrides. Top-level copy is shallow-partial; `errors` is itself - * partial so a host can localize a single error code without restating the map. - */ -export type EmailOtpInputMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type EmailOtpInputProps = { - /** Email the OTP was sent to (required for the default sign-in hook). */ - email: string; - /** Number of OTP segments. Default: 6 */ - length?: number; - /** Countdown timer duration in seconds before resend is enabled. Default: 60 */ - resendCooldownSeconds?: number; - messages?: EmailOtpInputMessageOverrides; - /** - * Custom verify function. Required until `sign_in_email_otp` ships. - * After codegen, the host wires in `useSignInEmailOtpMutation`. - * Use for non-sign-in OTP types (verify, reset, change-email). - */ - onVerify?: (email: string, code: string) => Promise; - /** - * Custom resend function. Required until `send_email_otp` ships. - * After codegen, the host wires in `useSendEmailOtpMutation`. - */ - onResend?: (email: string) => Promise; - /** Fires after a successful verification. Always fires. */ - onSuccess?: (result: EmailOtpVerifyResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for all events. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function EmailOtpInput({ - email, - length = DEFAULT_LENGTH, - resendCooldownSeconds = DEFAULT_RESEND_COOLDOWN_SECONDS, - messages: messageOverrides, - onVerify: onVerifyOverride, - onResend: onResendOverride, - onSuccess, - onError, - onMessage, - className -}: EmailOtpInputProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: EmailOtpInputMessages = { - ...defaultEmailOtpInputMessages, - ...messageOverrides, - errors: { ...defaultEmailOtpInputMessages.errors, ...messageOverrides?.errors } - }; - - // OTP digits state — array of `length` single-character strings. - const [digits, setDigits] = useState(() => Array(length).fill('')); - const inputRefs = useRef>(Array(length).fill(null)); - - // Pending / error state. - const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); - - // Resend state. - const [isResendPending, setIsResendPending] = useState(false); - const [resendSuccess, setResendSuccess] = useState(false); - - // Countdown timer. - const [cooldownRemaining, setCooldownRemaining] = useState(0); - - // --------------------------------------------------------------------------- - // Helpers - // --------------------------------------------------------------------------- - - const verifyFn = onVerifyOverride ?? stubVerify; - const resendFn = onResendOverride ?? stubResend; - - const startCooldown = useCallback(() => { - setCooldownRemaining(resendCooldownSeconds); - }, [resendCooldownSeconds]); - - useEffect(() => { - if (cooldownRemaining <= 0) return; - const timeout = setTimeout(() => { - setCooldownRemaining((seconds) => Math.max(0, seconds - 1)); - }, 1000); - return () => clearTimeout(timeout); - }, [cooldownRemaining]); - - // Auto-focus the first input on mount. - useEffect(() => { - inputRefs.current[0]?.focus(); - }, []); - - // --------------------------------------------------------------------------- - // Submit handler - // --------------------------------------------------------------------------- - - const handleVerify = useCallback( - async (code: string) => { - if (isPending) return; - setError(null); - setIsPending(true); - try { - const result = await verifyFn(email, code); - - if (result.mfaRequired) { - onMessage?.({ kind: 'warning', key: 'mfaRequired' }); - } else { - onMessage?.({ kind: 'success', key: 'signInEmailOtp.success' }); - } - onSuccess?.(result); - } catch (err) { - const { code: errCode, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = errCode ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setIsPending(false); - } - }, - [isPending, verifyFn, email, merged.errors, onMessage, onSuccess, onError] - ); - - // --------------------------------------------------------------------------- - // Digit input handling - // --------------------------------------------------------------------------- - - function updateDigit(index: number, value: string) { - const next = [...digits]; - next[index] = value; - setDigits(next); - return next; - } - - function focusNext(currentIndex: number) { - if (currentIndex < length - 1) { - inputRefs.current[currentIndex + 1]?.focus(); - } - } - - function focusPrev(currentIndex: number) { - if (currentIndex > 0) { - inputRefs.current[currentIndex - 1]?.focus(); - } - } - - function handleDigitChange(index: number, value: string) { - // Strip non-digits. - const digit = value.replace(/\D/g, '').slice(-1); - const nextDigits = updateDigit(index, digit); - - if (digit) { - focusNext(index); - // Auto-submit when all segments are filled. - if (nextDigits.every((d) => d !== '')) { - const code = nextDigits.join(''); - handleVerify(code); - } - } - } - - function handleKeyDown(index: number, e: React.KeyboardEvent) { - if (e.key === 'Backspace') { - if (digits[index]) { - updateDigit(index, ''); - } else { - focusPrev(index); - } - } else if (e.key === 'ArrowLeft') { - e.preventDefault(); - focusPrev(index); - } else if (e.key === 'ArrowRight') { - e.preventDefault(); - focusNext(index); - } - } - - function handlePaste(e: React.ClipboardEvent) { - e.preventDefault(); - const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, length); - if (!pasted) return; - - const nextDigits = Array(length) - .fill('') - .map((_, i) => pasted[i] ?? ''); - setDigits(nextDigits); - - // Focus the next empty slot or the last filled one. - const filledCount = pasted.length; - const focusIndex = Math.min(filledCount, length - 1); - inputRefs.current[focusIndex]?.focus(); - - // Auto-submit if we pasted a full code. - if (filledCount >= length) { - handleVerify(pasted.slice(0, length)); - } - } - - // --------------------------------------------------------------------------- - // Form submit (manual — fallback for accessibility) - // --------------------------------------------------------------------------- - - function handleFormSubmit(e: React.FormEvent) { - e.preventDefault(); - const code = digits.join(''); - if (code.length < length) return; - handleVerify(code); - } - - // --------------------------------------------------------------------------- - // Resend handler - // --------------------------------------------------------------------------- - - async function handleResend() { - if (isResendPending || cooldownRemaining > 0) return; - setError(null); - setResendSuccess(false); - setIsResendPending(true); - try { - await resendFn(email); - setResendSuccess(true); - setDigits(Array(length).fill('')); - inputRefs.current[0]?.focus(); - startCooldown(); - onMessage?.({ kind: 'info', key: 'sendEmailOtp.success', message: merged.resendSuccess }); - } catch (err) { - const { code: errCode, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = errCode ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setIsResendPending(false); - } - } - - // --------------------------------------------------------------------------- - // Resend button label - // --------------------------------------------------------------------------- - - function resendButtonLabel(): string { - if (isResendPending) return merged.resendPending; - if (cooldownRemaining > 0) return interpolate(merged.resendCooldown, { seconds: cooldownRemaining }); - return merged.resendButton; - } - - const isResendDisabled = isResendPending || cooldownRemaining > 0; - const isSubmitDisabled = isPending || digits.join('').length < length; - - // --------------------------------------------------------------------------- - // JSX - // --------------------------------------------------------------------------- - - return ( - - - {merged.title} - - {interpolate(merged.description, { email })} - - - - - - - {resendSuccess && ( -

- {merged.resendSuccess} -

- )} - -
-
- {merged.inputLabel} - - {/* OTP segment inputs */} -
- {digits.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - pattern="[0-9]*" - maxLength={1} - value={digit} - aria-label={`Digit ${index + 1} of ${length}`} - data-testid={`otp-digit-${index}`} - autoComplete={index === 0 ? 'one-time-code' : 'off'} - className={cn( - 'h-12 w-10 rounded-md border text-center text-lg font-semibold', - 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1', - 'transition-colors', - digit ? 'border-ring' : 'border-input', - 'bg-background text-foreground' - )} - onChange={(e) => handleDigitChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={handlePaste} - /> - ))} -
-
- - - {merged.submitButton} - -
- - {/* Resend CTA */} -
- -
-
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/email-otp-input/messages.ts b/apps/blocks/src/blocks/auth/email-otp-input/messages.ts deleted file mode 100644 index 25240bf..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-input/messages.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * email-otp-input — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and is passed straight to `parseGraphQLError` - * as `customMessages`. - * - * Runtime interpolation tokens: - * description → {{email}} - * resendCooldown → {{seconds}} - * - * PROCEDURE_NOT_FOUND is included because `sign_in_email_otp` and - * `send_email_otp` are backend-pending (sdk-binding-contract.md §10). - */ - -export type EmailOtpInputMessages = { - title: string; - /** Runtime interpolation: {{email}} */ - description: string; - inputLabel: string; - submitButton: string; - submitButtonPending: string; - resendButton: string; - resendPending: string; - /** Runtime interpolation: {{seconds}} */ - resendCooldown: string; - resendSuccess: string; - errors: { - INVALID_OTP: string; - EXPIRED_TOKEN: string; - RATE_LIMITED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultEmailOtpInputMessages: EmailOtpInputMessages = { - title: 'Enter your code', - description: 'We sent a 6-digit code to {{email}}.', - inputLabel: 'One-time code', - submitButton: 'Verify', - submitButtonPending: 'Verifying…', - resendButton: 'Resend code', - resendPending: 'Resending…', - resendCooldown: 'Resend in {{seconds}}s', - resendSuccess: 'Code resent. Check your inbox.', - errors: { - INVALID_OTP: 'Invalid code. Please check and try again.', - EXPIRED_TOKEN: 'This code has expired. Please request a new one.', - RATE_LIMITED: 'Too many attempts. Please wait before trying again.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/email-otp-request-card/auth-email-otp-request-card.requires.json b/apps/blocks/src/blocks/auth/email-otp-request-card/auth-email-otp-request-card.requires.json deleted file mode 100644 index b68e34d..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-request-card/auth-email-otp-request-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["sendEmailOtp"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/email-otp-request-card/email-otp-request-card.tsx b/apps/blocks/src/blocks/auth/email-otp-request-card/email-otp-request-card.tsx deleted file mode 100644 index d445b46..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-request-card/email-otp-request-card.tsx +++ /dev/null @@ -1,374 +0,0 @@ -'use client'; - -/** - * email-otp-request-card (registry: auth-email-otp-request-card) - * - * Email-only form that sends a one-time passcode to the user's email. On success - * it transitions to a "code sent" confirmation panel within the same card - * — NO navigation/redirect. The confirmation copy interpolates the submitted - * email address. A "Resend code" button in the confirmed state calls the same path. - * - * BACKEND-PENDING — CASE (b): `send_email_otp` is not yet deployed in - * `constructive_auth_public` and the generated `useSendEmailOtpMutation` hook - * does NOT exist in the reference SDK. This block therefore: - * • Does NOT import from `@/generated/auth` (no hook to import — tsc would fail). - * • Makes `onSubmit` the primary/required network path: the host wires the - * generated binding after running `cnc codegen --api-names auth ...`. - * • The stub default path throws a typed PROCEDURE_NOT_FOUND error so the - * block behaves gracefully (shows the error message) if accidentally mounted - * without the override. - * • `requires.json` names the pending op so `check-sdk-fixtures.ts` fails clearly. - * • `PROCEDURE_NOT_FOUND` is in `messages.errors`. - * - * When the backend ships and the host regenerates the SDK, replace the stub - * `defaultRunSend` with: - * import { useSendEmailOtpMutation } from '@/generated/auth'; - * const defaultMutation = useSendEmailOtpMutation({ selection: { fields: { clientMutationId: true } } }); - * const [overridePending, setOverridePending] = useState(false); - * const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - * async function defaultRunSend(vars: EmailOtpRequestVars): Promise { - * await defaultMutation.mutateAsync({ input: vars }).then((d) => d.sendEmailOtp); - * } - * and gate setOverridePending on onSubmitOverride (see sign-in-card.tsx). - * - * (`send_email_otp` returns void — no payload fields to select.) - * - * NO QueryClientProvider, NO configure(), NO fetch, NO GraphQL document strings. - * The host mounts `blocks-runtime` once at app root; that is the single wiring - * point. This block joins it as soon as the generated hook ships. - */ - -import { useRef, useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { forgotPasswordSchema, type ForgotPasswordFormData } from '@/blocks/lib/schemas'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; -import { EmailOtpInput } from '../email-otp-input/email-otp-input'; - -import { defaultEmailOtpRequestCardMessages, type EmailOtpRequestCardMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -/** - * OTP type discriminator. Passed as the `type` param to send_email_otp. - * Server-defined semantics; the block surfaces them via `otpType`. - */ -export type OtpType = 'sign_in' | 'verify' | 'reset' | 'change_email'; - -/** Variables the send-OTP call receives. The `onSubmit` override gets these verbatim. */ -export type EmailOtpRequestVars = { - email: string; - type: OtpType; -}; - -/** - * Message overrides. Top-level copy is shallow-partial; `errors` is itself - * partial so a host can localize a single error code without restating the map. - */ -export type EmailOtpRequestCardMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type EmailOtpRequestCardProps = { - /** - * OTP type discriminator. Passed as 'type' param to send_email_otp. - * Default: 'sign_in' - */ - otpType?: OtpType; - /** Pre-fill the email field (e.g. from a query param). */ - defaultEmail?: string; - /** - * When true (default), renders [[auth-email-otp-input]] inline in the - * code-sent state, passing `email` down for code entry. - * When false, only shows confirmation message + resend button; the host - * handles navigation to code entry via `onSuccess`. - * Default: true - */ - showOtpInputInline?: boolean; - messages?: EmailOtpRequestCardMessageOverrides; - /** Href for the back-to-sign-in link. Rendered as plain `` when provided. */ - signInHref?: string; - /** - * Replace the default mutation call. Receives the same vars. - * - * BACKEND-PENDING: Until `send_email_otp` is deployed and the host has - * regenerated its auth SDK, this prop is the ONLY way to wire a real network - * call. After codegen, the host may drop this prop and let the generated hook - * (`useSendEmailOtpMutation`) take over via `blocks-runtime`. - */ - onSubmit?: (vars: EmailOtpRequestVars) => Promise; - /** Fires after a resolved send-OTP call. Always fires on success. */ - onSuccess?: (vars: { email: string }) => void; - /** Fires after a mapped error. Always fires on error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and resend. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -type CardState = 'form' | 'code-sent'; - -/** Replaces all `{{email}}` tokens in a template string. */ -function interpolateEmail(template: string, email: string): string { - return template.replace(/\{\{email\}\}/g, email); -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function EmailOtpRequestCard({ - otpType = 'sign_in', - defaultEmail, - showOtpInputInline = true, - messages: messageOverrides, - signInHref, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: EmailOtpRequestCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: EmailOtpRequestCardMessages = { - ...defaultEmailOtpRequestCardMessages, - ...messageOverrides, - errors: { ...defaultEmailOtpRequestCardMessages.errors, ...messageOverrides?.errors } - }; - - // --------------------------------------------------------------------------- - // BACKEND-PENDING stub (CASE b) - // - // The generated `useSendEmailOtpMutation` does not exist yet. We provide a - // stub that throws PROCEDURE_NOT_FOUND so the block is self-describing when - // mounted without an `onSubmit` override. Replace this section with the real - // generated hook once the proc ships and the host regenerates its auth SDK. - // --------------------------------------------------------------------------- - const [overridePending, setOverridePending] = useState(false); - - // Hybrid pending: when override is provided, track it; otherwise the stub always - // returns synchronously (PROCEDURE_NOT_FOUND), so pending is always false. - const isPending = onSubmitOverride ? overridePending : false; - - // Resend has its own pending state; it reuses the same run path. - const [resendPending, setResendPending] = useState(false); - - const [error, setError] = useState(null); - const [cardState, setCardState] = useState('form'); - const submittedEmailRef = useRef(''); - const confirmationFocusRef = useRef(null); - - async function defaultRunSend(_vars: EmailOtpRequestVars): Promise { - // Throw a typed PROCEDURE_NOT_FOUND error so parseGraphQLError maps it to - // the human-readable message in merged.errors. - const err = Object.assign(new Error(merged.errors.PROCEDURE_NOT_FOUND), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - throw err; - } - - async function runSend(vars: EmailOtpRequestVars): Promise { - if (onSubmitOverride) return onSubmitOverride(vars); - return defaultRunSend(vars); - } - - async function handleSubmit(values: ForgotPasswordFormData) { - setError(null); - if (onSubmitOverride) setOverridePending(true); - try { - forgotPasswordSchema.parse(values); - await runSend({ email: values.email, type: otpType }); - submittedEmailRef.current = values.email; - setCardState('code-sent'); - onMessage?.({ kind: 'success', key: 'emailOtpRequest.success' }); - onSuccess?.({ email: values.email }); - // Move focus to the confirmation panel for accessibility. - setTimeout(() => confirmationFocusRef.current?.focus(), 0); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - async function handleResend() { - setResendPending(true); - try { - await runSend({ email: submittedEmailRef.current, type: otpType }); - onMessage?.({ kind: 'info', key: 'emailOtpRequest.resend', message: merged.resendSuccess }); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setResendPending(false); - } - } - - const form = useForm({ - defaultValues: { - email: defaultEmail ?? '' - } as ForgotPasswordFormData, - onSubmit: async ({ value }) => { - await handleSubmit(value); - } - }); - - // --------------------------------------------------------------------------- - // Code-sent state (confirmed) - // --------------------------------------------------------------------------- - - if (cardState === 'code-sent') { - // When showOtpInputInline is true (default), render the OTP input inline. - // The EmailOtpInput block is a full card itself — render it standalone, not - // nested inside another Card, to avoid double-card appearance. - if (showOtpInputInline) { - return ( - onSuccess?.({ email: submittedEmailRef.current })} - onError={onError} - onMessage={onMessage} - className={className} - /> - ); - } - - // showOtpInputInline=false: show confirmation + resend only; host navigates. - return ( - - - {/* Block-owned focusable anchor — CardTitle does not forward its ref. */} -
- {merged.title} -
- - {interpolateEmail(merged.codeSentMessage, submittedEmailRef.current)} - -
- - - - {merged.resendButton} - - - - {signInHref && ( - -
- - )} - - ); - } - - // --------------------------------------------------------------------------- - // Form state - // --------------------------------------------------------------------------- - - return ( - - - {merged.title} - {merged.description} - - - - - -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - { - if (!value) return 'Email is required'; - if (!/\S+@\S+\.\S+/.test(value)) return 'Please enter a valid email'; - return undefined; - } - }} - > - {(field) => ( - - )} - - -
- - {merged.submitButton} - -
-
-
- - {signInHref && ( - - - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/email-otp-request-card/messages.ts b/apps/blocks/src/blocks/auth/email-otp-request-card/messages.ts deleted file mode 100644 index 995e779..0000000 --- a/apps/blocks/src/blocks/auth/email-otp-request-card/messages.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * email-otp-request-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * BACKEND-PENDING (CASE b): `send_email_otp` is not yet deployed in - * `constructive_auth_public`. The `PROCEDURE_NOT_FOUND` key is required because - * the block's stub path throws this code when no `onSubmit` override is provided - * (sdk-binding-contract.md §10 — backend-pending block: requires.json names - * the absent op, messages.errors.PROCEDURE_NOT_FOUND surfaces the gap). - * - * `{{email}}` in `codeSentMessage` is substituted by `interpolateEmail()` in - * the component (no external interpolation lib needed — the block co-locates it). - */ - -export type EmailOtpRequestCardMessages = { - /** Card title for the initial form state. */ - title: string; - /** Card description for the initial form state. */ - description: string; - emailLabel: string; - emailPlaceholder: string; - submitButton: string; - submitButtonPending: string; - /** - * Shown in the confirmed state. Runtime interpolation: {{email}} - * Replace `{{email}}` with the submitted address before rendering. - */ - codeSentMessage: string; - resendButton: string; - resendPending: string; - resendSuccess: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - RATE_LIMITED: string; - CAPTCHA_FAILED: string; - EMAIL_OTP_DISABLED: string; - /** Required: surfaced when the backend procedure is not yet deployed (CASE b). */ - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultEmailOtpRequestCardMessages: EmailOtpRequestCardMessages = { - title: 'Sign in with a code', - description: "Enter your email and we'll send you a one-time code.", - emailLabel: 'Email', - emailPlaceholder: 'you@example.com', - submitButton: 'Send code', - submitButtonPending: 'Sending…', - codeSentMessage: 'We sent a 6-digit code to {{email}}. Enter it below.', - resendButton: 'Resend code', - resendPending: 'Resending…', - resendSuccess: 'Code resent.', - errors: { - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - CAPTCHA_FAILED: 'Captcha verification failed. Please try again.', - EMAIL_OTP_DISABLED: 'Email OTP sign-in is not enabled.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.', - }, -}; diff --git a/apps/blocks/src/blocks/auth/forgot-password-card/auth-forgot-password-card.requires.json b/apps/blocks/src/blocks/auth/forgot-password-card/auth-forgot-password-card.requires.json deleted file mode 100644 index 12e252b..0000000 --- a/apps/blocks/src/blocks/auth/forgot-password-card/auth-forgot-password-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["forgotPassword"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/forgot-password-card/forgot-password-card.tsx b/apps/blocks/src/blocks/auth/forgot-password-card/forgot-password-card.tsx deleted file mode 100644 index af705bb..0000000 --- a/apps/blocks/src/blocks/auth/forgot-password-card/forgot-password-card.tsx +++ /dev/null @@ -1,282 +0,0 @@ -'use client'; - -/** - * forgot-password-card (registry: auth-forgot-password-card) - * - * Email-only form that initiates the password reset flow. On success it - * transitions to a "check your email" confirmation panel within the same card - * — NO navigation/redirect. The confirmation copy interpolates the submitted - * email address. - * - * Data path: the generated `useForgotPasswordMutation` hook imported from - * `@/generated/auth`. `forgot_password` returns no domain object, so the hook - * selects the payload's `clientMutationId`. The mutation variables wrap the email - * in `{ input: { email } }` (confirmed from generated ForgotPasswordVariables). - * - * Override seam: `onSubmit` fully replaces the generated-hook call. - * Resend: the "Resend email" button in the confirmed panel calls the same hook. - */ - -import { useRef, useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { useForgotPasswordMutation } from '@/generated/auth'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { forgotPasswordSchema, type ForgotPasswordFormData } from '@/blocks/lib/schemas'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; - -import { defaultForgotPasswordCardMessages, type ForgotPasswordCardMessages } from './messages'; - -/** The input shape for the forgot-password call. The override `onSubmit` gets this verbatim. */ -export type ForgotPasswordVars = { - email: string; -}; - -type CardState = 'form' | 'confirmed'; - -/** - * Message overrides. Top-level copy is shallow-partial; `errors` is itself - * partial so a host can localize a single error code without restating the map. - */ -export type ForgotPasswordCardMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type ForgotPasswordCardProps = { - /** Pre-fill the email field (e.g. from a query param). */ - defaultEmail?: string; - /** Show a "Back to sign in" link. Default: true. */ - showBackLink?: boolean; - /** Href for the back-to-sign-in link. Rendered as plain `` when provided. */ - signInHref?: string; - messages?: ForgotPasswordCardMessageOverrides; - /** Replace the default `useForgotPasswordMutation` call. Receives the same vars. */ - onSubmit?: (vars: ForgotPasswordVars) => Promise; - /** Fires after a resolved forgot-password request. Always fires on success. */ - onSuccess?: (vars: ForgotPasswordVars) => void; - /** Fires after a mapped error. Always fires on error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and resend. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -/** Replaces all `{{email}}` tokens in a template string. */ -function interpolateEmail(template: string, email: string): string { - return template.replace(/\{\{email\}\}/g, email); -} - -export function ForgotPasswordCard({ - defaultEmail, - showBackLink = true, - signInHref, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: ForgotPasswordCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: ForgotPasswordCardMessages = { - ...defaultForgotPasswordCardMessages, - ...messageOverrides, - errors: { ...defaultForgotPasswordCardMessages.errors, ...messageOverrides?.errors } - }; - - // PostGraphile mutation payloads are composite objects and require at least - // one selected field even when the procedure itself returns void. - const defaultMutation = useForgotPasswordMutation({ - selection: { fields: { clientMutationId: true } } - }); - - // Hybrid pending: generated hook tracks its own; override path uses local state. - const [overridePending, setOverridePending] = useState(false); - const isPending = onSubmitOverride ? overridePending : defaultMutation.isPending; - - // Resend has its own pending state; it reuses the same mutation. - const [resendPending, setResendPending] = useState(false); - - const [error, setError] = useState(null); - const [cardState, setCardState] = useState('form'); - // Track the submitted email so the confirmation panel can show it. - const submittedEmailRef = useRef(''); - - // Block-owned ref on a plain div so focus reliably lands regardless of - // whether CardTitle forwards its ref (it does not in the current UI package). - const confirmationFocusRef = useRef(null); - - async function runForgotPassword(vars: ForgotPasswordVars): Promise { - if (onSubmitOverride) return onSubmitOverride(vars); - await defaultMutation.mutateAsync({ input: { email: vars.email } }).then((d) => d.forgotPassword); - } - - async function handleSubmit(values: ForgotPasswordFormData) { - setError(null); - if (onSubmitOverride) setOverridePending(true); - try { - forgotPasswordSchema.parse(values); - await runForgotPassword({ email: values.email }); - submittedEmailRef.current = values.email; - setCardState('confirmed'); - onMessage?.({ kind: 'success', key: 'forgotPassword.success' }); - onSuccess?.({ email: values.email }); - // Move focus to the confirmation panel for accessibility. - setTimeout(() => confirmationFocusRef.current?.focus(), 0); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - async function handleResend() { - setResendPending(true); - try { - await runForgotPassword({ email: submittedEmailRef.current }); - onMessage?.({ kind: 'info', key: 'forgotPassword.resend', message: merged.resendSuccessMessage }); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setResendPending(false); - } - } - - const form = useForm({ - defaultValues: { - email: defaultEmail ?? '' - } as ForgotPasswordFormData, - onSubmit: async ({ value }) => { - await handleSubmit(value); - } - }); - - if (cardState === 'confirmed') { - return ( - - - {/* Block-owned focusable anchor — CardTitle does not forward its ref. */} -
- {merged.confirmationTitle} -
- - {interpolateEmail(merged.confirmationDescription, submittedEmailRef.current)} - -
- - - - {merged.resendLabel} - - - - {showBackLink && signInHref && ( - -
- - )} - - ); - } - - return ( - - - {merged.title} - {merged.description} - - - - - -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - { - if (!value) return 'Email is required'; - if (!/\S+@\S+\.\S+/.test(value)) return 'Please enter a valid email'; - return undefined; - } - }} - > - {(field) => ( - - )} - - -
- - {merged.submitLabel} - -
-
-
- - {showBackLink && signInHref && ( - - - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/forgot-password-card/messages.ts b/apps/blocks/src/blocks/auth/forgot-password-card/messages.ts deleted file mode 100644 index e493422..0000000 --- a/apps/blocks/src/blocks/auth/forgot-password-card/messages.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * forgot-password-card — message catalog - * - * Canonical block-messages pattern: top-level camelCase keys are UI copy; the - * nested `errors` map is keyed by backend error CODE (UPPER_SNAKE_CASE) and is - * handed straight to `parseGraphQLError` as `customMessages`, so a host - * localizes any code by overriding a single key. - * - * `{{email}}` in `confirmationDescription` is a runtime interpolation token - * replaced by the block at render time. - */ - -export type ForgotPasswordCardMessages = { - title: string; - description: string; - emailLabel: string; - emailPlaceholder: string; - submitLabel: string; - loadingLabel: string; - backToSignInLabel: string; - /** Confirmation panel copy */ - confirmationTitle: string; - /** May contain {{email}} token — replaced at render. */ - confirmationDescription: string; - resendLabel: string; - resendLoadingLabel: string; - resendSuccessMessage: string; - errors: { - RATE_LIMITED: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultForgotPasswordCardMessages: ForgotPasswordCardMessages = { - title: 'Forgot your password?', - description: "Enter your email address and we'll send you a reset link.", - emailLabel: 'Email', - emailPlaceholder: 'you@example.com', - submitLabel: 'Send reset link', - loadingLabel: 'Sending…', - backToSignInLabel: '← Back to sign in', - confirmationTitle: 'Check your email', - confirmationDescription: - "If an account exists for {{email}}, you'll receive a password reset link shortly.", - resendLabel: 'Resend email', - resendLoadingLabel: 'Resending…', - resendSuccessMessage: 'Email resent.', - errors: { - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/forgot-password-page/forgot-password-page.tsx b/apps/blocks/src/blocks/auth/forgot-password-page/forgot-password-page.tsx deleted file mode 100644 index 8e97418..0000000 --- a/apps/blocks/src/blocks/auth/forgot-password-page/forgot-password-page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -/** - * forgot-password-page (registry: auth-forgot-password-page) - * - * Thin Next.js 15 page that composes [[auth-forgot-password-card]] inside a - * centered layout. This is the page-glue layer (block-contract.md §2): - * - * • Reads `?email=` from searchParams and passes it to the card as - * `defaultEmail` (reduces friction when navigated from a sign-in form - * that already knows the typed email). - * • Provides a centered `
` layout (fulfilling the layout-kit - * accessibility requirement — landmark
). - * • No navigation on success — the card transitions to its own confirmed - * state internally; this page does not redirect. - * • Imports `next/navigation` — this is CORRECT for a page block. - * Card blocks NEVER import it; page blocks always do (block-contract §2). - * - * The block calls NO data hooks directly. All data logic lives in the card. - * This block ships NO requires.json (presentational page glue, no generated hook). - * - * Configurable constants at the top of the installed file: - * SIGN_IN_PATH — href rendered inside the card's "Back to sign in" link. - */ - -import { useSearchParams } from 'next/navigation'; - -import { ForgotPasswordCard } from '@/blocks/auth/forgot-password-card/forgot-password-card'; - -// Editable constants in the installed page: -const SIGN_IN_PATH = '/auth/sign-in'; - -export default function ForgotPasswordPage() { - const searchParams = useSearchParams(); - const email = searchParams.get('email') ?? undefined; - - return ( -
- -
- ); -} diff --git a/apps/blocks/src/blocks/auth/invitation-acceptance-card/auth-invitation-acceptance-card.requires.json b/apps/blocks/src/blocks/auth/invitation-acceptance-card/auth-invitation-acceptance-card.requires.json deleted file mode 100644 index c3e242f..0000000 --- a/apps/blocks/src/blocks/auth/invitation-acceptance-card/auth-invitation-acceptance-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "admin", - "mutations": ["submitAppInviteCode", "submitOrgInviteCode"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/invitation-acceptance-card/invitation-acceptance-card.tsx b/apps/blocks/src/blocks/auth/invitation-acceptance-card/invitation-acceptance-card.tsx deleted file mode 100644 index a934b64..0000000 --- a/apps/blocks/src/blocks/auth/invitation-acceptance-card/invitation-acceptance-card.tsx +++ /dev/null @@ -1,352 +0,0 @@ -'use client'; - -/** - * invitation-acceptance-card (registry: auth-invitation-acceptance-card) - * - * Card for accepting or declining an invite (app-level or org-level). Calls - * the host's generated `useSubmitAppInviteCodeMutation` or - * `useSubmitOrgInviteCodeMutation` from `@/generated/admin` — both ops live - * in `invites_public` → namespace `admin`. - * - * Binding doctrine (sdk-binding-contract.md §2, §5–§7): - * • Data path = generated React-Query hooks, no fetch / GraphQL document. - * • NO client bootstrap — blocks-runtime wires the QueryClient + configure(). - * • Override seam: `onSubmit` fully replaces the mutation calls. - * • Error mapping via auth-errors; inline alert only (no toast in v1). - * - * Both mutations take `{ input: { token?: string } }` and return a payload - * with `{ result?: boolean | null }`. A boolean `true` means accepted. - */ - -import { useState } from 'react'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Badge } from '@constructive-io/ui/badge'; -import { Separator } from '@constructive-io/ui/separator'; - -import { cn } from '@/lib/utils'; -import { useSubmitAppInviteCodeMutation, useSubmitOrgInviteCodeMutation } from '@/generated/admin'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { UserAvatar, type UserAvatarUser } from '@/blocks/user/user-avatar/user-avatar'; - -import { - defaultInvitationAcceptanceMessages, - type InvitationAcceptanceMessageOverrides, - type InvitationAcceptanceMessages -} from './messages'; - -// ── Types ──────────────────────────────────────────────────────────────────── - -/** Minimal invite metadata surfaced as props (caller parses URL/token). */ -export type InviteMetadata = { - /** The invitation token from the URL. */ - token: string; - /** App-level or org-level invite. */ - kind: 'app' | 'org'; - /** Inviter's user record. Optional — shown in org invites. */ - inviter?: UserAvatarUser | null; - /** The org being joined. Required when kind === 'org'. */ - org?: UserAvatarUser | null; - /** Human-readable role label (e.g. "Member", "Admin"). */ - role?: string | null; - /** ISO-8601 expiry timestamp — for display only. */ - expiresAt?: string | null; -}; - -/** Result returned from the accept action and passed to `onSuccess`. */ -export type InviteAcceptResult = { - kind: 'app' | 'org'; - /** Populated for org invites if an org was passed as a prop. */ - org?: { - id: string; - displayName: string; - }; - /** Caller routes to this path after acceptance, if provided. */ - redirectTo?: string; -}; - -export type InvitationAcceptanceCardProps = { - /** The invite token from the URL (required). */ - token: string; - /** App-level or org-level invite (default 'app'). */ - kind?: 'app' | 'org'; - /** Optional: inviter user data for display. */ - inviter?: UserAvatarUser | null; - /** Optional: org user data for display (kind='org'). */ - org?: UserAvatarUser | null; - /** Optional: role label for display (kind='org'). */ - role?: string | null; - messages?: InvitationAcceptanceMessageOverrides; - /** - * Replace the default mutation calls. - * Receives `{ token, kind }` and must resolve to `InviteAcceptResult`. - */ - onSubmit?: (input: { token: string; kind: 'app' | 'org' }) => Promise; - /** Fires after acceptance. Always fires. */ - onSuccess?: (result: InviteAcceptResult) => void; - /** Fires when Decline is clicked. Caller navigates away. */ - onDecline?: () => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and non-fatal branches. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// ── Simple mustache interpolation for {{key}} tokens ───────────────────────── -function interpolate(template: string, vars: Record): string { - return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ''); -} - -// ── Component ───────────────────────────────────────────────────────────────── - -export function InvitationAcceptanceCard({ - token, - kind = 'app', - inviter, - org, - role, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onDecline, - onError, - onMessage, - className -}: InvitationAcceptanceCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: InvitationAcceptanceMessages = { - ...defaultInvitationAcceptanceMessages, - ...messageOverrides, - errors: { ...defaultInvitationAcceptanceMessages.errors, ...messageOverrides?.errors } - }; - - // Generated hooks from the host's `admin` SDK (invites_public → namespace admin). - // Both are always instantiated — only one is called based on `kind`. - const submitApp = useSubmitAppInviteCodeMutation({ - selection: { fields: { result: true } } - }); - - const submitOrg = useSubmitOrgInviteCodeMutation({ - selection: { fields: { result: true } } - }); - - // Hybrid pending: generated hooks track their own; override path does not. - const [overridePending, setOverridePending] = useState(false); - const defaultIsPending = kind === 'org' ? submitOrg.isPending : submitApp.isPending; - const isPending = onSubmitOverride ? overridePending : defaultIsPending; - - const [error, setError] = useState(null); - const [accepted, setAccepted] = useState(false); - const [pendingApproval, setPendingApproval] = useState(false); - - async function handleAccept() { - if (!token) { - const msg = merged.missingTokenDescription; - setError(msg); - onMessage?.({ kind: 'error', key: 'MISSING_TOKEN', message: msg }); - onError?.({ message: msg, code: 'MISSING_TOKEN' }); - return; - } - - setError(null); - if (onSubmitOverride) setOverridePending(true); - - try { - let result: InviteAcceptResult; - - let serverAccepted: boolean; - - if (onSubmitOverride) { - result = await onSubmitOverride({ token, kind }); - // B2 compliance: InviteAcceptResult has no accepted field. - // Derive acceptance from result shape: app invites are always accepted; - // org invites are accepted when the caller populates result.org. - serverAccepted = result.kind === 'app' || result.org !== undefined; - } else { - if (kind === 'org') { - const data = await submitOrg.mutateAsync({ input: { token } }); - serverAccepted = data.submitOrgInviteCode?.result ?? false; - } else { - const data = await submitApp.mutateAsync({ input: { token } }); - serverAccepted = data.submitAppInviteCode?.result ?? false; - } - - result = { - kind, - org: - kind === 'org' && org - ? { id: org.id, displayName: org.displayName } - : undefined - }; - } - - if (serverAccepted) { - setAccepted(true); - - const successKey = kind === 'org' ? 'inviteAccepted.org' : 'inviteAccepted.app'; - const successMsg = - kind === 'org' - ? interpolate(merged.orgSuccessTitle, { orgName: org?.displayName ?? '' }) - : merged.appSuccessTitle; - - onMessage?.({ kind: 'success', key: successKey, message: successMsg }); - onSuccess?.(result); - } else { - // Server returned result:false — org approval is pending (is_approved=false). - setPendingApproval(true); - const orgName = org?.displayName ?? ''; - const pendingMsg = interpolate(merged.pendingApprovalDescription, { orgName }); - onMessage?.({ kind: 'info', key: 'INVITE_PENDING_APPROVAL', message: pendingMsg }); - onSuccess?.(result); - } - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - function handleDecline() { - onDecline?.(); - } - - // ── Success screen ────────────────────────────────────────────────────────── - if (accepted) { - const orgName = org?.displayName ?? ''; - - return ( - - - - {kind === 'org' - ? interpolate(merged.orgSuccessTitle, { orgName }) - : merged.appSuccessTitle} - - - {kind === 'org' - ? interpolate(merged.orgSuccessDescription, { orgName }) - : merged.appSuccessDescription} - - - {kind === 'org' && ( - -

{merged.orgSuccessSwitchHint}

-
- )} -
- ); - } - - // ── Pending-approval screen (org invite, is_approved=false) ───────────────── - if (pendingApproval) { - const orgName = org?.displayName ?? ''; - - return ( - - - {merged.pendingApprovalTitle} - - {interpolate(merged.pendingApprovalDescription, { orgName })} - - - - ); - } - - // ── Invite display ────────────────────────────────────────────────────────── - const orgName = org?.displayName ?? ''; - const inviterName = inviter?.displayName ?? ''; - - const title = - kind === 'org' - ? interpolate(merged.orgInviteTitle, { orgName }) - : merged.appInviteTitle; - - const description = - kind === 'org' - ? interpolate(merged.orgInviteDescription, { orgName, inviterName }) - : merged.appInviteDescription; - - return ( - - - {title} - {description} - - - - - - {/* Org avatar + inviter */} - {kind === 'org' && (org || inviter) && ( -
- {org && ( -
- -
-

{org.displayName}

- {org.username && ( -

@{org.username}

- )} -
-
- )} - - {org && inviter && } - - {inviter && ( -
- -

- {merged.orgInviteFrom}:{' '} - {inviter.displayName} -

-
- )} - - {role && ( -
-

{merged.orgInviteRole}:

- {role} -
- )} -
- )} -
- - - - {merged.acceptButton} - - - - -
- ); -} diff --git a/apps/blocks/src/blocks/auth/invitation-acceptance-card/messages.ts b/apps/blocks/src/blocks/auth/invitation-acceptance-card/messages.ts deleted file mode 100644 index 6a120d3..0000000 --- a/apps/blocks/src/blocks/auth/invitation-acceptance-card/messages.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * invitation-acceptance-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - */ - -export type InvitationAcceptanceMessages = { - loadingTitle: string; - appInviteTitle: string; - appInviteDescription: string; - /** Runtime interpolation: {{orgName}} */ - orgInviteTitle: string; - /** Runtime interpolation: {{inviterName}}, {{orgName}} */ - orgInviteDescription: string; - orgInviteRole: string; - orgInviteFrom: string; - acceptButton: string; - acceptButtonPending: string; - declineButton: string; - appSuccessTitle: string; - appSuccessDescription: string; - /** Runtime interpolation: {{orgName}} */ - orgSuccessTitle: string; - /** Runtime interpolation: {{orgName}} */ - orgSuccessDescription: string; - orgSuccessSwitchHint: string; - /** Shown when server returns result:false — org approval is pending. Runtime interpolation: {{orgName}} */ - pendingApprovalTitle: string; - /** Runtime interpolation: {{orgName}} */ - pendingApprovalDescription: string; - expiredTitle: string; - expiredDescription: string; - alreadyUsedTitle: string; - alreadyUsedDescription: string; - emailMismatchTitle: string; - emailMismatchDescription: string; - emailNotVerifiedError: string; - limitReachedTitle: string; - limitReachedDescription: string; - notFoundTitle: string; - notFoundDescription: string; - missingTokenTitle: string; - missingTokenDescription: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - INVITE_NOT_FOUND: string; - INVITE_LIMIT: string; - INVITE_EMAIL_NOT_FOUND: string; - EMAIL_NOT_VERIFIED: string; - UNKNOWN_ERROR: string; - }; -}; - -/** Deep-partial override type: top-level keys optional; errors nested-partial. */ -export type InvitationAcceptanceMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultInvitationAcceptanceMessages: InvitationAcceptanceMessages = { - loadingTitle: 'Loading invitation…', - appInviteTitle: "You’ve been invited", - appInviteDescription: "You’ve received an invitation to join the app.", - orgInviteTitle: "You’ve been invited to {{orgName}}", - orgInviteDescription: '{{inviterName}} has invited you to join {{orgName}}.', - orgInviteRole: 'Role', - orgInviteFrom: 'Invited by', - acceptButton: 'Accept invitation', - acceptButtonPending: 'Accepting…', - declineButton: 'Decline', - appSuccessTitle: 'Welcome aboard!', - appSuccessDescription: "You’ve successfully joined the app.", - orgSuccessTitle: "You’ve joined {{orgName}}", - orgSuccessDescription: 'You are now a member of {{orgName}}.', - orgSuccessSwitchHint: 'You can switch to this organization using the context switcher.', - pendingApprovalTitle: 'Request submitted', - pendingApprovalDescription: 'Your request to join {{orgName}} is pending approval by an administrator.', - expiredTitle: 'Invitation expired', - expiredDescription: 'This invitation link has expired. Ask the sender for a new one.', - alreadyUsedTitle: 'Already used', - alreadyUsedDescription: 'This invitation has already been claimed.', - emailMismatchTitle: 'Wrong account', - emailMismatchDescription: - 'This invitation was sent to a different email address. Sign in with the correct account.', - emailNotVerifiedError: 'Please verify your email address before accepting this invitation.', - limitReachedTitle: 'Invitation limit reached', - limitReachedDescription: 'This invitation link has reached its maximum number of uses.', - notFoundTitle: 'Invitation not found', - notFoundDescription: 'This invitation link is invalid or has been cancelled.', - missingTokenTitle: 'Invalid link', - missingTokenDescription: 'This invitation link is missing required parameters.', - errors: { - INVITE_NOT_FOUND: 'This invitation was not found.', - INVITE_LIMIT: 'This invitation has reached its usage limit.', - INVITE_EMAIL_NOT_FOUND: 'This invitation was sent to a different email address.', - EMAIL_NOT_VERIFIED: 'Please verify your email before accepting this invitation.', - UNKNOWN_ERROR: 'Something went wrong. Please try again.', - }, -}; diff --git a/apps/blocks/src/blocks/auth/invitation-acceptance-page/auth-invitation-acceptance-page.requires.json b/apps/blocks/src/blocks/auth/invitation-acceptance-page/auth-invitation-acceptance-page.requires.json deleted file mode 100644 index b61c8d3..0000000 --- a/apps/blocks/src/blocks/auth/invitation-acceptance-page/auth-invitation-acceptance-page.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": [], - "queries": ["currentUser"], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/invitation-acceptance-page/invitation-acceptance-page.tsx b/apps/blocks/src/blocks/auth/invitation-acceptance-page/invitation-acceptance-page.tsx deleted file mode 100644 index dcd0dee..0000000 --- a/apps/blocks/src/blocks/auth/invitation-acceptance-page/invitation-acceptance-page.tsx +++ /dev/null @@ -1,184 +0,0 @@ -'use client'; - -/** - * invitation-acceptance-page (registry: auth-invitation-acceptance-page) - * - * Thin Next.js page wrapper that composes [[auth-invitation-acceptance-card]] - * inside a centered full-viewport layout and adds router glue: - * - * • Auth gate: calls `useCurrentUserQuery` from `@/generated/auth` to check - * authentication before rendering the card. Unauthenticated users are - * redirected to SIGN_IN_PATH with a `?redirect=` return URL. Users with - * `is_verified=false` see a warning before the card renders. - * • Reads `?token=` and `?kind=` from `useSearchParams()` and passes them - * to `InvitationAcceptanceCard`. - * • Routes to `result.redirectTo` (or `DEFAULT_REDIRECT`) after a successful - * acceptance via `onSuccess`. - * • Routes to `DECLINE_REDIRECT` when the user clicks Decline. - * - * Pages MAY use `next/navigation`; Cards MUST NOT (block-contract.md §6). - * This block imports `useCurrentUserQuery` from `@/generated/auth` and ships - * `auth-invitation-acceptance-page.requires.json` (sdk-binding-contract.md §7). - * - * Editable constants after install: - * const DEFAULT_REDIRECT = '/dashboard'; - * const DECLINE_REDIRECT = '/'; - * const SIGN_IN_PATH = '/auth/sign-in'; - * const BRAND_LOGO_SRC = ''; // optional logo URL - */ - -import { useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; - -import { useCurrentUserQuery } from '@/generated/auth'; -import { - InvitationAcceptanceCard, - type InviteAcceptResult -} from '@/blocks/auth/invitation-acceptance-card/invitation-acceptance-card'; -import { - defaultInvitationAcceptanceMessages, - type InvitationAcceptanceMessageOverrides -} from '@/blocks/auth/invitation-acceptance-card/messages'; - -// --------------------------------------------------------------------------- -// Editable constants (installed page — consumer modifies these in place) -// --------------------------------------------------------------------------- -const DEFAULT_REDIRECT = '/dashboard'; -const DECLINE_REDIRECT = '/'; -const SIGN_IN_PATH = '/auth/sign-in'; -/** Optional brand logo URL. Renders above the card when non-empty. */ -const BRAND_LOGO_SRC = ''; - -// --------------------------------------------------------------------------- -// Open-redirect guard (same guard used in sign-in-page) -// --------------------------------------------------------------------------- - -/** - * Returns `redirect` only when it resolves to the same origin. External URLs, - * protocol-relative URLs, and path-encoded bypasses fall back to `fallback`. - */ -function safeRedirect(redirect: string | null | undefined, fallback: string): string { - if (!redirect) return fallback; - try { - const url = new URL(redirect, window.location.origin); - return url.origin === window.location.origin ? redirect : fallback; - } catch { - return fallback; - } -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export type InvitationAcceptancePageProps = { - messages?: InvitationAcceptanceMessageOverrides; - className?: string; -}; - -export default function InvitationAcceptancePage({ messages: messageOverrides, className }: InvitationAcceptancePageProps) { - const router = useRouter(); - const searchParams = useSearchParams(); - - const token = searchParams.get('token') ?? ''; - const rawKind = searchParams.get('kind'); - const kind: 'app' | 'org' = rawKind === 'org' ? 'org' : 'app'; - const rawRedirect = searchParams.get('redirect'); - const redirectTo = safeRedirect(rawRedirect ? decodeURIComponent(rawRedirect) : null, DEFAULT_REDIRECT); - - // Merge messages for page-level copy (missing-token state, auth gate) - const merged = { - ...defaultInvitationAcceptanceMessages, - ...messageOverrides, - errors: { ...defaultInvitationAcceptanceMessages.errors, ...messageOverrides?.errors } - }; - - // --------------------------------------------------------------------------- - // Auth gate — check current user before rendering the acceptance card - // --------------------------------------------------------------------------- - const { data: currentUserData, isLoading: authLoading } = useCurrentUserQuery({ - selection: { fields: { id: true } } - }); - - const currentUser = currentUserData?.currentUser; - - // Redirect unauthenticated users to sign-in with a return URL. - useEffect(() => { - if (!authLoading && !currentUser) { - const returnUrl = encodeURIComponent(`/invite?token=${token}&kind=${kind}`); - router.replace(`${SIGN_IN_PATH}?redirect=${returnUrl}`); - } - }, [authLoading, currentUser, token, kind, router]); - - // --------------------------------------------------------------------------- - // Handlers - // --------------------------------------------------------------------------- - - function handleSuccess(result: InviteAcceptResult) { - const target = result.redirectTo ? safeRedirect(result.redirectTo, redirectTo) : redirectTo; - router.push(target); - } - - function handleDecline() { - router.push(DECLINE_REDIRECT); - } - - // --------------------------------------------------------------------------- - // Render - // --------------------------------------------------------------------------- - - return ( -
- {BRAND_LOGO_SRC && ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Brand logo -
- )} - - {/* Auth loading skeleton */} - {authLoading && ( -
-
-
-
-
-
- )} - - {/* Signed-in state: show page content */} - {!authLoading && currentUser && ( - <> - {!token ? ( -
-

{merged.missingTokenTitle}

-

{merged.missingTokenDescription}

- - Go to sign in - -
- ) : ( - - )} - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/magic-link-callback-page/auth-magic-link-callback-page.requires.json b/apps/blocks/src/blocks/auth/magic-link-callback-page/auth-magic-link-callback-page.requires.json deleted file mode 100644 index f674770..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-callback-page/auth-magic-link-callback-page.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["signInMagicLink"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/magic-link-callback-page/magic-link-callback-page.tsx b/apps/blocks/src/blocks/auth/magic-link-callback-page/magic-link-callback-page.tsx deleted file mode 100644 index aea031c..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-callback-page/magic-link-callback-page.tsx +++ /dev/null @@ -1,404 +0,0 @@ -'use client'; - -/** - * magic-link-callback-page (registry: auth-magic-link-callback-page) - * - * Handles the /auth/magic-link?token=... URL that users land on from their - * email client. On mount, calls `constructive_auth_public.sign_in_magic_link` - * via the generated `useSignInMagicLinkMutation` hook and transitions through: - * loading → success (redirect) | expired | invalid | missing-token. - * - * BACKEND-PENDING — CASE (b): - * `sign_in_magic_link` is not yet deployed in `constructive_auth_public`, so - * `useSignInMagicLinkMutation` is not present in the generated auth SDK at the - * time this block was authored. The import is therefore OMITTED so that - * `tsc --noEmit` passes. The `onSubmit` override is the primary/required path; - * the host wires the generated binding after regenerating the SDK once the proc - * ships. `requires.json` names `signInMagicLink` so `check-sdk-fixtures.ts` will fail - * with a precise message until the host SDK exports it. - * - * DATA PATH (after proc ships): - * import { useSignInMagicLinkMutation } from '@/generated/auth'; - * const defaultMutation = useSignInMagicLinkMutation({ - * selection: { - * fields: { - * result: { - * select: { - * id: true, userId: true, accessToken: true, accessTokenExpiresAt: true, - * isVerified: true, mfaRequired: true, mfaChallengeToken: true, - * } - * } - * } - * } - * }); - * const result = await defaultMutation.mutateAsync({ input: { token, credentialKind } }) - * .then((d) => d.signInMagicLink?.result ?? null); - * - * No fetch, no GraphQL document, no client bootstrap in this file. - * Pages may use next/navigation (block-contract.md §2, §6). - */ - -import { Suspense, useEffect, useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; - -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; - -import { - defaultMagicLinkCallbackPageMessages, - type MagicLinkCallbackPageMessageOverrides, - type MagicLinkCallbackPageMessages -} from './messages'; - -// ─── Configurable constants (edit after installing) ───────────────────────── -const DEFAULT_REDIRECT = '/dashboard'; -const SIGN_IN_PATH = '/auth/sign-in'; -const MAGIC_LINK_REQUEST_PATH = '/auth/magic-link-request'; -const MFA_PATH = '/auth/mfa/totp'; -const CREDENTIAL_KIND = 'bearer'; -// ──────────────────────────────────────────────────────────────────────────── - -/** Internal page state machine */ -type PageState = 'loading' | 'success' | 'expired' | 'invalid' | 'missing-token'; - -/** - * The sign-in result shape this page consumes. Mirrors the auth SDK's - * `SignInMagicLinkPayload` (the fields this page selects); declared here so - * the `onSubmit` override contract does not depend on a generated type name. - */ -export type MagicLinkSignInResult = { - id: string | null; - userId: string | null; - accessToken: string | null; - accessTokenExpiresAt: string | null; - isVerified: boolean | null; - mfaRequired: boolean | null; - mfaChallengeToken: string | null; -}; - -// ─── Inner implementation (needs useSearchParams + useRouter) ──────────────── - -interface MagicLinkCallbackInnerProps { - messages?: MagicLinkCallbackPageMessageOverrides; - /** - * Replace the default `useSignInMagicLinkMutation` call. Receives the same - * vars ({ token, credentialKind }) the default hook would send. - * - * REQUIRED while the backend procedure is pending (CASE b). Once the proc is - * deployed and the host SDK regenerated, this becomes optional — the host can - * remove the override and the generated hook takes over. - */ - onSubmit?: (vars: { token: string; credentialKind: string }) => Promise; - /** Fires after a successful sign-in. Always fires. */ - onSuccess?: (result: MagicLinkSignInResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, warnings, and errors. Always fires. */ - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - className?: string; -} - -function MagicLinkCallbackInner({ - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MagicLinkCallbackInnerProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: MagicLinkCallbackPageMessages = { - ...defaultMagicLinkCallbackPageMessages, - ...messageOverrides, - errors: { - ...defaultMagicLinkCallbackPageMessages.errors, - ...messageOverrides?.errors - } - }; - - const router = useRouter(); - const searchParams = useSearchParams(); - const token = searchParams.get('token'); - const redirectParam = searchParams.get('redirect'); - - // Validate redirect is same-origin to prevent open-redirect attacks. - function safeRedirect(raw: string | null): string { - if (!raw) return DEFAULT_REDIRECT; - try { - const url = new URL(raw, window.location.origin); - return url.origin === window.location.origin ? url.pathname + url.search : DEFAULT_REDIRECT; - } catch { - return DEFAULT_REDIRECT; - } - } - - const [pageState, setPageState] = useState(() => - !token ? 'missing-token' : 'loading' - ); - - // Hybrid pending: the generated hook tracks its own; the override path does not. - const [overridePending, setOverridePending] = useState(false); - - // ── BACKEND-PENDING (CASE b): defaultMutation is unavailable until the proc - // ships and the host regenerates the SDK. The block uses the onSubmit - // override seam as its sole execution path right now. - // - // After the proc ships, restore the generated hook binding: - // - // import { useSignInMagicLinkMutation } from '@/generated/auth'; - // const defaultMutation = useSignInMagicLinkMutation({ - // selection: { - // fields: { - // result: { - // select: { - // id: true, userId: true, accessToken: true, - // accessTokenExpiresAt: true, isVerified: true, - // mfaRequired: true, mfaChallengeToken: true, - // } - // } - // } - // } - // }); - // - // And replace `runSignIn` with the hybrid pattern (see sign-in-card.tsx). - // ──────────────────────────────────────────────────────────────────────────── - - async function runSignIn(vars: { - token: string; - credentialKind: string; - }): Promise { - if (onSubmitOverride) { - return onSubmitOverride(vars); - } - // PROCEDURE_NOT_FOUND guard: if no override and no generated hook yet, - // surface the backend-pending error message rather than crashing. - throw Object.assign( - new Error(merged.errors.PROCEDURE_NOT_FOUND), - { extensions: { code: 'PROCEDURE_NOT_FOUND' } } - ); - } - - // Fire on mount when token param is present. - useEffect(() => { - if (!token) return; - - async function runCallback() { - if (onSubmitOverride) setOverridePending(true); - try { - const result = await runSignIn({ token: token!, credentialKind: CREDENTIAL_KIND }); - - if (!result) { - // Null result without an exception → treat as invalid token. - const message = merged.errors.INVALID_TOKEN; - setPageState('invalid'); - onError?.({ message, code: 'INVALID_TOKEN' }); - onMessage?.({ kind: 'error', key: 'INVALID_TOKEN', message }); - return; - } - - if (result.mfaRequired && result.mfaChallengeToken) { - // MFA required: route to TOTP challenge page. - const redirect = safeRedirect(redirectParam); - onMessage?.({ kind: 'warning', key: 'mfaRequired' }); - onSuccess?.(result); - router.push( - `${MFA_PATH}?token=${encodeURIComponent(result.mfaChallengeToken)}&redirect=${encodeURIComponent(redirect)}` - ); - return; - } - - // Full success: transition to success state then redirect. - setPageState('success'); - onMessage?.({ kind: 'success', key: 'signInMagicLink.success', message: merged.successDescription }); - onSuccess?.(result); - router.push(safeRedirect(redirectParam)); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - - if (code === 'EXPIRED_TOKEN') { - setPageState('expired'); - onError?.({ message, code: key }); - onMessage?.({ kind: 'error', key, message }); - } else { - setPageState('invalid'); - onError?.({ message, code: key }); - onMessage?.({ kind: 'error', key, message }); - } - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - runCallback(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const isLoading = pageState === 'loading' && overridePending; - - return ( -
-
- {pageState === 'loading' && ( - - - {merged.loadingTitle} - {merged.loadingDescription} - - - )} - - {pageState === 'success' && ( - - - {merged.successTitle} - {merged.successDescription} - - - )} - - {pageState === 'expired' && ( - - - {merged.expiredTitle} - {merged.expiredDescription} - - - - - - )} - - {pageState === 'invalid' && ( - - - {merged.invalidTitle} - {merged.invalidDescription} - - - - - - )} - - {pageState === 'missing-token' && ( - - - {merged.missingTokenTitle} - {merged.missingTokenDescription} - - - - - - )} -
-
- ); -} - -// ─── Public page export ────────────────────────────────────────────────────── - -/** - * Props for the magic-link callback page. Because this is a `registry:page`, - * the host typically wires these at the page file level rather than via the - * registry entry. The `onSubmit` prop is the primary seam while the backend - * procedure `sign_in_magic_link` is pending (sdk-binding-contract.md §10, - * CASE b). - */ -export interface MagicLinkCallbackPageProps { - messages?: MagicLinkCallbackPageMessageOverrides; - onSubmit?: (vars: { token: string; credentialKind: string }) => Promise; - onSuccess?: (result: MagicLinkSignInResult) => void; - onError?: (err: { message: string; code: string }) => void; - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - className?: string; -} - -/** - * Next.js page component for the magic-link sign-in callback. - * Mount at `/auth/magic-link` (the URL embedded in magic-link emails). - * Wrap with `` at the page level per Next.js 15 requirements. - * - * @example - * ```tsx - * // app/auth/magic-link/page.tsx - * import { Suspense } from 'react'; - * import MagicLinkCallbackPage from '@/blocks/auth/magic-link-callback-page/magic-link-callback-page'; - * - * export default function Page() { - * return ( - * - * { - * // Host wires the generated hook here until the proc ships. - * return null; - * }} - * /> - * - * ); - * } - * ``` - */ -export default function MagicLinkCallbackPage({ - messages, - onSubmit, - onSuccess, - onError, - onMessage, - className -}: MagicLinkCallbackPageProps) { - return ( - -
- - - {defaultMagicLinkCallbackPageMessages.loadingTitle} - - {defaultMagicLinkCallbackPageMessages.loadingDescription} - - - -
-
- } - > - - - ); -} diff --git a/apps/blocks/src/blocks/auth/magic-link-callback-page/messages.ts b/apps/blocks/src/blocks/auth/magic-link-callback-page/messages.ts deleted file mode 100644 index 17e080c..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-callback-page/messages.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * magic-link-callback-page — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend - * error CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` - * as `customMessages`. The `PROCEDURE_NOT_FOUND` key is required because - * `sign_in_magic_link` is a backend-pending procedure; it surfaces a clear - * message if the host SDK is installed before the DB proc is deployed. - * - * (sdk-binding-contract.md §10 — backend-pending block: requires.json names - * the absent op, messages.errors.PROCEDURE_NOT_FOUND surfaces the gap.) - */ - -export type MagicLinkCallbackPageMessages = { - loadingTitle: string; - loadingDescription: string; - successTitle: string; - successDescription: string; - expiredTitle: string; - expiredDescription: string; - expiredRequestNewLink: string; - invalidTitle: string; - invalidDescription: string; - invalidSignInLink: string; - missingTokenTitle: string; - missingTokenDescription: string; - missingTokenSignInLink: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - EXPIRED_TOKEN: string; - INVALID_TOKEN: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: top-level is shallow-partial; `errors` is itself - * partial so a host can override a single error code without restating the map. - */ -export type MagicLinkCallbackPageMessageOverrides = Partial< - Omit -> & { - errors?: Partial; -}; - -export const defaultMagicLinkCallbackPageMessages: MagicLinkCallbackPageMessages = { - loadingTitle: 'Signing you in…', - loadingDescription: 'Please wait while we verify your link.', - successTitle: 'Signed in', - successDescription: 'You have been signed in successfully. Redirecting…', - expiredTitle: 'Link expired', - expiredDescription: - 'This sign-in link has expired or has already been used. Request a new one.', - expiredRequestNewLink: 'Request a new link', - invalidTitle: 'Invalid link', - invalidDescription: 'This sign-in link is invalid.', - invalidSignInLink: 'Back to sign in', - missingTokenTitle: 'Invalid link', - missingTokenDescription: - 'This sign-in link is missing required parameters. Try clicking the link in your email again.', - missingTokenSignInLink: 'Back to sign in', - errors: { - EXPIRED_TOKEN: 'This sign-in link has expired.', - INVALID_TOKEN: 'This sign-in link is invalid.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Sign-in failed. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/magic-link-request-card/auth-magic-link-request-card.requires.json b/apps/blocks/src/blocks/auth/magic-link-request-card/auth-magic-link-request-card.requires.json deleted file mode 100644 index 8e69cdf..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-request-card/auth-magic-link-request-card.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["requestMagicLink"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/magic-link-request-card/magic-link-request-card.tsx b/apps/blocks/src/blocks/auth/magic-link-request-card/magic-link-request-card.tsx deleted file mode 100644 index a32dec1..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-request-card/magic-link-request-card.tsx +++ /dev/null @@ -1,335 +0,0 @@ -'use client'; - -/** - * magic-link-request-card (registry: auth-magic-link-request-card) - * - * Email-only form that initiates the magic-link sign-in flow. On success it - * transitions to a "Check your email" confirmation panel within the same card - * — NO navigation/redirect. The confirmation copy interpolates the submitted - * email address. Mirrors the `forgot-password-card` pattern exactly. - * - * BACKEND-PENDING — CASE (b): `request_magic_link` is not yet deployed in - * `constructive_auth_public` and the generated `useRequestMagicLinkMutation` - * hook does NOT exist in the reference SDK. This block therefore: - * • Does NOT import from `@/generated/auth` (no hook to import — tsc would fail). - * • Makes `onSubmit` the primary/required network path: the host wires the - * generated binding after running `cnc codegen --api-names auth ...`. - * • The stub default path throws a typed PROCEDURE_NOT_FOUND error so the - * block behaves gracefully (shows the error message) if accidentally mounted - * without the override. - * • `requires.json` names the pending op so `check-sdk-fixtures.ts` fails clearly. - * • `PROCEDURE_NOT_FOUND` is in `messages.errors`. - * - * When the backend ships and the host regenerates the SDK, replace the stub - * `defaultRunRequest` with: - * const defaultMutation = useRequestMagicLinkMutation({ selection: { fields: { clientMutationId: true } } }); - * async function defaultRunRequest(vars: MagicLinkRequestVars) { - * await defaultMutation.mutateAsync({ input: { email: vars.email } }).then((d) => d.requestMagicLink); - * } - * and add the hybrid-isPending pattern (see sdk-binding-contract.md §5). - * - * Override seam: `onSubmit` fully replaces the default network call. - * Resend: the "Resend email" button in the confirmed panel calls the same path. - * - * NO QueryClientProvider, NO configure(), NO fetch, NO GraphQL document strings. - * The host mounts `blocks-runtime` once at app root; that is the single wiring - * point. This block joins it as soon as the generated hook ships. - */ - -import { useRef, useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { forgotPasswordSchema, type ForgotPasswordFormData } from '@/blocks/lib/schemas'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { FormField } from '@/blocks/primitives/form-field'; - -import { defaultMagicLinkRequestCardMessages, type MagicLinkRequestCardMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -/** Variables the magic-link request call receives. The `onSubmit` override gets these verbatim. */ -export type MagicLinkRequestVars = { - email: string; -}; - -/** - * Message overrides. Top-level copy is shallow-partial; `errors` is itself - * partial so a host can localize a single error code without restating the map. - */ -export type MagicLinkRequestCardMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type MagicLinkRequestCardProps = { - /** Pre-fill the email field (e.g. from a query param). */ - defaultEmail?: string; - /** Show a "Back to sign in" link. Default: true. */ - showBackLink?: boolean; - messages?: MagicLinkRequestCardMessageOverrides; - /** Href for the back-to-sign-in link. Rendered as plain `` when provided. */ - signInHref?: string; - /** - * Replace the default mutation call. Receives the same vars. - * - * BACKEND-PENDING: Until `request_magic_link` is deployed and the host has - * regenerated its auth SDK, this prop is the ONLY way to wire a real network - * call. After codegen, the host may drop this prop and let the generated hook - * (`useRequestMagicLinkMutation`) take over via `blocks-runtime`. - */ - onSubmit?: (vars: MagicLinkRequestVars) => Promise; - /** Fires after a resolved magic-link request. Always fires on success. */ - onSuccess?: (vars: MagicLinkRequestVars) => void; - /** Fires after a mapped error. Always fires on error. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success, mapped errors, and resend. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -type CardState = 'form' | 'confirmed'; - -/** Replaces all `{{email}}` tokens in a template string. */ -function interpolateEmail(template: string, email: string): string { - return template.replace(/\{\{email\}\}/g, email); -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function MagicLinkRequestCard({ - defaultEmail, - showBackLink = true, - messages: messageOverrides, - signInHref, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MagicLinkRequestCardProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: MagicLinkRequestCardMessages = { - ...defaultMagicLinkRequestCardMessages, - ...messageOverrides, - errors: { ...defaultMagicLinkRequestCardMessages.errors, ...messageOverrides?.errors } - }; - - // --------------------------------------------------------------------------- - // BACKEND-PENDING stub (CASE b) - // - // The generated `useRequestMagicLinkMutation` does not exist yet. We provide - // a stub that throws PROCEDURE_NOT_FOUND so the block is self-describing when - // mounted without an `onSubmit` override. Replace this section with the real - // generated hook once the proc ships and the host regenerates its auth SDK. - // --------------------------------------------------------------------------- - const [stubPending, setStubPending] = useState(false); - - async function defaultRunRequest(vars: MagicLinkRequestVars): Promise { - // Throw a typed PROCEDURE_NOT_FOUND error so parseGraphQLError maps it to - // the human-readable message in merged.errors. - const err = Object.assign(new Error(merged.errors.PROCEDURE_NOT_FOUND), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - throw err; - } - - // Hybrid pending state: override path tracks its own; stub uses local state. - const isPending = onSubmitOverride ? stubPending : stubPending; - - // Resend has its own pending state; it reuses the same run path. - const [resendPending, setResendPending] = useState(false); - - const [error, setError] = useState(null); - const [cardState, setCardState] = useState('form'); - const submittedEmailRef = useRef(''); - const confirmationFocusRef = useRef(null); - - async function runRequest(vars: MagicLinkRequestVars): Promise { - if (onSubmitOverride) return onSubmitOverride(vars); - return defaultRunRequest(vars); - } - - async function handleSubmit(values: ForgotPasswordFormData) { - setError(null); - if (onSubmitOverride) setStubPending(true); - try { - forgotPasswordSchema.parse(values); - await runRequest({ email: values.email }); - submittedEmailRef.current = values.email; - setCardState('confirmed'); - onMessage?.({ kind: 'success', key: 'magicLinkRequest.success' }); - onSuccess?.({ email: values.email }); - // Move focus to the confirmation panel for accessibility. - setTimeout(() => confirmationFocusRef.current?.focus(), 0); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setStubPending(false); - } - } - - async function handleResend() { - setResendPending(true); - try { - await runRequest({ email: submittedEmailRef.current }); - onMessage?.({ kind: 'info', key: 'magicLinkRequest.resend', message: merged.resendSuccess }); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setResendPending(false); - } - } - - const form = useForm({ - defaultValues: { - email: defaultEmail ?? '' - } as ForgotPasswordFormData, - onSubmit: async ({ value }) => { - await handleSubmit(value); - } - }); - - // --------------------------------------------------------------------------- - // Confirmed state - // --------------------------------------------------------------------------- - - if (cardState === 'confirmed') { - return ( - - - {/* Block-owned focusable anchor — CardTitle does not forward its ref. */} -
- {merged.confirmationTitle} -
- - {interpolateEmail(merged.confirmationDescription, submittedEmailRef.current)} - -
- - - - {merged.resendButton} - - - - {showBackLink && signInHref && ( - -
- - )} - - ); - } - - // --------------------------------------------------------------------------- - // Form state - // --------------------------------------------------------------------------- - - return ( - - - {merged.title} - {merged.description} - - - - - -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - { - if (!value) return 'Email is required'; - if (!/\S+@\S+\.\S+/.test(value)) return 'Please enter a valid email'; - return undefined; - } - }} - > - {(field) => ( - - )} - - -
- - {merged.submitButton} - -
-
-
- - {showBackLink && signInHref && ( - - - - )} -
- ); -} diff --git a/apps/blocks/src/blocks/auth/magic-link-request-card/messages.ts b/apps/blocks/src/blocks/auth/magic-link-request-card/messages.ts deleted file mode 100644 index 902bf65..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-request-card/messages.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * magic-link-request-card — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * PROCEDURE_NOT_FOUND is included because `request_magic_link` is backend-pending - * (not yet deployed in `constructive_auth_public`). Until the proc ships and the - * host regenerates its auth SDK, any attempt to call the mutation will surface this - * code. The block uses the override seam (`onSubmit`) as the primary path until - * the generated hook becomes available. - */ - -export type MagicLinkRequestCardMessages = { - title: string; - description: string; - emailLabel: string; - emailPlaceholder: string; - submitButton: string; - submitButtonPending: string; - backToSignIn: string; - /** Confirmation state strings */ - confirmationTitle: string; - /** Runtime interpolation: {{email}} */ - confirmationDescription: string; - resendButton: string; - resendPending: string; - resendSuccess: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - RATE_LIMITED: string; - CAPTCHA_FAILED: string; - MAGIC_LINK_DISABLED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultMagicLinkRequestCardMessages: MagicLinkRequestCardMessages = { - title: 'Sign in with email link', - description: "Enter your email and we'll send you a sign-in link.", - emailLabel: 'Email', - emailPlaceholder: 'you@example.com', - submitButton: 'Send sign-in link', - submitButtonPending: 'Sending…', - backToSignIn: '← Back to sign in', - confirmationTitle: 'Check your email', - confirmationDescription: 'We sent a sign-in link to {{email}}. Check your inbox.', - resendButton: 'Resend email', - resendPending: 'Resending…', - resendSuccess: 'Email resent.', - errors: { - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - CAPTCHA_FAILED: 'Captcha verification failed. Please try again.', - MAGIC_LINK_DISABLED: 'Magic link sign-in is not enabled.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/magic-link-sent-page/auth-magic-link-sent-page.requires.json b/apps/blocks/src/blocks/auth/magic-link-sent-page/auth-magic-link-sent-page.requires.json deleted file mode 100644 index 8e69cdf..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-sent-page/auth-magic-link-sent-page.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["requestMagicLink"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/magic-link-sent-page/magic-link-sent-page.tsx b/apps/blocks/src/blocks/auth/magic-link-sent-page/magic-link-sent-page.tsx deleted file mode 100644 index e0e9158..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-sent-page/magic-link-sent-page.tsx +++ /dev/null @@ -1,287 +0,0 @@ -'use client'; - -/** - * magic-link-sent-page (registry: auth-magic-link-sent-page) - * - * Static confirmation page shown after [[auth-magic-link-request-card]] submits. - * Provides a "Check your email" affordance with: - * • Resend CTA — calls `useRequestMagicLinkMutation` from the host's generated - * `auth` SDK (sdk-binding-contract.md §3). - * • Resend cooldown — 60-second countdown tracked in component state. - * • "Use a different email" / "Back to sign in" navigation links. - * - * BACKEND-PENDING — CASE (b): `request_magic_link` is not yet deployed in - * `constructive_auth_public` and `useRequestMagicLinkMutation` does NOT exist - * in the reference SDK. This block therefore: - * • Does NOT import from `@/generated/auth` (no hook to import — tsc would fail). - * • Makes `onSubmit` the primary/recommended network path: the host wires the - * generated binding after running `cnc codegen --api-names auth ...`. - * • The stub default path throws a typed PROCEDURE_NOT_FOUND error so the - * block behaves gracefully (shows the error message) if mounted without the override. - * • `requires.json` names the pending op so `check-sdk-fixtures.ts` fails clearly. - * • `PROCEDURE_NOT_FOUND` is in `messages.errors`. - * - * When the backend ships and the host regenerates the SDK, replace the stub - * `defaultResend` with: - * const defaultMutation = useRequestMagicLinkMutation({ selection: {} }); - * async function defaultResend(vars: RequestMagicLinkVars) { - * await defaultMutation.mutateAsync({ email: vars.email }); - * } - * and add the hybrid-isPending pattern (see sdk-binding-contract.md §5). - * - * Email is read from `?email=` searchParam (via useSearchParams) or - * sessionStorage (fallback for direct navigation / bookmark). - * - * Pages MAY import `next/navigation`; Cards MUST NOT (block-contract.md §6). - * - * Editable constants after install: - * const MAGIC_LINK_REQUEST_PATH = '/auth/magic-link'; - * const SIGN_IN_PATH = '/auth/sign-in'; - * const RESEND_COOLDOWN_SECONDS = 60; - */ - -import { useCallback, useEffect, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; - -import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; - -import { defaultMagicLinkSentPageMessages, type MagicLinkSentPageMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Editable constants (installed page — consumer modifies these in place) -// --------------------------------------------------------------------------- -const MAGIC_LINK_REQUEST_PATH = '/auth/magic-link'; -const SIGN_IN_PATH = '/auth/sign-in'; -const RESEND_COOLDOWN_SECONDS = 60; - -// --------------------------------------------------------------------------- -// Simple {{key}} mustache interpolation (no dep needed) -// --------------------------------------------------------------------------- - -function interpolate(template: string, vars: Record): string { - return template.replace(/\{\{(\w+)\}\}/g, (_, key) => String(vars[key] ?? '')); -} - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -/** Variables sent to the resend request. The override `onSubmit` gets these. */ -export type RequestMagicLinkVars = { - email: string; -}; - -/** Result of the resend operation. Void on the wire; null here for the override seam. */ -export type RequestMagicLinkResult = null; - -export type MagicLinkSentPageMessageOverrides = Partial> & { - errors?: Partial; -}; - -export type MagicLinkSentPageProps = { - messages?: MagicLinkSentPageMessageOverrides; - /** - * Replace the default resend call. - * Required until `request_magic_link` ships in `constructive_auth_public`. - * After codegen, the host wires in `useRequestMagicLinkMutation`. - */ - onSubmit?: (vars: RequestMagicLinkVars) => Promise; - /** Fires after a successful resend. Always fires. */ - onSuccess?: (result: RequestMagicLinkResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Stub default path (backend-pending) -// Throws PROCEDURE_NOT_FOUND so the error message surfaces in the UI. -// Replace this with the generated hook once `request_magic_link` ships. -// --------------------------------------------------------------------------- - -class ProcedureNotFoundError extends Error { - public readonly extensions = { code: 'PROCEDURE_NOT_FOUND' }; - constructor() { - super('PROCEDURE_NOT_FOUND'); - this.name = 'ProcedureNotFoundError'; - } -} - -async function stubResend(_vars: RequestMagicLinkVars): Promise { - throw new ProcedureNotFoundError(); -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export default function MagicLinkSentPage({ - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MagicLinkSentPageProps) { - // Deep merge: top-level copy + errors map merged separately. - const merged: MagicLinkSentPageMessages = { - ...defaultMagicLinkSentPageMessages, - ...messageOverrides, - errors: { ...defaultMagicLinkSentPageMessages.errors, ...messageOverrides?.errors } - }; - - const searchParams = useSearchParams(); - - // Read email from ?email= searchParam; fall back to sessionStorage for direct nav. - const [email] = useState(() => { - const param = searchParams.get('email'); - if (param) return decodeURIComponent(param); - if (typeof sessionStorage !== 'undefined') { - return sessionStorage.getItem('magic-link-email'); - } - return null; - }); - - // The default resend is backend-pending (stub throws PROCEDURE_NOT_FOUND). - // When the host provides `onSubmit`, that path is used instead. - const resendFn = onSubmitOverride ?? stubResend; - - // Track pending state manually (stub is sync-throw; override may be async). - const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); - const [resendSuccess, setResendSuccess] = useState(false); - - // --------------------------------------------------------------------------- - // Countdown timer - // --------------------------------------------------------------------------- - - const [cooldownRemaining, setCooldownRemaining] = useState(0); - - const startCooldown = useCallback(() => { - setCooldownRemaining(RESEND_COOLDOWN_SECONDS); - }, []); - - useEffect(() => { - if (cooldownRemaining <= 0) return; - const timeout = setTimeout(() => { - setCooldownRemaining((seconds) => Math.max(0, seconds - 1)); - }, 1000); - return () => clearTimeout(timeout); - }, [cooldownRemaining]); - - // --------------------------------------------------------------------------- - // Resend handler - // --------------------------------------------------------------------------- - - async function handleResend() { - if (!email || isPending || cooldownRemaining > 0) return; - setError(null); - setResendSuccess(false); - setIsPending(true); - try { - await resendFn({ email }); - setResendSuccess(true); - startCooldown(); - onMessage?.({ kind: 'success', key: 'requestMagicLink.success', message: merged.resendSuccess }); - onSuccess?.(null); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setIsPending(false); - } - } - - // --------------------------------------------------------------------------- - // Resend button label - // --------------------------------------------------------------------------- - - function resendButtonLabel(): string { - if (isPending) return merged.resendPending; - if (cooldownRemaining > 0) return interpolate(merged.resendCooldown, { seconds: cooldownRemaining }); - return merged.resendButton; - } - - const isResendDisabled = !email || isPending || cooldownRemaining > 0; - - // --------------------------------------------------------------------------- - // JSX - // --------------------------------------------------------------------------- - - return ( -
- - - {merged.title} - {email ? ( - {interpolate(merged.description, { email })} - ) : ( - {merged.description.replace('{{email}}', 'your email address')} - )} - - - - - - {resendSuccess && ( -

- {merged.resendSuccess} -

- )} - - - {resendButtonLabel()} - - - -
- - - - -
-
- ); -} diff --git a/apps/blocks/src/blocks/auth/magic-link-sent-page/messages.ts b/apps/blocks/src/blocks/auth/magic-link-sent-page/messages.ts deleted file mode 100644 index c098a92..0000000 --- a/apps/blocks/src/blocks/auth/magic-link-sent-page/messages.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * magic-link-sent-page — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * Runtime interpolation: {{email}} in `description`, {{seconds}} in `resendCooldown`. - */ - -export type MagicLinkSentPageMessages = { - title: string; - /** Runtime interpolation: {{email}} */ - description: string; - resendButton: string; - resendPending: string; - /** Runtime interpolation: {{seconds}} */ - resendCooldown: string; - resendSuccess: string; - differentEmailLink: string; - signInLink: string; - /** Error messages — UPPER_SNAKE_CASE keys match err.extensions.code from PostGraphile */ - errors: { - RATE_LIMITED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultMagicLinkSentPageMessages: MagicLinkSentPageMessages = { - title: 'Check your email', - description: 'We sent a sign-in link to {{email}}. The link expires in a few minutes.', - resendButton: 'Resend email', - resendPending: 'Resending…', - resendCooldown: 'Resend in {{seconds}}s', - resendSuccess: 'Email resent. Check your inbox.', - differentEmailLink: 'Use a different email', - signInLink: 'Back to sign in', - errors: { - RATE_LIMITED: 'Too many requests. Please wait before trying again.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Failed to resend email. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-backup-codes-display/messages.ts b/apps/blocks/src/blocks/auth/mfa-backup-codes-display/messages.ts deleted file mode 100644 index d2f5908..0000000 --- a/apps/blocks/src/blocks/auth/mfa-backup-codes-display/messages.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * mfa-backup-codes-display — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy. This block has no error-code map because it is - * purely display-only — errors from clipboard/download APIs are handled inline. - * The `errors` object is minimal but present per block-contract pattern; it - * includes PROCEDURE_NOT_FOUND as a sentinel for future wiring (the backend - * `generate_backup_codes` proc is not yet deployed; this block's callers will - * surface that error when it ships, and a host can override this key). - */ - -export type MfaBackupCodesDisplayMessages = { - title: string; - description: string; - warningText: string; - copyAllButton: string; - copiedButton: string; - downloadButton: string; - confirmCheckboxLabel: string; - continueButton: string; - errors: { - /** - * Sentinel: the `generate_backup_codes` backend procedure is not yet - * deployed. Callers that wrap this block and call the future hook should - * surface this message when the mutation fails with PROCEDURE_NOT_FOUND. - */ - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export type MfaBackupCodesDisplayMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultMfaBackupCodesDisplayMessages: MfaBackupCodesDisplayMessages = { - title: 'Save your backup codes', - description: - 'If you lose access to your authenticator app, you can use one of these codes to sign in. Each code can only be used once.', - warningText: 'Store these codes somewhere safe. They will not be shown again.', - copyAllButton: 'Copy all', - copiedButton: 'Copied!', - downloadButton: 'Download as .txt', - confirmCheckboxLabel: 'I have saved my backup codes in a safe place.', - continueButton: 'Continue', - errors: { - PROCEDURE_NOT_FOUND: - 'Backup code generation is not available yet. Contact your administrator.', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-backup-codes-display/mfa-backup-codes-display.tsx b/apps/blocks/src/blocks/auth/mfa-backup-codes-display/mfa-backup-codes-display.tsx deleted file mode 100644 index 89e27f8..0000000 --- a/apps/blocks/src/blocks/auth/mfa-backup-codes-display/mfa-backup-codes-display.tsx +++ /dev/null @@ -1,203 +0,0 @@ -'use client'; - -/** - * mfa-backup-codes-display (registry: auth-mfa-backup-codes-display) - * - * Display-only card for one-time presentation of MFA backup codes. Codes are - * passed in via the `codes` prop by the caller (auth-mfa-totp-enroll or - * auth-mfa-backup-codes-regenerate); this block has NO generated-hook import, - * NO fetch, NO network call, and NO requires.json — it is purely presentational - * (sdk-binding-contract.md §7: presentational blocks ship no manifest). - * - * Affordances: - * • 2-column grid of monospace cells (selectable, 1-col on mobile). - * • "Copy all" — writes all codes as newline-separated text to the clipboard. - * • "Download as .txt" — client-side download, one code per line + header. - * • "I have saved…" checkbox gate + "Continue" button (when requireConfirmation=true). - * - * Accessibility: - * • Codes rendered as
  • for screen readers. - * • Copy/download buttons have aria-label. - * • Checkbox associated to label via htmlFor. - * • Continue button has aria-disabled="true" until checkbox is checked. - * - * NOTE: Backend generate_backup_codes procedure is future/undeployed. Codes are - * generated by the CALLER and passed in here — see the `codes` prop. - */ - -import { useState } from 'react'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; -import { Checkbox } from '@constructive-io/ui/checkbox'; -import { Badge } from '@constructive-io/ui/badge'; - -import { cn } from '@/lib/utils'; - -import { - defaultMfaBackupCodesDisplayMessages, - type MfaBackupCodesDisplayMessageOverrides -} from './messages'; - -export type MfaBackupCodesDisplayProps = { - /** The backup codes to display. Returned from generate_backup_codes() by the caller. */ - codes: string[]; - /** - * When true (default), renders an "I have saved these codes" checkbox gate. - * The onConfirm callback fires only after the user checks the box and clicks Continue. - */ - requireConfirmation?: boolean; - /** Fires when user confirms they have saved the codes (clicks Continue). */ - onConfirm?: () => void; - /** Notification seam — fires for the confirm event. Always fires when onConfirm is called. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - messages?: MfaBackupCodesDisplayMessageOverrides; - className?: string; -}; - -export function MfaBackupCodesDisplay({ - codes, - requireConfirmation = true, - onConfirm, - onMessage, - messages: messageOverrides, - className -}: MfaBackupCodesDisplayProps) { - // Deep merge: top-level copy + the errors map merged separately. - const merged = { - ...defaultMfaBackupCodesDisplayMessages, - ...messageOverrides, - errors: { - ...defaultMfaBackupCodesDisplayMessages.errors, - ...messageOverrides?.errors - } - }; - - const [copied, setCopied] = useState(false); - const [confirmed, setConfirmed] = useState(false); - - function handleCopyAll() { - const text = codes.join('\n'); - navigator.clipboard.writeText(text).then(() => { - setCopied(true); - // Reset the "Copied!" label after 2 seconds - setTimeout(() => setCopied(false), 2000); - }); - } - - function handleDownload() { - const header = '# Backup codes for your account — keep these safe\n\n'; - const body = codes.join('\n'); - const blob = new Blob([header + body], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const anchor = document.createElement('a'); - anchor.href = url; - anchor.download = 'backup-codes.txt'; - anchor.click(); - URL.revokeObjectURL(url); - } - - function handleContinue() { - onMessage?.({ kind: 'success', key: 'backupCodes.confirmed' }); - onConfirm?.(); - } - - const continueDisabled = requireConfirmation && !confirmed; - - return ( - - - {merged.title} - {merged.description} - - - - {/* Warning badge — codes are shown once */} -
    - - Warning - -

    {merged.warningText}

    -
    - - {/* Code grid — 2 columns, 1 on mobile */} -
      - {codes.map((code, i) => ( -
    • - - {code} - -
    • - ))} -
    - - {/* Copy all + Download actions */} -
    - - -
    - - {/* Confirmation checkbox */} - {requireConfirmation && ( -
    - setConfirmed(checked === true)} - data-testid="confirm-checkbox" - /> - -
    - )} -
    - - {/* Continue button in footer */} - - - -
    - ); -} diff --git a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/auth-mfa-backup-codes-regenerate.requires.json b/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/auth-mfa-backup-codes-regenerate.requires.json deleted file mode 100644 index 82bbb14..0000000 --- a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/auth-mfa-backup-codes-regenerate.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["generateBackupCodes"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/messages.ts b/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/messages.ts deleted file mode 100644 index b597183..0000000 --- a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/messages.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * mfa-backup-codes-regenerate — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * PROCEDURE_NOT_FOUND is included because `generate_backup_codes` is - * backend-pending (sdk-binding-contract.md §10, CASE b). When the proc is - * deployed and codegen produces `useGenerateBackupCodesMutation`, the host wires - * the generated hook via the `onSubmit` seam; this error code will surface if the - * deployed proc somehow fails with PROCEDURE_NOT_FOUND. - */ - -export type MfaBackupCodesRegenerateMessages = { - title: string; - description: string; - warningText: string; - regenerateButton: string; - cancelButton: string; - generatingButton: string; - successMessage: string; - errors: { - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: consumers can override any top-level key and/or - * any individual error code without restating the full catalog. - */ -export type MfaBackupCodesRegenerateMessageOverrides = Partial< - Omit -> & { - errors?: Partial; -}; - -export const defaultMfaBackupCodesRegenerateMessages: MfaBackupCodesRegenerateMessages = { - title: 'Regenerate backup codes', - description: - 'Generate a new set of backup codes. Your old backup codes will stop working immediately.', - warningText: 'Make sure to save the new codes. Old codes cannot be recovered.', - regenerateButton: 'Regenerate backup codes', - cancelButton: 'Cancel', - generatingButton: 'Generating…', - successMessage: 'Backup codes regenerated successfully.', - errors: { - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/mfa-backup-codes-regenerate.tsx b/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/mfa-backup-codes-regenerate.tsx deleted file mode 100644 index 40545bd..0000000 --- a/apps/blocks/src/blocks/auth/mfa-backup-codes-regenerate/mfa-backup-codes-regenerate.tsx +++ /dev/null @@ -1,250 +0,0 @@ -'use client'; - -/** - * mfa-backup-codes-regenerate (registry: auth-mfa-backup-codes-regenerate) - * - * Confirmation dialog → step-up → generate_backup_codes() → display new codes - * via [[auth-mfa-backup-codes-display]]. Used in account security settings when - * the user wants to rotate their backup codes. - * - * BACKEND-PENDING (CASE b): `generate_backup_codes` is NOT yet deployed to - * `constructive_auth_public`. The generated hook `useGenerateBackupCodesMutation` - * does not exist in the current SDK — importing it would fail tsc. Consequently: - * • The `@/generated/auth` import for that hook is OMITTED. - * • `onSubmit` is REQUIRED (no default mutation path until the proc ships). - * • When the backend deploys and codegen regenerates, the host replaces the - * `onSubmit` prop with the generated hook binding. - * • requires.json names `generateBackupCodes` so `check-sdk-fixtures.ts` fails clearly. - * • messages.errors.PROCEDURE_NOT_FOUND is present for when the proc first lands. - * - * Flow: - * 1. Confirmation state — dialog open; warns that old codes are immediately invalidated. - * 2. Step-up — `await stepUp({ tier: 'high' })`. Cancel returns silently. - * 3. Generating — calls onSubmit() (the mutation adapter). Shows loading state. - * 4. Display — renders [[auth-mfa-backup-codes-display]] with new codes. - * onSuccess fires when user confirms codes are saved. - * - * Binding doctrine: sdk-binding-contract.md §5–§7, MASTER-PROMPT §5. - * Step-up: step-up-contract.md §3 — `tier: 'high'` gates the regenerate action. - * STEP_UP_CANCELLED → silent return (no error callbacks, no toast). - */ - -import { useState } from 'react'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; -import { MfaBackupCodesDisplay } from '@/blocks/auth/mfa-backup-codes-display/mfa-backup-codes-display'; - -import { - defaultMfaBackupCodesRegenerateMessages, - type MfaBackupCodesRegenerateMessages, - type MfaBackupCodesRegenerateMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -/** The result type returned by the onSubmit adapter and passed to onSuccess. */ -export type MfaBackupCodesRegenerateResult = { - codes: string[]; -}; - -export type MfaBackupCodesRegenerateProps = { - /** Controlled open state. */ - open: boolean; - /** Called when the dialog requests open-state change (close on cancel/X). */ - onOpenChange: (open: boolean) => void; - messages?: MfaBackupCodesRegenerateMessageOverrides; - /** - * Adapter override — REQUIRED until `generate_backup_codes` is deployed to - * the backend and codegen regenerates `useGenerateBackupCodesMutation`. - * - * Expected signature: - * `onSubmit={async () => { const d = await generateBackupCodes.mutateAsync({}); return d.generateBackupCodes; }}` - * - * Returns: `{ codes: string[] }` — the newly generated backup codes. - */ - onSubmit: () => Promise; - /** Fires after the user confirms codes are saved (clicks Continue in the display step). Always fires. */ - onSuccess?: (result: MfaBackupCodesRegenerateResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. Always fires. */ - onMessage?: (event: { - kind: 'success' | 'error' | 'info' | 'warning'; - key: string; - message?: string; - }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function MfaBackupCodesRegenerate({ - open, - onOpenChange, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MfaBackupCodesRegenerateProps) { - // Deep merge: top-level copy + the errors map merged separately. - const merged: MfaBackupCodesRegenerateMessages = { - ...defaultMfaBackupCodesRegenerateMessages, - ...messageOverrides, - errors: { - ...defaultMfaBackupCodesRegenerateMessages.errors, - ...messageOverrides?.errors - } - }; - - const stepUp = useStepUp(); - const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); - /** Codes returned after a successful regeneration — drives the display step. */ - const [codes, setCodes] = useState(null); - - async function handleRegenerate() { - setError(null); - try { - // Step-up must complete before the mutation fires. - // tier:'high' → MFA if enrolled (TOTP is active at this point), else password. - await stepUp({ tier: 'high' }); - - // Proceed with the mutation via the override seam (BACKEND-PENDING). - setIsPending(true); - const result = await onSubmitOverride(); - - // Transition to display step — show codes via [[auth-mfa-backup-codes-display]]. - setCodes(result.codes); - } catch (err) { - // STEP_UP_CANCELLED: user dismissed the step-up dialog — silent return. - if (err instanceof StepUpError && err.reason === 'cancelled') return; - - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setIsPending(false); - } - } - - function handleCancel() { - if (isPending) return; - setError(null); - onOpenChange(false); - } - - function handleCodesConfirmed() { - // User confirmed codes are saved — fire success and close. - const result: MfaBackupCodesRegenerateResult = { codes: codes! }; - onMessage?.({ - kind: 'success', - key: 'generateBackupCodes.success', - message: merged.successMessage - }); - onSuccess?.(result); - // Reset dialog state and close. - setCodes(null); - onOpenChange(false); - } - - return ( - { - if (!isOpen) handleCancel(); - }} - > - - {codes !== null ? ( - // Display step — show new codes via [[auth-mfa-backup-codes-display]]. - // The display component handles "I have saved these" confirmation gate. - - ) : ( - // Confirmation step — warn about invalidation before regenerating. - <> - - - {merged.title} - - - {merged.description} - - - -
    - {/* Prominent warning — old codes invalidated immediately */} -
    -

    {merged.warningText}

    -
    - - {/* Async error alert (aria-live="polite" is inside AuthErrorAlert) */} - -
    - - - {/* Cancel button receives initial focus (safer default for destructive action) */} - - - - {merged.regenerateButton} - - - - )} -
    -
    - ); -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/messages.ts b/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/messages.ts deleted file mode 100644 index 35b45e1..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/messages.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * mfa-totp-challenge-page — message catalog - * - * Canonical block-messages pattern (block-contract.md §4): top-level camelCase - * keys are UI copy for the page's own error states (missing token, expired token). - * The card's own messages are handled by auth-mfa-totp-challenge — these are - * only for the page wrapper states. - */ - -export type MfaTotpChallengePageMessages = { - /** Shown when ?token= is absent from the URL */ - missingTokenTitle: string; - missingTokenDescription: string; - missingTokenCta: string; - /** Shown when the challenge token has expired (EXPIRED_TOKEN from the card's onError) */ - expiredTokenTitle: string; - expiredTokenDescription: string; - expiredTokenCta: string; -}; - -export const defaultMfaTotpChallengePageMessages: MfaTotpChallengePageMessages = { - missingTokenTitle: 'Invalid link', - missingTokenDescription: 'This sign-in link is missing required parameters. Please sign in again.', - missingTokenCta: 'Back to sign in', - expiredTokenTitle: 'Session expired', - expiredTokenDescription: - 'Your sign-in session has expired. Please sign in again to get a new verification link.', - expiredTokenCta: 'Sign in again' -}; diff --git a/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/mfa-totp-challenge-page.tsx b/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/mfa-totp-challenge-page.tsx deleted file mode 100644 index b993bf4..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-challenge-page/mfa-totp-challenge-page.tsx +++ /dev/null @@ -1,187 +0,0 @@ -'use client'; - -/** - * mfa-totp-challenge-page (registry: auth-mfa-totp-challenge-page) - * - * Thin Next.js page mounted at `/auth/mfa/totp`. Reads `?token=` and - * `?redirect=` from `useSearchParams()`, validates both, and mounts - * ``. Routes to `redirectTo` on success. - * - * Page states: - * ready — ?token= present; shows the MfaTotpChallenge card - * missing-token — ?token= absent; shows error card - * expired — card fires onError with code EXPIRED_TOKEN; shows error card - * - * This block calls NO generated hook directly. All mutation logic is delegated - * to auth-mfa-totp-challenge. No requires.json is shipped (sdk-binding-contract §7). - * - * Pages MAY use `next/navigation`; Cards MUST NOT (block-contract.md §6). - * - * Editable constants after install: - * const DEFAULT_REDIRECT = '/dashboard'; - * const SIGN_IN_PATH = '/auth/sign-in'; - */ - -import { useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; - -import { Card, CardContent, CardHeader, CardDescription } from '@constructive-io/ui/card'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { MfaTotpChallenge } from '@/blocks/auth/mfa-totp-challenge/mfa-totp-challenge'; -import type { MfaChallengeResult, MfaTotpChallengeVars } from '@/blocks/auth/mfa-totp-challenge/mfa-totp-challenge'; - -import { - defaultMfaTotpChallengePageMessages, - type MfaTotpChallengePageMessages -} from './messages'; - -// --------------------------------------------------------------------------- -// Editable constants (installed page — consumer modifies these in place) -// --------------------------------------------------------------------------- -const DEFAULT_REDIRECT = '/dashboard'; -const SIGN_IN_PATH = '/auth/sign-in'; - -// --------------------------------------------------------------------------- -// Open-redirect guard (same pattern as auth-sign-in-page) -// --------------------------------------------------------------------------- - -/** - * Returns `redirect` only when it resolves to the same origin as the current - * page. External URLs — including absolute URLs, protocol-relative, and path- - * encoded bypasses — are silently replaced with `fallback`. - */ -function safeRedirect(redirect: string | null | undefined, fallback: string): string { - if (!redirect) return fallback; - try { - const url = new URL(redirect, window.location.origin); - return url.origin === window.location.origin ? redirect : fallback; - } catch { - return fallback; - } -} - -// --------------------------------------------------------------------------- -// Internal page state -// --------------------------------------------------------------------------- - -type PageState = 'ready' | 'missing-token' | 'expired'; - -// --------------------------------------------------------------------------- -// Message override type (deep-partial) -// --------------------------------------------------------------------------- - -export type MfaTotpChallengePageMessageOverrides = Partial; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export default function MfaTotpChallengePage({ - messages: messageOverrides, - onSubmit: onSubmitOverride, - className -}: { - messages?: MfaTotpChallengePageMessageOverrides; - /** - * Override seam — passed through to so tests (and consumers - * who want an early integration before the generated hook ships) can inject a - * custom submit handler. Mirrors the card's own onSubmit prop. - */ - onSubmit?: (vars: MfaTotpChallengeVars) => Promise; - className?: string; -}) { - const router = useRouter(); - const searchParams = useSearchParams(); - - // Merge messages - const merged: MfaTotpChallengePageMessages = { - ...defaultMfaTotpChallengePageMessages, - ...messageOverrides - }; - - // Read ?token= and ?redirect= from URL - const rawToken = searchParams.get('token'); - const rawRedirect = searchParams.get('redirect'); - const redirectTo = safeRedirect(rawRedirect ? decodeURIComponent(rawRedirect) : null, DEFAULT_REDIRECT); - - // Page state — starts in 'missing-token' if no token, otherwise 'ready' - const [pageState, setPageState] = useState(rawToken ? 'ready' : 'missing-token'); - - function handleSuccess(_result: MfaChallengeResult) { - // _result is unused today; when the backend ships, extract result.redirectTo - // as a server-provided redirect hint (takes precedence over ?redirect= param). - router.push(redirectTo); - } - - function handleError(err: { message: string; code: string }) { - if (err.code === 'EXPIRED_TOKEN') { - setPageState('expired'); - } - } - - // --------------------------------------------------------------------------- - // Error state rendering (missing-token or expired) - // --------------------------------------------------------------------------- - - if (pageState === 'missing-token') { - return ( -
    - - -

    {merged.missingTokenTitle}

    - {merged.missingTokenDescription} -
    - - - -
    -
    - ); - } - - if (pageState === 'expired') { - return ( -
    - - -

    {merged.expiredTokenTitle}

    - {merged.expiredTokenDescription} -
    - - - -
    -
    - ); - } - - // --------------------------------------------------------------------------- - // Ready state — mount the MFA challenge card - // --------------------------------------------------------------------------- - - return ( -
    - -
    - ); -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-challenge/auth-mfa-totp-challenge.requires.json b/apps/blocks/src/blocks/auth/mfa-totp-challenge/auth-mfa-totp-challenge.requires.json deleted file mode 100644 index a9a0f03..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-challenge/auth-mfa-totp-challenge.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["completeMfaChallenge"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-challenge/messages.ts b/apps/blocks/src/blocks/auth/mfa-totp-challenge/messages.ts deleted file mode 100644 index c89c685..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-challenge/messages.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * mfa-totp-challenge — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * PROCEDURE_NOT_FOUND is included because `complete_mfa_challenge` is backend- - * pending (sdk-binding-contract.md §10). It will surface until the procedure - * is deployed and codegen regenerated. - */ - -export type MfaTotpChallengeMessages = { - title: string; - description: string; - codeLabel: string; - codePlaceholder: string; - trustDeviceLabel: string; - trustDeviceHint: string; - submitButton: string; - loadingLabel: string; - backupCodeLink: string; - successToast: string; - errors: { - INVALID_TOTP: string; - EXPIRED_TOKEN: string; - RATE_LIMITED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -export const defaultMfaTotpChallengeMessages: MfaTotpChallengeMessages = { - title: 'Two-factor authentication', - description: 'Enter the 6-digit code from your authenticator app.', - codeLabel: 'Authentication code', - codePlaceholder: '000000', - trustDeviceLabel: 'Trust this device for 30 days', - trustDeviceHint: 'Skip two-factor on this device for 30 days.', - submitButton: 'Verify', - loadingLabel: 'Verifying...', - backupCodeLink: 'Use a backup code instead', - successToast: 'Verified successfully.', - errors: { - INVALID_TOTP: 'Invalid code. Check your authenticator app and try again.', - EXPIRED_TOKEN: 'Your session expired. Please sign in again.', - RATE_LIMITED: 'Too many attempts. Please wait before trying again.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-totp-challenge/mfa-totp-challenge.tsx b/apps/blocks/src/blocks/auth/mfa-totp-challenge/mfa-totp-challenge.tsx deleted file mode 100644 index 3a5bc0d..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-challenge/mfa-totp-challenge.tsx +++ /dev/null @@ -1,315 +0,0 @@ -'use client'; - -/** - * mfa-totp-challenge (registry: auth-mfa-totp-challenge) - * - * Presents a 6-digit TOTP code input when `sign_in` returns - * `mfa_required=true` with a non-null `mfa_challenge_token`. - * - * BACKEND-PENDING CASE (b): `complete_mfa_challenge` is not yet deployed in - * `constructive_auth_public`, so `useCompleteMfaChallengeMutation` does NOT - * exist in the generated `auth` SDK. To keep tsc clean the import from - * `@/generated/auth` is OMITTED here. The `onSubmit` override seam is the - * primary call path (the host wires the generated binding after they - * regenerate the SDK). Until then PROCEDURE_NOT_FOUND surfaces at runtime. - * See: sdk-binding-contract.md §10, planning/blocks/auth/auth-mfa-totp-challenge.md - * - * Data-binding contract (sdk-binding-contract.md §5): - * Hook: useCompleteMfaChallengeMutation (pending — not in SDK yet) - * Namespace: auth - * Import (when deployed): import { useCompleteMfaChallengeMutation } from '@/generated/auth' - * Op: completeMfaChallenge - * Payload key: d.completeMfaChallenge - */ - -import { useState } from 'react'; -import { useForm } from '@tanstack/react-form'; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@constructive-io/ui/card'; -import { Checkbox } from '@constructive-io/ui/checkbox'; - -import { Input } from '@constructive-io/ui/input'; -import { FormControl } from '@constructive-io/ui/form-control'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; - -import { defaultMfaTotpChallengeMessages, type MfaTotpChallengeMessages } from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -/** The session/user payload returned after a successful MFA challenge. */ -export type MfaChallengeResult = { - session: { id: string; accessToken: string; expiresAt: string }; - user: { id: string; [key: string]: unknown }; - redirectTo?: string; -}; - -/** Vars the submit call receives. The override `onSubmit` gets these verbatim. */ -export type MfaTotpChallengeVars = { - totpValue: string; - trustDevice: boolean; - challengeToken: string; - mfaMethod: string; - credentialKind: string; - deviceToken?: string; - rememberMe?: boolean; -}; - -/** - * Deep-partial message override type: top-level keys are shallow-partial; - * `errors` is itself partial so a host can localize a single error code without - * restating the whole map. - */ -export type MfaTotpChallengeMessageOverrides = Partial> & { - errors?: Partial; -}; - -// --------------------------------------------------------------------------- -// Internal form values shape -// --------------------------------------------------------------------------- - -type TotpFormData = { - totpCode: string; - trustDevice: boolean; -}; - -// --------------------------------------------------------------------------- -// Props -// --------------------------------------------------------------------------- - -export type MfaTotpChallengeProps = { - /** The mfa_challenge_token from the sign_in result. Required. */ - challengeToken: string; - /** The mfa_method to pass to complete_mfa_challenge. Default: 'totp'. */ - mfaMethod?: string; - /** The credential_kind to use for session creation. Default: 'bearer'. */ - credentialKind?: string; - /** Whether the "Trust this device for 30 days" checkbox is shown. Default: true. */ - showTrustDevice?: boolean; - /** - * Backup-code path affordance — locked false in v1; enable when - * verify_backup_code lands. See: backend-spec/future-procedures.md - */ - allowBackupCode?: false; - messages?: MfaTotpChallengeMessageOverrides; - /** - * Replace the default `useCompleteMfaChallengeMutation` call (backend-pending). - * In v1 this is the PRIMARY path — the host provides a custom implementation - * until `complete_mfa_challenge` is deployed. - */ - onSubmit?: (vars: MfaTotpChallengeVars) => Promise; - /** Fires after MFA challenge completed and session is active. Always fires. */ - onSuccess?: (result: MfaChallengeResult) => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function MfaTotpChallenge({ - challengeToken, - mfaMethod = 'totp', - credentialKind = 'bearer', - showTrustDevice = true, - // allowBackupCode is locked false in v1 (backup-code feature is backend-pending) - allowBackupCode: _allowBackupCode = false, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MfaTotpChallengeProps) { - // Deep merge: top-level copy + the errors map merged separately. - const merged: MfaTotpChallengeMessages = { - ...defaultMfaTotpChallengeMessages, - ...messageOverrides, - errors: { ...defaultMfaTotpChallengeMessages.errors, ...messageOverrides?.errors } - }; - - /** - * BACKEND-PENDING CASE (b): the generated hook does not exist yet. - * When `complete_mfa_challenge` is deployed and codegen regenerated: - * 1. Add: import { useCompleteMfaChallengeMutation } from '@/generated/auth'; - * 2. Instantiate: const defaultMutation = useCompleteMfaChallengeMutation({ selection: ... }); - * 3. Wire hybrid isPending: onSubmitOverride ? overridePending : defaultMutation.isPending - * 4. The `if (onSubmitOverride) setOverridePending(true/false)` guards in handleVerify - * MUST stay — they prevent double-pending when both the override and the default - * hook are present (matches gold-standard sign-in-card pattern). - */ - const [overridePending, setOverridePending] = useState(false); - // When the generated hook lands, this becomes: - // onSubmitOverride ? overridePending : defaultMutation.isPending - const isPending = overridePending; - - const [error, setError] = useState(null); - - async function runChallenge(vars: MfaTotpChallengeVars): Promise { - if (onSubmitOverride) { - return onSubmitOverride(vars); - } - // PROCEDURE_NOT_FOUND: complete_mfa_challenge is not yet deployed. - // The generated hook will be wired here once the procedure ships. - const procedureErr = Object.assign(new Error('complete_mfa_challenge is not yet deployed'), { - extensions: { code: 'PROCEDURE_NOT_FOUND' } - }); - throw procedureErr; - } - - async function handleVerify(values: TotpFormData) { - setError(null); - if (onSubmitOverride) setOverridePending(true); - try { - const vars: MfaTotpChallengeVars = { - totpValue: values.totpCode.replace(/[\s-]/g, ''), - trustDevice: values.trustDevice, - challengeToken, - mfaMethod, - credentialKind, - rememberMe: values.trustDevice - }; - const result = await runChallenge(vars); - onMessage?.({ kind: 'success', key: 'completeMfaChallenge.success', message: merged.successToast }); - onSuccess?.(result); - } catch (err) { - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - if (onSubmitOverride) setOverridePending(false); - } - } - - const form = useForm({ - defaultValues: { - totpCode: '', - trustDevice: false - } as TotpFormData, - onSubmit: async ({ value }) => { - await handleVerify(value); - } - }); - - return ( - - - {merged.title} - {merged.description} - - - - - -
    { - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - > - { - if (!value) return 'Authentication code is required'; - const digits = value.replace(/[\s-]/g, ''); - if (!/^\d{6}$/.test(digits)) return 'Enter a 6-digit code'; - return undefined; - } - }} - > - {(field) => { - const errors = field.state.meta.errors?.filter(Boolean) ?? []; - const hasError = errors.length > 0; - const errorMessage = errors[0] as string | undefined; - return ( - - field.handleChange(e.target.value.replace(/[\s-]/g, '').slice(0, 6))} - onBlur={field.handleBlur} - /> - - ); - }} - - - {showTrustDevice && ( - - {(field) => ( -
    -
    - field.handleChange(checked === true)} - /> - -
    -

    {merged.trustDeviceHint}

    -
    - )} -
    - )} - - - {merged.submitButton} - -
    -
    - - {/* allowBackupCode is locked false in v1 — the backup-code affordance is a - v1.1 feature gated on verify_backup_code procedure. The backupCodeLink - message key is in the catalog now to avoid a breaking change later. */} - {false && ( - - - - )} -
    - ); -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/auth-mfa-totp-disable-confirm.requires.json b/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/auth-mfa-totp-disable-confirm.requires.json deleted file mode 100644 index 900ff9c..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/auth-mfa-totp-disable-confirm.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["disableTotp"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/messages.ts b/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/messages.ts deleted file mode 100644 index 90b29b2..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/messages.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * mfa-totp-disable-confirm — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * PROCEDURE_NOT_FOUND is included because `disable_totp` is backend-pending - * (sdk-binding-contract.md §10, CASE b). When the proc is deployed, this code - * will be resolved at runtime by the generated hook; until then, hosts using the - * onSubmit override seam will not see it. - */ - -export type MfaTotpDisableConfirmMessages = { - title: string; - description: string; - warningText: string; - backupCodesWarning: string; - confirmButton: string; - cancelButton: string; - loadingLabel: string; - successMessage: string; - errors: { - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Deep-partial override type: consumers can override any top-level key and/or - * any individual error code without restating the full catalog. - */ -export type MfaTotpDisableConfirmMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultMfaTotpDisableConfirmMessages: MfaTotpDisableConfirmMessages = { - title: 'Disable two-factor authentication', - description: 'This will remove the extra layer of security from your account.', - warningText: 'Your account will be less secure without two-factor authentication.', - backupCodesWarning: 'All backup codes will also be invalidated.', - confirmButton: 'Disable two-factor authentication', - cancelButton: 'Keep enabled', - loadingLabel: 'Disabling...', - successMessage: 'Two-factor authentication disabled.', - errors: { - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/mfa-totp-disable-confirm.tsx b/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/mfa-totp-disable-confirm.tsx deleted file mode 100644 index 3feebfa..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-disable-confirm/mfa-totp-disable-confirm.tsx +++ /dev/null @@ -1,189 +0,0 @@ -'use client'; - -/** - * mfa-totp-disable-confirm (registry: auth-mfa-totp-disable-confirm) - * - * Confirmation dialog for disabling TOTP. Requires high-severity step-up - * (`tier: 'high'`) before calling `disable_totp`. Shows prominent warnings about - * the security implications of removing two-factor authentication. - * - * BACKEND-PENDING (CASE b): `disable_totp` is NOT yet deployed to - * `constructive_auth_public`. The generated hook `useDisableTotpMutation` does - * not exist in the current SDK — importing it would fail tsc. Consequently: - * • The `@/generated/auth` import for that hook is OMITTED. - * • `onSubmit` is REQUIRED (no default mutation path). - * • When the backend deploys and codegen regenerates, the host replaces the - * `onSubmit` prop with the generated hook. - * • requires.json names `disableTotp` so `check-sdk-fixtures.ts` fails clearly. - * • messages.errors.PROCEDURE_NOT_FOUND is present for when the proc first lands. - * - * Binding doctrine: sdk-binding-contract.md §5–§7, MASTER-PROMPT §5. - * Step-up: step-up-contract.md §3 — `tier: 'high'` gates the confirm action. - * STEP_UP_CANCELLED → silent return (no error callbacks, no toast). - */ - -import { useState } from 'react'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription -} from '@constructive-io/ui/dialog'; -import { Button } from '@constructive-io/ui/button'; - -import { cn } from '@/lib/utils'; -import { parseGraphQLError } from '@/blocks/lib/auth-errors'; -import { AuthErrorAlert } from '@/blocks/primitives/auth-error-alert'; -import { AuthLoadingButton } from '@/blocks/primitives/auth-loading-button'; -import { useStepUp, StepUpError } from '@/blocks/auth/use-step-up/use-step-up'; - -import { - defaultMfaTotpDisableConfirmMessages, - type MfaTotpDisableConfirmMessages, - type MfaTotpDisableConfirmMessageOverrides -} from './messages'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type MfaTotpDisableConfirmProps = { - /** Controlled open state. */ - open: boolean; - /** Called when the dialog requests open-state change (close on cancel/X). */ - onOpenChange: (open: boolean) => void; - messages?: MfaTotpDisableConfirmMessageOverrides; - /** - * Adapter override — REQUIRED until `disable_totp` is deployed to the backend - * and codegen regenerates `useDisableTotpMutation`. After the proc ships, the - * host can wire the generated hook here: - * `onSubmit={async () => { await disableTotp.mutateAsync({}); }}` - * Until then, pass a mock or real implementation. - */ - onSubmit: () => Promise; - /** Fires after a successful disable. Always fires. */ - onSuccess?: () => void; - /** Fires after a mapped error. Always fires. */ - onError?: (err: { message: string; code: string }) => void; - /** Notification seam — fires for success and mapped errors. Always fires. */ - onMessage?: (event: { kind: 'success' | 'error' | 'info' | 'warning'; key: string; message?: string }) => void; - className?: string; -}; - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -export function MfaTotpDisableConfirm({ - open, - onOpenChange, - messages: messageOverrides, - onSubmit: onSubmitOverride, - onSuccess, - onError, - onMessage, - className -}: MfaTotpDisableConfirmProps) { - // Deep merge: top-level copy + the errors map merged separately. - const merged: MfaTotpDisableConfirmMessages = { - ...defaultMfaTotpDisableConfirmMessages, - ...messageOverrides, - errors: { ...defaultMfaTotpDisableConfirmMessages.errors, ...messageOverrides?.errors } - }; - - const stepUp = useStepUp(); - const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); - - async function handleConfirm() { - setError(null); - try { - // Step-up must complete before the disable mutation fires. - // tier:'high' → MFA if enrolled (user has TOTP, so this is correct), else password. - await stepUp({ tier: 'high' }); - - // Proceed with the disable mutation via the override seam (BACKEND-PENDING). - setIsPending(true); - await onSubmitOverride(); - - onMessage?.({ kind: 'success', key: 'disableTotp.success', message: merged.successMessage }); - onSuccess?.(); - onOpenChange(false); - } catch (err) { - // STEP_UP_CANCELLED: user dismissed the step-up dialog — silent return. - if (err instanceof StepUpError && err.reason === 'cancelled') return; - - const { code, message } = parseGraphQLError(err, { - customMessages: merged.errors, - defaultMessage: merged.errors.UNKNOWN_ERROR - }); - const key = code ?? 'UNKNOWN_ERROR'; - setError(message); - onMessage?.({ kind: 'error', key, message }); - onError?.({ message, code: key }); - } finally { - setIsPending(false); - } - } - - function handleCancel() { - if (isPending) return; - setError(null); - onOpenChange(false); - } - - return ( - { if (!isOpen) handleCancel(); }}> - - - {merged.title} - {merged.description} - - -
    - {/* Prominent security warning — both texts visible simultaneously per spec */} -
    -

    {merged.warningText}

    -

    {merged.backupCodesWarning}

    -
    - - {/* Async error alert (aria-live="polite" is inside AuthErrorAlert) */} - -
    - - - {/* Cancel button receives initial focus (safer default for destructive action) */} - - - - {merged.confirmButton} - - -
    -
    - ); -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-enroll/auth-mfa-totp-enroll.requires.json b/apps/blocks/src/blocks/auth/mfa-totp-enroll/auth-mfa-totp-enroll.requires.json deleted file mode 100644 index ad5df1a..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-enroll/auth-mfa-totp-enroll.requires.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "namespace": "auth", - "mutations": ["enableTotp", "confirmTotpSetup", "generateBackupCodes"], - "queries": [], - "models": [] -} diff --git a/apps/blocks/src/blocks/auth/mfa-totp-enroll/messages.ts b/apps/blocks/src/blocks/auth/mfa-totp-enroll/messages.ts deleted file mode 100644 index 9224e48..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-enroll/messages.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * mfa-totp-enroll — message catalog - * - * Canonical block-messages pattern (block-contract.md §4, §10): top-level - * camelCase keys are UI copy; the nested `errors` map is keyed by backend error - * CODE (UPPER_SNAKE_CASE) and is handed straight to `parseGraphQLError` as - * `customMessages`, so a host localizes any code by overriding a single key. - * - * PROCEDURE_NOT_FOUND is included because all three ops (enableTotp, - * confirmTotpSetup, generateBackupCodes) are backend-pending — they will surface - * this code at runtime until the procedures are deployed. - */ - -export type MfaTotpEnrollMessages = { - // Step 1: Setup - setupTitle: string; - setupDescription: string; - qrInstructions: string; - manualEntryLabel: string; - nextButton: string; - // Step 2: Verify - verifyTitle: string; - verifyDescription: string; - codeLabel: string; - codePlaceholder: string; - verifyButton: string; - verifyingButton: string; - backButton: string; - // Step 3: Backup codes (delegated to auth-mfa-backup-codes-display) - // Shared - errors: { - INVALID_TOTP: string; - RATE_LIMITED: string; - PROCEDURE_NOT_FOUND: string; - UNKNOWN_ERROR: string; - }; -}; - -/** - * Override type: top-level is shallow-partial; `errors` is itself partial so a - * host can localize a single error code without restating the map. - */ -export type MfaTotpEnrollMessageOverrides = Partial> & { - errors?: Partial; -}; - -export const defaultMfaTotpEnrollMessages: MfaTotpEnrollMessages = { - setupTitle: 'Set up two-factor authentication', - setupDescription: 'Scan the QR code with your authenticator app, then enter the code it shows.', - qrInstructions: 'Or enter this key manually into your authenticator app:', - manualEntryLabel: 'Manual entry key', - nextButton: 'Next', - verifyTitle: 'Verify your authenticator', - verifyDescription: 'Enter the 6-digit code from your authenticator app to confirm setup.', - codeLabel: 'Verification code', - codePlaceholder: '000000', - verifyButton: 'Verify and enable', - verifyingButton: 'Verifying…', - backButton: 'Back', - errors: { - INVALID_TOTP: 'Invalid code. Check your authenticator app and try again.', - RATE_LIMITED: 'Too many attempts. Please wait before trying again.', - PROCEDURE_NOT_FOUND: - 'This feature requires a backend update. See: https://constructive.io/docs/backend-spec/future-procedures', - UNKNOWN_ERROR: 'Something went wrong. Please try again.' - } -}; diff --git a/apps/blocks/src/blocks/auth/mfa-totp-enroll/mfa-totp-enroll.test.tsx b/apps/blocks/src/blocks/auth/mfa-totp-enroll/mfa-totp-enroll.test.tsx deleted file mode 100644 index 4bf7a0d..0000000 --- a/apps/blocks/src/blocks/auth/mfa-totp-enroll/mfa-totp-enroll.test.tsx +++ /dev/null @@ -1,454 +0,0 @@ -/** - * mfa-totp-enroll tests - * - * BACKEND-PENDING CASE (b): the three required hooks (useEnableTotpMutation, - * useConfirmTotpSetupMutation, useGenerateBackupCodesMutation) do NOT exist in - * the generated SDK yet — the procedures are undeployed. Therefore: - * • This block does NOT import from @/generated/auth (no hooks to mock). - * • Tests exercise the onSubmit/onConfirm/onGenerateCodes override path. - * • The "no adapters" path (graceful degradation) asserts PROCEDURE_NOT_FOUND. - * - * NOTE: vi.mock('@/generated/auth') is present as a no-op guard so that if a - * future developer accidentally adds a generated import, tests break clearly - * rather than silently hitting the real client. - */ - -import { Suspense } from 'react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, act } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -// No-op guard — this block currently imports nothing from @/generated/auth -// (CASE b), but the mock prevents any accidental real-client hit if that changes. -vi.mock('@/generated/auth', () => ({})); - -import { MfaTotpEnroll } from './mfa-totp-enroll'; -import { defaultMfaTotpEnrollMessages } from './messages'; -import { defaultMfaBackupCodesDisplayMessages } from '@/blocks/auth/mfa-backup-codes-display/messages'; - -// --------------------------------------------------------------------------- -// Shared adapter factories -// --------------------------------------------------------------------------- - -function makeSetupAdapter(overrides?: { qrUrl?: string; manualKey?: string }) { - return vi.fn().mockResolvedValue({ - qrUrl: overrides?.qrUrl ?? 'https://example.com/qr.png', - manualKey: overrides?.manualKey ?? 'ABCDEFGHIJKLMNOP' - }); -} - -function makeConfirmAdapter(result = true) { - return vi.fn().mockResolvedValue(result); -} - -function makeCodesAdapter(codes?: string[]) { - return vi.fn().mockResolvedValue(codes ?? ['abc-def-ghi', 'jkl-mno-pqr', 'stu-vwx-yz0']); -} - -function makeAdapters() { - return { - onSubmit: makeSetupAdapter(), - onConfirm: makeConfirmAdapter(), - onGenerateCodes: makeCodesAdapter() - }; -} - -function deferred() { - let resolve!: (value: T) => void; - let reject!: (reason?: unknown) => void; - const promise = new Promise((resolvePromise, rejectPromise) => { - resolve = resolvePromise; - reject = rejectPromise; - }); - return { promise, resolve, reject }; -} - -beforeEach(() => { - vi.clearAllMocks(); -}); - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('MfaTotpEnroll', () => { - it('renders setup step title on mount', async () => { - await act(async () => { - render(); - }); - expect(screen.getByText(defaultMfaTotpEnrollMessages.setupTitle)).toBeInTheDocument(); - }); - - it('calls onSubmit adapter on mount and shows QR image', async () => { - const onSubmit = makeSetupAdapter({ qrUrl: 'https://example.com/qr.png' }); - render(); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(screen.getByRole('img', { name: /qr code/i })).toBeInTheDocument()); - expect(screen.getByRole('img', { name: /qr code/i })).toHaveAttribute('src', 'https://example.com/qr.png'); - }); - - it('finishes one-time setup with the latest committed success callback', async () => { - const setup = deferred<{ qrUrl: string; manualKey: string }>(); - const initialSubmit = vi.fn(() => setup.promise); - const replacementSubmit = vi.fn().mockResolvedValue({ - qrUrl: 'https://example.com/replacement.png', - manualKey: 'REPLACEMENTKEY' - }); - const initialOnMessage = vi.fn(); - const latestOnMessage = vi.fn(); - - const { rerender } = render( - - ); - await waitFor(() => expect(initialSubmit).toHaveBeenCalledTimes(1)); - - rerender( - - ); - await act(async () => { - setup.resolve({ qrUrl: 'https://example.com/qr-latest.png', manualKey: 'LATESTCOMMITTEDKEY' }); - }); - - expect(initialSubmit).toHaveBeenCalledTimes(1); - expect(replacementSubmit).not.toHaveBeenCalled(); - expect(initialOnMessage).not.toHaveBeenCalled(); - expect(latestOnMessage).toHaveBeenCalledWith({ kind: 'info', key: 'qr_ready' }); - expect(screen.getByRole('img', { name: /qr code/i })).toHaveAttribute( - 'src', - 'https://example.com/qr-latest.png' - ); - }); - - it('maps a pending setup failure with the latest committed messages and callbacks', async () => { - const setup = deferred<{ qrUrl: string; manualKey: string }>(); - const onSubmit = vi.fn(() => setup.promise); - const initialOnError = vi.fn(); - const initialOnMessage = vi.fn(); - const latestOnError = vi.fn(); - const latestOnMessage = vi.fn(); - - const { rerender } = render( - - ); - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - - rerender( - - ); - await act(async () => { - setup.reject(Object.assign(new Error('rate limited'), { extensions: { code: 'RATE_LIMITED' } })); - }); - - expect(initialOnError).not.toHaveBeenCalled(); - expect(initialOnMessage).not.toHaveBeenCalled(); - expect(latestOnError).toHaveBeenCalledWith({ - message: 'Latest committed setup error.', - code: 'RATE_LIMITED' - }); - expect(latestOnMessage).toHaveBeenCalledWith({ - kind: 'error', - key: 'RATE_LIMITED', - message: 'Latest committed setup error.' - }); - expect(screen.getByText('Latest committed setup error.')).toBeInTheDocument(); - }); - - it('does not leak callbacks from a suspended render into pending setup', async () => { - const setup = deferred<{ qrUrl: string; manualKey: string }>(); - const onSubmit = vi.fn(() => setup.promise); - const committedOnMessage = vi.fn(); - const abandonedOnMessage = vi.fn(); - const never = new Promise(() => {}); - - function SuspendAfterEnroll({ suspend }: { suspend: boolean }) { - if (suspend) throw never; - return null; - } - - function EnrollTree({ onMessage, suspend }: { onMessage: typeof committedOnMessage; suspend: boolean }) { - return ( - Suspended update
}> - - -
- ); - } - - const { rerender } = render(); - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - - rerender(); - await act(async () => { - setup.resolve({ qrUrl: 'https://example.com/qr.png', manualKey: 'ABCDEFGHIJKLMNOP' }); - }); - - expect(committedOnMessage).toHaveBeenCalledWith({ kind: 'info', key: 'qr_ready' }); - expect(abandonedOnMessage).not.toHaveBeenCalled(); - - rerender(); - }); - - it('shows manual entry key formatted in groups of 4', async () => { - render(); - - await waitFor(() => expect(screen.getByText(/ABCD/)).toBeInTheDocument()); - // Key should be rendered as code element with groups-of-4 formatting - const codeEl = screen.getByText(/ABCD EFGH IJKL MNOP/); - expect(codeEl.tagName).toBe('CODE'); - }); - - it('advances to verify step when Next is clicked', async () => { - const user = userEvent.setup(); - render(); - - await waitFor(() => expect(screen.getByTestId('setup-next')).toBeInTheDocument()); - await user.click(screen.getByTestId('setup-next')); - - expect(screen.getByText(defaultMfaTotpEnrollMessages.verifyTitle)).toBeInTheDocument(); - expect(screen.getByTestId('totp-code')).toBeInTheDocument(); - }); - - it('returns to setup step when Back is clicked from verify', async () => { - const user = userEvent.setup(); - render(); - - await waitFor(() => expect(screen.getByTestId('setup-next')).toBeInTheDocument()); - await user.click(screen.getByTestId('setup-next')); - await user.click(screen.getByTestId('verify-back')); - - expect(screen.getByText(defaultMfaTotpEnrollMessages.setupTitle)).toBeInTheDocument(); - }); - - it('calls onConfirm with the entered code and advances to backup-codes', async () => { - const user = userEvent.setup(); - const adapters = makeAdapters(); - const onMessage = vi.fn(); - - render(); - - await waitFor(() => expect(screen.getByTestId('setup-next')).toBeInTheDocument()); - await user.click(screen.getByTestId('setup-next')); - - const codeInput = screen.getByTestId('totp-code'); - await user.type(codeInput, '123456'); - await user.click(screen.getByTestId('verify-submit')); - - await waitFor(() => expect(adapters.onConfirm).toHaveBeenCalledWith('123456')); - await waitFor(() => expect(adapters.onGenerateCodes).toHaveBeenCalledTimes(1)); - await waitFor(() => - expect(screen.getByText(defaultMfaBackupCodesDisplayMessages.title)).toBeInTheDocument() - ); - }); - - it('displays backup codes in the list', async () => { - const user = userEvent.setup(); - const codes = ['aaa-bbb-111', 'ccc-ddd-222']; - const adapters = { - onSubmit: makeSetupAdapter(), - onConfirm: makeConfirmAdapter(), - onGenerateCodes: makeCodesAdapter(codes) - }; - - render(); - - await waitFor(() => expect(screen.getByTestId('setup-next')).toBeInTheDocument()); - await user.click(screen.getByTestId('setup-next')); - await user.type(screen.getByTestId('totp-code'), '654321'); - await user.click(screen.getByTestId('verify-submit')); - - // Child block [[auth-mfa-backup-codes-display]] renders codes in a