@jmruthers/pace-core 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/audit-tool/00-dependencies.cjs +46 -13
- package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
- package/audit-tool/audits/02-project-structure.cjs +74 -2
- package/audit-tool/audits/03-architecture.cjs +220 -20
- package/audit-tool/audits/04-code-quality.cjs +95 -3
- package/audit-tool/audits/05-styling.cjs +19 -7
- package/audit-tool/audits/06-security-rbac.cjs +214 -25
- package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
- package/audit-tool/audits/08-testing-documentation.cjs +11 -3
- package/audit-tool/audits/09-operations.cjs +19 -7
- package/audit-tool/index.cjs +22 -11
- package/audit-tool/utils/report-utils.cjs +4 -0
- package/cursor-rules/01-pace-core-compliance.mdc +1 -0
- package/cursor-rules/02-project-structure.mdc +3 -26
- package/cursor-rules/03-architecture.mdc +3 -1
- package/cursor-rules/04-code-quality.mdc +1 -0
- package/cursor-rules/05-styling.mdc +120 -8
- package/cursor-rules/06-security-rbac.mdc +126 -2
- package/cursor-rules/07-api-tech-stack.mdc +1 -0
- package/cursor-rules/08-testing-documentation.mdc +1 -0
- package/cursor-rules/09-operations.mdc +1 -0
- package/dist/DataTable-EFYP2QLE.js +16 -0
- package/dist/InactivityServiceProvider-BbxwwDz1.d.ts +308 -0
- package/dist/UnifiedAuthProvider-Bkt_tzdS.d.ts +183 -0
- package/dist/api-BZR2CYXL.js +5 -0
- package/dist/api-result-USV1Czr-.d.ts +51 -0
- package/dist/assets/app-icons/admin_favicon.svg +462 -0
- package/dist/assets/app-icons/base_favicon.svg +85 -0
- package/dist/assets/app-icons/cake_favicon.svg +68 -0
- package/dist/assets/app-icons/core_favicon.svg +256 -0
- package/dist/assets/app-icons/gear_favicon.svg +91 -0
- package/dist/assets/app-icons/medi_favicon.svg +92 -0
- package/dist/assets/app-icons/mint_favicon.svg +83 -0
- package/dist/assets/app-icons/pace_favicon.svg +49 -0
- package/dist/assets/app-icons/pump_favicon.svg +68 -0
- package/dist/assets/app-icons/seed_favicon.svg +91 -0
- package/dist/assets/app-icons/team_favicon.svg +67 -0
- package/dist/assets/app-icons/trac_favicon.svg +112 -0
- package/dist/assets/app-icons/trip_favicon.svg +102 -0
- package/dist/audit-HI2DHUVU.js +4 -0
- package/dist/auth-JvdRVaud.d.ts +49 -0
- package/dist/chunk-2DL2WSOE.js +327 -0
- package/dist/chunk-2OEVOGGR.js +9598 -0
- package/dist/chunk-44CNXN4P.js +15 -0
- package/dist/chunk-4R3T5ENU.js +2943 -0
- package/dist/chunk-7A6IMHH2.js +2321 -0
- package/dist/chunk-BTHN5MKC.js +121 -0
- package/dist/chunk-CU2BU2MQ.js +2 -0
- package/dist/chunk-D6BMFMQZ.js +200 -0
- package/dist/chunk-DDMPHZ3D.js +58 -0
- package/dist/chunk-ENLXB7GP.js +721 -0
- package/dist/chunk-J2KQK6DG.js +2159 -0
- package/dist/chunk-KJXRL3XE.js +6434 -0
- package/dist/chunk-L5LFKKLJ.js +61 -0
- package/dist/chunk-PCSHBLPB.js +811 -0
- package/dist/chunk-QRYSEPHB.js +429 -0
- package/dist/chunk-RMLY6KB5.js +187 -0
- package/dist/chunk-SACF5YSM.js +31 -0
- package/dist/chunk-UZNAFKGW.js +125 -0
- package/dist/chunk-V7FTM2LU.js +1080 -0
- package/dist/chunk-WY6Y7KC3.js +264 -0
- package/dist/chunk-XOJME5T7.js +407 -0
- package/dist/chunk-XPFVT3GN.js +492 -0
- package/dist/chunk-YFTFFJIV.js +529 -0
- package/dist/chunk-YYTWKVHO.js +1334 -0
- package/dist/components.d.ts +12 -89
- package/dist/components.js +23 -55
- package/dist/database.generated-qkdoiVrJ.d.ts +9441 -0
- package/dist/eslint-rules/index.cjs +3 -0
- package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
- package/dist/eslint-rules/rules/05-styling.cjs +507 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +84 -0
- package/dist/event-BfCox3N2.d.ts +265 -0
- package/dist/file-reference-DU1hcawx.d.ts +164 -0
- package/dist/functions-DH45k8ec.d.ts +208 -0
- package/dist/hooks.d.ts +28 -14
- package/dist/hooks.js +90 -56
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +392 -155
- package/dist/index.js +337 -347
- package/dist/pagination-BW1mqywp.d.ts +201 -0
- package/dist/papaparseLoader-WG2UXQ22.js +7 -0
- package/dist/providers.d.ts +29 -14
- package/dist/providers.js +7 -5
- package/dist/rbac/eslint-rules.js +2 -2
- package/dist/rbac/index.d.ts +180 -351
- package/dist/rbac/index.js +13 -11
- package/dist/theming/runtime.d.ts +28 -5
- package/dist/theming/runtime.js +2 -2
- package/dist/timezone-BTWWXKVY.d.ts +696 -0
- package/dist/types-BE2sEHKd.d.ts +55 -0
- package/dist/types-CvOPXWWZ.d.ts +111 -0
- package/dist/types-Dr8sNhER.d.ts +50 -0
- package/dist/types.d.ts +20 -13
- package/dist/types.js +1 -0
- package/dist/usePublicPageContext-B91dGYW1.d.ts +4367 -0
- package/dist/usePublicRouteParams-BgV6VhMi.d.ts +946 -0
- package/dist/utils.d.ts +338 -156
- package/dist/utils.js +78 -60
- package/dist/validation-g5n0hDkh.d.ts +177 -0
- package/docs/api/modules.md +1226 -1094
- package/docs/api-reference/components.md +5 -5
- package/docs/api-reference/rpc-functions.md +12 -3
- package/docs/core-concepts/rbac-system.md +8 -0
- package/docs/getting-started/cursor-rules.md +17 -20
- package/docs/getting-started/dependencies.md +1 -1
- package/docs/getting-started/setup.md +235 -0
- package/docs/implementation-guides/authentication.md +27 -0
- package/docs/implementation-guides/data-tables.md +365 -10
- package/docs/migration/ApiResult-migration.md +25 -0
- package/docs/rbac/RBAC_CONTRACT.md +0 -12
- package/docs/rbac/api-reference.md +33 -31
- package/docs/standards/0-standards-overview.md +50 -15
- package/docs/standards/1-pace-core-compliance-standards.md +62 -57
- package/docs/standards/2-project-structure-standards.md +45 -90
- package/docs/standards/3-architecture-standards.md +41 -1
- package/docs/standards/4-code-quality-standards.md +26 -6
- package/docs/standards/5-styling-standards.md +35 -1
- package/docs/standards/6-security-rbac-standards.md +288 -7
- package/docs/standards/7-api-tech-stack-standards.md +116 -17
- package/docs/standards/8-testing-documentation-standards.md +31 -0
- package/docs/standards/9-operations-standards.md +19 -0
- package/docs/standards/README.md +20 -201
- package/docs/testing/README.md +10 -0
- package/docs/testing/test-setup-for-consumers.md +916 -0
- package/docs/troubleshooting/common-issues.md +17 -1
- package/docs/troubleshooting/organisation-context-setup.md +8 -0
- package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
- package/eslint-config-pace-core.cjs +24 -0
- package/package.json +14 -20
- package/scripts/build-docs.js +180 -0
- package/scripts/setup.cjs +536 -0
- package/scripts/validate.cjs +480 -0
- package/src/__mocks__/lucide-react.ts +0 -2
- package/src/__tests__/helpers/component-test-utils.test.tsx +260 -0
- package/src/__tests__/helpers/optimized-test-setup.test.ts +224 -0
- package/src/__tests__/helpers/supabaseMock.test.ts +273 -0
- package/src/__tests__/helpers/test-providers.test.tsx +99 -0
- package/src/__tests__/helpers/test-providers.tsx +37 -39
- package/src/__tests__/helpers/test-utils.test.tsx +447 -0
- package/src/__tests__/helpers/timer-utils.test.ts +371 -0
- package/src/assets/app-icons/admin_favicon.svg +462 -0
- package/src/assets/app-icons/base_favicon.svg +85 -0
- package/src/assets/app-icons/cake_favicon.svg +68 -0
- package/src/assets/app-icons/core_favicon.svg +256 -0
- package/src/assets/app-icons/gear_favicon.svg +91 -0
- package/src/assets/app-icons/index.test.ts +304 -0
- package/src/assets/app-icons/index.ts +83 -0
- package/src/assets/app-icons/medi_favicon.svg +92 -0
- package/src/assets/app-icons/mint_favicon.svg +83 -0
- package/src/assets/app-icons/pace_favicon.svg +49 -0
- package/src/assets/app-icons/pump_favicon.svg +68 -0
- package/src/assets/app-icons/seed_favicon.svg +91 -0
- package/src/assets/app-icons/team_favicon.svg +67 -0
- package/src/assets/app-icons/trac_favicon.svg +112 -0
- package/src/assets/app-icons/trip_favicon.svg +102 -0
- package/src/components/AddressField/AddressField.test.tsx +379 -4
- package/src/components/AddressField/AddressField.tsx +239 -213
- package/src/components/AddressField/types.ts +2 -2
- package/src/components/Alert/Alert.test.tsx +35 -25
- package/src/components/Alert/Alert.tsx +8 -8
- package/src/components/AppSwitcher/AppSwitcher.test.tsx +1250 -0
- package/src/components/AppSwitcher/AppSwitcher.tsx +315 -0
- package/src/components/Avatar/Avatar.test.tsx +11 -1
- package/src/components/Avatar/Avatar.tsx +3 -2
- package/src/components/Badge/Badge.test.tsx +11 -1
- package/src/components/Button/Button.test.tsx +13 -3
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Calendar/Calendar.test.tsx +523 -131
- package/src/components/Calendar/Calendar.tsx +107 -488
- package/src/components/Card/Card.test.tsx +384 -258
- package/src/components/Card/Card.tsx +19 -10
- package/src/components/Checkbox/Checkbox.test.tsx +58 -174
- package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
- package/src/components/ContextSelector/ContextSelector.test.tsx +360 -0
- package/src/components/ContextSelector/ContextSelector.tsx +66 -280
- package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
- package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
- package/src/components/DataTable/AUDIT_REPORT.md +59 -44
- package/src/components/DataTable/DataTable.comprehensive.test.tsx +759 -0
- package/src/components/DataTable/DataTable.default-state.test.tsx +524 -0
- package/src/components/DataTable/DataTable.export.test.tsx +705 -0
- package/src/components/DataTable/DataTable.grouping-aggregation.test.tsx +658 -0
- package/src/components/DataTable/DataTable.hooks.test.tsx +192 -0
- package/src/components/DataTable/DataTable.select-label-display.test.tsx +485 -0
- package/src/components/DataTable/DataTable.test.tsx +787 -416
- package/src/components/DataTable/DataTable.tsx +14 -14
- package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
- package/src/components/DataTable/DataTableCore.test-setup.ts +221 -0
- package/src/components/DataTable/DataTableCore.test.tsx +970 -0
- package/src/components/DataTable/README.md +155 -0
- package/src/components/DataTable/TESTING.md +101 -0
- package/src/components/DataTable/a11y.basic.test.tsx +788 -0
- package/src/components/DataTable/components/DataTableCore.tsx +126 -894
- package/src/components/DataTable/components/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/components/GroupingDropdown.tsx +2 -3
- package/src/components/DataTable/components/ImportModal.tsx +82 -408
- package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/context/DataTableContext.test.tsx +328 -0
- package/src/components/DataTable/context/DataTableContext.tsx +13 -13
- package/src/components/DataTable/core/ColumnFactory.test.ts +403 -0
- package/src/components/DataTable/core/ColumnFactory.ts +3 -3
- package/src/components/DataTable/hooks/useColumnOrderPersistence.test.ts +516 -0
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.test.ts +256 -0
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useDataTableConfiguration.test.ts +297 -0
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +15 -3
- package/src/components/DataTable/hooks/useDataTableDataPipeline.test.ts +270 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +238 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.test.ts +280 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +81 -260
- package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
- package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
- package/src/components/DataTable/hooks/useDataTableScope.test.ts +110 -0
- package/src/components/DataTable/hooks/useDataTableScope.ts +123 -0
- package/src/components/DataTable/hooks/useDataTableState.test.ts +733 -0
- package/src/components/DataTable/hooks/useDataTableState.ts +161 -114
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.test.ts +183 -0
- package/src/components/DataTable/hooks/useHierarchicalState.test.ts +294 -0
- package/src/components/DataTable/hooks/useImportModalFocus.test.ts +184 -0
- package/src/components/DataTable/hooks/useImportModalFocus.ts +53 -0
- package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
- package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.test.ts +787 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +311 -271
- package/src/components/DataTable/hooks/usePermissionTracking.test.ts +381 -0
- package/src/components/DataTable/hooks/usePermissionTracking.ts +122 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.test.ts +258 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +27 -4
- package/src/components/DataTable/hooks/useTableColumns.test.ts +499 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +15 -39
- package/src/components/DataTable/hooks/useTableHandlers.test.ts +461 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +13 -22
- package/src/components/DataTable/index.ts +28 -5
- package/src/components/DataTable/keyboard.test.tsx +734 -0
- package/src/components/DataTable/mocks/MockRBACProvider.tsx +66 -0
- package/src/components/DataTable/pagination.modes.test.tsx +728 -0
- package/src/components/DataTable/ssr.strict-mode.test.tsx +319 -0
- package/src/components/DataTable/styles.test.ts +379 -0
- package/src/components/DataTable/styles.ts +0 -1
- package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
- package/src/components/DataTable/test-utils/dataFactories.ts +103 -0
- package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
- package/src/components/DataTable/test-utils/sharedTestUtils.ts +419 -0
- package/src/components/DataTable/test-utils.ts +94 -0
- package/src/components/DataTable/types/actions.ts +71 -0
- package/src/components/DataTable/types/base.ts +39 -0
- package/src/components/DataTable/types/columns.ts +125 -0
- package/src/components/DataTable/types/export.ts +32 -0
- package/src/components/DataTable/types/features.ts +81 -0
- package/src/components/DataTable/types/hierarchical.ts +44 -0
- package/src/components/DataTable/types/index.ts +43 -0
- package/src/components/DataTable/types/pagination.ts +85 -0
- package/src/components/DataTable/types/performance.ts +47 -0
- package/src/components/DataTable/types/props.ts +62 -0
- package/src/components/DataTable/types/rbac.ts +45 -0
- package/src/components/DataTable/ui/layout/DataTableCore.test.tsx +1194 -0
- package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.test.tsx +438 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.tsx +225 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
- package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
- package/src/components/DataTable/ui/modals/DataTableModals.tsx +341 -0
- package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
- package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
- package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
- package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.test.tsx +245 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.tsx +159 -0
- package/src/components/DataTable/ui/shared/ActionButtons.test.tsx +921 -0
- package/src/components/DataTable/ui/shared/ActionButtons.tsx +195 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.test.tsx +497 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.tsx +113 -0
- package/src/components/DataTable/ui/shared/PaginationControls.test.tsx +451 -0
- package/src/components/DataTable/ui/shared/PaginationControls.tsx +291 -0
- package/src/components/DataTable/ui/shared/SortIndicator.test.tsx +135 -0
- package/src/components/DataTable/ui/shared/SortIndicator.tsx +50 -0
- package/src/components/DataTable/ui/table/EditFields.test.tsx +526 -0
- package/src/components/DataTable/ui/table/EditFields.tsx +355 -0
- package/src/components/DataTable/ui/table/EditableRow.test.tsx +1003 -0
- package/src/components/DataTable/ui/table/EditableRow.tsx +444 -0
- package/src/components/DataTable/ui/table/EmptyState.test.tsx +360 -0
- package/src/components/DataTable/ui/table/EmptyState.tsx +74 -0
- package/src/components/DataTable/ui/table/FilterRow.test.tsx +416 -0
- package/src/components/DataTable/ui/table/FilterRow.tsx +148 -0
- package/src/components/DataTable/ui/table/LoadingState.test.tsx +77 -0
- package/src/components/DataTable/ui/table/LoadingState.tsx +17 -0
- package/src/components/DataTable/ui/table/RowComponent.test.tsx +1024 -0
- package/src/components/DataTable/ui/table/RowComponent.tsx +429 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.test.tsx +1273 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
- package/src/components/DataTable/ui/table/cellValueUtils.test.ts +453 -0
- package/src/components/DataTable/ui/table/cellValueUtils.ts +40 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.test.tsx +551 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.tsx +160 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.test.tsx +751 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.tsx +114 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.test.tsx +629 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.tsx +271 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
- package/src/components/DataTable/utils/a11yUtils.test.ts +548 -0
- package/src/components/DataTable/utils/a11yUtils.ts +1 -1
- package/src/components/DataTable/utils/aggregationUtils.test.ts +288 -0
- package/src/components/DataTable/utils/aggregationUtils.ts +5 -5
- package/src/components/DataTable/utils/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/csvParse.test.ts +74 -0
- package/src/components/DataTable/utils/csvParse.ts +65 -0
- package/src/components/DataTable/utils/errorHandling.test.ts +209 -0
- package/src/components/DataTable/utils/errorHandling.ts +3 -1
- package/src/components/DataTable/utils/exportUtils.test.ts +954 -0
- package/src/components/DataTable/utils/exportUtils.ts +1 -1
- package/src/components/DataTable/utils/flexibleImport.test.ts +573 -0
- package/src/components/DataTable/utils/flexibleImport.ts +3 -186
- package/src/components/DataTable/utils/hierarchicalSorting.test.ts +235 -0
- package/src/components/DataTable/utils/hierarchicalSorting.ts +3 -3
- package/src/components/DataTable/utils/hierarchicalUtils.test.ts +586 -0
- package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
- package/src/components/DataTable/utils/importDateParser.ts +114 -0
- package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
- package/src/components/DataTable/utils/importValueParser.ts +91 -0
- package/src/components/DataTable/utils/paginationUtils.test.ts +593 -0
- package/src/components/DataTable/utils/paginationUtils.ts +7 -4
- package/src/components/DataTable/utils/performanceUtils.test.ts +470 -0
- package/src/components/DataTable/utils/performanceUtils.ts +1 -1
- package/src/components/DataTable/utils/rowUtils.test.ts +235 -0
- package/src/components/DataTable/utils/selectFieldUtils.test.ts +271 -0
- package/src/components/DataTable/utils/selectFieldUtils.ts +97 -67
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +18 -25
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.test.tsx +3 -16
- package/src/components/DateTimeField/DateTimeField.tsx +1 -1
- package/src/components/Dialog/Dialog.test-utils.ts +49 -0
- package/src/components/Dialog/Dialog.test.tsx +2865 -458
- package/src/components/Dialog/Dialog.tsx +183 -986
- package/src/components/Dialog/dialogLock.test.ts +238 -0
- package/src/components/Dialog/dialogLock.ts +98 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
- package/src/components/Dialog/useDialogDimensions.ts +140 -0
- package/src/components/Dialog/useDialogLifecycle.test.ts +358 -0
- package/src/components/Dialog/useDialogLifecycle.ts +135 -0
- package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
- package/src/components/Dialog/useDialogPersistence.ts +357 -0
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +2 -62
- package/src/components/ErrorBoundary/ErrorBoundaryContext.context.ts +17 -0
- package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +2 -45
- package/src/components/ErrorBoundary/ErrorBoundaryContext.types.ts +41 -0
- package/src/components/ErrorBoundary/index.ts +3 -4
- package/src/components/ErrorBoundary/useErrorBoundaryContext.ts +20 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +479 -247
- package/src/components/FileDisplay/FileDisplay.tsx +29 -659
- package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
- package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
- package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
- package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
- package/src/components/FileDisplay/fallbackUtils.ts +44 -0
- package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
- package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
- package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
- package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
- package/src/components/FileDisplay/index.tsx +1 -1
- package/src/components/FileDisplay/useFileDisplay.test.ts +538 -0
- package/src/components/FileDisplay/useFileDisplay.ts +515 -0
- package/src/components/FileDisplay/useFileDisplay.unit.test.ts +1438 -0
- package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
- package/src/components/FileDisplay/usePublicFileDisplay.test.ts +729 -0
- package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
- package/src/components/FileUpload/FileUpload.test.tsx +69 -27
- package/src/components/FileUpload/FileUpload.tsx +112 -527
- package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
- package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
- package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
- package/src/components/FileUpload/index.tsx +1 -1
- package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
- package/src/components/FileUpload/useFileUploadManager.ts +454 -0
- package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
- package/src/components/FileUpload/useResolvedAppId.ts +77 -0
- package/src/components/Footer/Footer.test.tsx +15 -382
- package/src/components/Footer/Footer.tsx +8 -125
- package/src/components/Form/Form.test.tsx +425 -88
- package/src/components/Form/Form.tsx +91 -299
- package/src/components/Form/useFormPersistence.ts +257 -0
- package/src/components/Header/Header.test.tsx +653 -163
- package/src/components/Header/Header.tsx +62 -44
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +35 -76
- package/src/components/Input/Input.test.tsx +34 -120
- package/src/components/Input/Input.tsx +1 -1
- package/src/components/Label/Label.test.tsx +46 -45
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +8 -11
- package/src/components/LoginForm/LoginForm.test.tsx +0 -1
- package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +2422 -102
- package/src/components/NavigationMenu/NavigationMenu.tsx +62 -362
- package/src/components/NavigationMenu/index.ts +6 -1
- package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
- package/src/components/NavigationMenu/useNavigationFiltering.test.ts +1949 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +199 -308
- package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
- package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +1322 -0
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +50 -49
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +81 -38
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +103 -85
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +774 -44
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +282 -764
- package/src/components/PaceAppLayout/README.md +0 -9
- package/src/components/PaceAppLayout/test-setup.tsx +15 -9
- package/src/components/PaceAppLayout/useFilteredNavItems.ts +304 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +142 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +79 -0
- package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
- package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +782 -20
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +33 -125
- package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +1 -1
- package/src/components/Progress/Progress.test.tsx +127 -1
- package/src/components/Progress/Progress.tsx +1 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +1196 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -217
- package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
- package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
- package/src/components/PublicLayout/PublicLayout.test.tsx +1640 -38
- package/src/components/PublicLayout/PublicPageContext.ts +28 -0
- package/src/components/PublicLayout/PublicPageLayout.tsx +134 -75
- package/src/components/PublicLayout/PublicPageProvider.tsx +7 -42
- package/src/components/PublicLayout/usePublicPageContext.ts +36 -0
- package/src/components/Select/Select.test.tsx +45 -8
- package/src/components/Select/Select.tsx +57 -40
- package/src/components/Select/context.test.tsx +56 -0
- package/src/components/Select/text.test.tsx +104 -0
- package/src/components/Select/text.ts +26 -0
- package/src/components/Select/types.ts +3 -0
- package/src/components/Select/useSelectEvents.test.ts +279 -0
- package/src/components/Select/useSelectEvents.ts +87 -0
- package/src/components/Select/useSelectSearch.test.tsx +295 -0
- package/src/components/Select/useSelectSearch.ts +91 -0
- package/src/components/Select/useSelectState.test.ts +268 -0
- package/src/components/Select/useSelectState.ts +104 -0
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.test.tsx +28 -112
- package/src/components/Switch/Switch.test.tsx +57 -153
- package/src/components/Table/Table.test.tsx +395 -317
- package/src/components/Tabs/Tabs.test.tsx +270 -0
- package/src/components/Tabs/Tabs.tsx +4 -4
- package/src/components/Textarea/Textarea.test.tsx +11 -38
- package/src/components/Toast/Toast.test.tsx +425 -496
- package/src/components/Tooltip/Tooltip.test.tsx +4 -21
- package/src/components/UserMenu/UserMenu.test.tsx +1 -21
- package/src/components/UserMenu/UserMenu.tsx +0 -1
- package/src/components/index.test.ts +346 -0
- package/src/components/index.ts +12 -1
- package/src/constants/performance.test.ts +91 -0
- package/src/hooks/ServiceHooks.test.tsx +725 -0
- package/src/hooks/hooks.integration.test.tsx +608 -0
- package/src/hooks/index.ts +18 -3
- package/src/hooks/index.unit.test.ts +220 -0
- package/src/hooks/public/usePublicEvent.test.ts +304 -0
- package/src/hooks/public/usePublicEvent.ts +11 -11
- package/src/hooks/public/usePublicEventLogo.test.ts +655 -120
- package/src/hooks/public/usePublicEventLogo.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.test.ts +595 -0
- package/src/hooks/public/usePublicRouteParams.ts +2 -2
- package/src/hooks/services/useAuth.ts +9 -7
- package/src/hooks/services/useAuthService.ts +1 -1
- package/src/hooks/services/useEventService.ts +1 -1
- package/src/hooks/useAccessibleApps.test.ts +400 -0
- package/src/hooks/useAccessibleApps.ts +264 -0
- package/src/hooks/useAddressAutocomplete.test.ts +170 -47
- package/src/hooks/useAddressAutocomplete.ts +109 -81
- package/src/hooks/useApiFetch.unit.test.ts +111 -0
- package/src/hooks/useAppConfig.ts +13 -3
- package/src/hooks/useAppConfig.unit.test.ts +712 -0
- package/src/hooks/useComponentPerformance.unit.test.tsx +314 -0
- package/src/hooks/useDataTablePerformance.ts +111 -130
- package/src/hooks/useDataTablePerformance.unit.test.ts +720 -0
- package/src/hooks/useDataTableState.test.ts +170 -0
- package/src/hooks/useDataTableState.ts +5 -5
- package/src/hooks/useDebounce.unit.test.ts +157 -0
- package/src/hooks/useEventTheme.test.ts +70 -18
- package/src/hooks/useEventTheme.ts +50 -22
- package/src/hooks/useEvents.ts +49 -2
- package/src/hooks/useEvents.unit.test.ts +227 -0
- package/src/hooks/useFileReference.test.ts +388 -107
- package/src/hooks/useFileReference.ts +184 -179
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useFileUrl.unit.test.ts +686 -0
- package/src/hooks/useFileUrlCache.test.ts +319 -0
- package/src/hooks/useFileUrlCache.ts +5 -2
- package/src/hooks/useFocusManagement.unit.test.ts +604 -0
- package/src/hooks/useFocusTrap.unit.test.tsx +613 -0
- package/src/hooks/useFormDialog.test.ts +307 -0
- package/src/hooks/useFormDialog.ts +2 -2
- package/src/hooks/useInactivityTracker.ts +141 -134
- package/src/hooks/useInactivityTracker.unit.test.ts +446 -0
- package/src/hooks/useIsMobile.unit.test.ts +317 -0
- package/src/hooks/useIsPrint.ts +62 -0
- package/src/hooks/useIsPrint.unit.test.ts +545 -0
- package/src/hooks/useKeyboardShortcuts.unit.test.ts +907 -0
- package/src/hooks/useOrganisationPermissions.test.ts +1 -2
- package/src/hooks/useOrganisationPermissions.ts +1 -4
- package/src/hooks/useOrganisationPermissions.unit.test.tsx +293 -0
- package/src/hooks/useOrganisationSecurity.test.ts +4 -33
- package/src/hooks/useOrganisationSecurity.ts +192 -203
- package/src/hooks/useOrganisationSecurity.unit.test.tsx +959 -0
- package/src/hooks/useOrganisations.ts +1 -1
- package/src/hooks/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/usePerformanceMonitor.ts +1 -1
- package/src/hooks/usePerformanceMonitor.unit.test.ts +693 -0
- package/src/hooks/usePermissionCache.test.ts +298 -329
- package/src/hooks/usePermissionCache.ts +277 -276
- package/src/hooks/usePreventTabReload.test.ts +307 -0
- package/src/hooks/usePublicEvent.simple.test.ts +794 -0
- package/src/hooks/usePublicEvent.test.ts +670 -0
- package/src/hooks/usePublicEvent.unit.test.ts +638 -0
- package/src/hooks/usePublicFileDisplay.test.ts +948 -0
- package/src/hooks/usePublicRouteParams.unit.test.ts +442 -0
- package/src/hooks/useQueryCache.test.ts +391 -0
- package/src/hooks/useQueryCache.ts +7 -9
- package/src/hooks/useRBAC.unit.test.ts +253 -0
- package/src/hooks/useSessionDraft.test.ts +556 -0
- package/src/hooks/useSessionDraft.ts +14 -11
- package/src/hooks/useSessionRestoration.ts +1 -1
- package/src/hooks/useSessionRestoration.unit.test.tsx +381 -0
- package/src/hooks/useStorage.ts +94 -54
- package/src/hooks/useStorage.unit.test.ts +684 -0
- package/src/hooks/useToast.test.ts +413 -0
- package/src/hooks/useToast.ts +2 -2
- package/src/hooks/useToast.unit.test.tsx +481 -0
- package/src/hooks/useZodForm.ts +3 -3
- package/src/hooks/useZodForm.unit.test.tsx +191 -0
- package/src/icons/index.test.ts +133 -0
- package/src/icons/index.ts +3 -1
- package/src/index.test.ts +528 -0
- package/src/index.ts +56 -9
- package/src/providers/AuthProvider.test.tsx +218 -0
- package/src/providers/EventProvider.test.tsx +487 -0
- package/src/providers/InactivityProvider.test-helper.tsx +40 -0
- package/src/providers/InactivityProvider.test.tsx +421 -0
- package/src/providers/ProviderLifecycle.test.tsx +308 -0
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +7 -12
- package/src/providers/UnifiedAuthProvider.test.tsx +503 -0
- package/src/providers/index.test.ts +138 -0
- package/src/providers/services/AuthServiceContext.ts +27 -0
- package/src/providers/services/AuthServiceProvider.integration.test.tsx +229 -0
- package/src/providers/services/AuthServiceProvider.test.tsx +638 -0
- package/src/providers/services/AuthServiceProvider.tsx +81 -20
- package/src/providers/services/EventServiceContext.ts +25 -0
- package/src/providers/services/EventServiceProvider.test.tsx +839 -0
- package/src/providers/services/EventServiceProvider.tsx +11 -20
- package/src/providers/services/InactivityServiceContext.ts +25 -0
- package/src/providers/services/InactivityServiceProvider.test.tsx +662 -0
- package/src/providers/services/InactivityServiceProvider.tsx +7 -17
- package/src/providers/services/OrganisationServiceContext.ts +25 -0
- package/src/providers/services/OrganisationServiceProvider.test.tsx +440 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +7 -17
- package/src/providers/services/UnifiedAuthContext.ts +102 -0
- package/src/providers/services/UnifiedAuthProvider.advanced.test.tsx +434 -0
- package/src/providers/services/UnifiedAuthProvider.appId.test.tsx +408 -0
- package/src/providers/services/UnifiedAuthProvider.integration.test.tsx +304 -0
- package/src/providers/services/UnifiedAuthProvider.test.tsx +212 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +147 -497
- package/src/providers/services/contexts.test.tsx +281 -0
- package/src/providers/services/useUnifiedAuth.test.tsx +251 -0
- package/src/providers/services/useUnifiedAuth.ts +29 -0
- package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
- package/src/providers/useInactivity.test-helper.ts +27 -0
- package/src/rbac/README.md +5 -5
- package/src/rbac/adapters.comprehensive.test.tsx +429 -0
- package/src/rbac/adapters.test.tsx +654 -0
- package/src/rbac/adapters.tsx +53 -38
- package/src/rbac/api.test.ts +986 -259
- package/src/rbac/api.ts +260 -216
- package/src/rbac/audit-batched.test.ts +550 -0
- package/src/rbac/audit-batched.ts +5 -4
- package/src/rbac/audit.test.ts +225 -28
- package/src/rbac/audit.ts +26 -18
- package/src/rbac/auth-rbac-security.integration.test.tsx +300 -0
- package/src/rbac/auth-rbac.e2e.test.tsx +510 -0
- package/src/rbac/cache-invalidation.test.ts +715 -0
- package/src/rbac/cache-invalidation.ts +18 -15
- package/src/rbac/cache.test.ts +123 -63
- package/src/rbac/cache.ts +3 -4
- package/src/rbac/components/AccessDenied.test.tsx +324 -0
- package/src/rbac/components/AccessDenied.tsx +20 -18
- package/src/rbac/components/NavigationGuard.test.tsx +1148 -0
- package/src/rbac/components/NavigationGuard.tsx +10 -8
- package/src/rbac/components/PagePermissionGuard.guard.test.tsx +236 -0
- package/src/rbac/components/PagePermissionGuard.performance.test.tsx +252 -0
- package/src/rbac/components/PagePermissionGuard.race-condition.test.tsx +243 -0
- package/src/rbac/components/PagePermissionGuard.test.tsx +1430 -0
- package/src/rbac/components/PagePermissionGuard.tsx +188 -381
- package/src/rbac/components/PagePermissionGuard.verification.test.tsx +185 -0
- package/src/rbac/config.test.ts +131 -48
- package/src/rbac/config.ts +69 -26
- package/src/rbac/docs/event-based-apps.md +26 -13
- package/src/rbac/engine.comprehensive.test.ts +808 -0
- package/src/rbac/engine.test.ts +974 -130
- package/src/rbac/engine.ts +53 -13
- package/src/rbac/errors.test.ts +99 -87
- package/src/rbac/errors.ts +89 -55
- package/src/rbac/eslint-rules.js +2 -2
- package/src/rbac/hooks/permissions/runPermissionCheck.ts +77 -0
- package/src/rbac/hooks/permissions/useAccessLevel.test.ts +622 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +23 -14
- package/src/rbac/hooks/permissions/useCan.test.ts +798 -0
- package/src/rbac/hooks/permissions/useCan.ts +173 -253
- package/src/rbac/hooks/permissions/useMultiplePermissions.test.ts +843 -0
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +63 -10
- package/src/rbac/hooks/permissions/usePermissions.test.ts +543 -0
- package/src/rbac/hooks/permissions/usePermissions.ts +50 -78
- package/src/rbac/hooks/useCan.test.ts +348 -32
- package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
- package/src/rbac/hooks/usePageGuardScope.ts +117 -0
- package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
- package/src/rbac/hooks/usePermissions.integration.test.ts +427 -0
- package/src/rbac/hooks/usePermissions.stability.test.ts +268 -0
- package/src/rbac/hooks/usePermissions.test.ts +459 -33
- package/src/rbac/hooks/usePermissions.ts +5 -7
- package/src/rbac/hooks/useRBAC.test.ts +1784 -21
- package/src/rbac/hooks/useRBAC.ts +148 -88
- package/src/rbac/hooks/useResolvedScope.test.ts +442 -5
- package/src/rbac/hooks/useResolvedScope.ts +4 -1
- package/src/rbac/hooks/useResourcePermissions.test.ts +561 -24
- package/src/rbac/hooks/useResourcePermissions.ts +76 -140
- package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +634 -61
- package/src/rbac/hooks/useRoleManagement.ts +158 -586
- package/src/rbac/hooks/useSecureSupabase.test.ts +1179 -0
- package/src/rbac/hooks/useSecureSupabase.ts +21 -14
- package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
- package/src/rbac/index.test.ts +107 -0
- package/src/rbac/index.ts +32 -32
- package/src/rbac/performance.test.ts +451 -0
- package/src/rbac/permissions.test.ts +149 -68
- package/src/rbac/permissions.ts +0 -3
- package/src/rbac/rbac-core.test.tsx +276 -0
- package/src/rbac/rbac-engine-core-logic.test.ts +387 -0
- package/src/rbac/rbac-engine-simplified.test.ts +252 -0
- package/src/rbac/rbac-functions.test.ts +703 -0
- package/src/rbac/rbac-integration.test.ts +523 -0
- package/src/rbac/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/request-deduplication.test.ts +352 -0
- package/src/rbac/request-deduplication.ts +5 -4
- package/src/rbac/scenarios.user-role.test.tsx +271 -0
- package/src/rbac/secureClient.test.ts +499 -115
- package/src/rbac/secureClient.ts +54 -28
- package/src/rbac/security.test.ts +448 -44
- package/src/rbac/security.ts +7 -6
- package/src/rbac/types/roleManagement.ts +66 -0
- package/src/rbac/types.test.ts +236 -0
- package/src/rbac/types.ts +7 -5
- package/src/rbac/utils/clientSecurity.test.ts +192 -0
- package/src/rbac/utils/clientSecurity.ts +6 -4
- package/src/rbac/utils/contextValidator.test.ts +126 -0
- package/src/rbac/utils/contextValidator.ts +6 -3
- package/src/rbac/utils/deep-equal.test.ts +76 -0
- package/src/rbac/utils/eventContext.test.ts +401 -0
- package/src/rbac/utils/eventContext.ts +38 -34
- package/src/rbac/utils/fetchPermissionMap.ts +13 -0
- package/src/rbac/utils/permissionMapHelpers.ts +34 -0
- package/src/rbac/utils/roleManagementRpc.ts +303 -0
- package/src/services/AuthService.edge-cases.test.ts +746 -0
- package/src/services/AuthService.restoreSession.test.ts +59 -0
- package/src/services/AuthService.test.ts +1362 -0
- package/src/services/AuthService.ts +197 -216
- package/src/services/BaseService.edge-cases.test.ts +506 -0
- package/src/services/BaseService.test.ts +363 -0
- package/src/services/EventService.edge-cases.test.ts +636 -0
- package/src/services/EventService.eventColours.test.ts +64 -0
- package/src/services/EventService.test.ts +1250 -0
- package/src/services/EventService.ts +244 -315
- package/src/services/InactivityService.edge-cases.test.ts +492 -0
- package/src/services/InactivityService.lifecycle.test.ts +406 -0
- package/src/services/InactivityService.test.ts +829 -0
- package/src/services/InactivityService.ts +172 -213
- package/src/services/OrganisationService.edge-cases.test.ts +633 -0
- package/src/services/OrganisationService.pagination.test.ts +409 -0
- package/src/services/OrganisationService.test.ts +1579 -0
- package/src/services/OrganisationService.ts +186 -257
- package/src/services/base/BaseService.test.ts +214 -0
- package/src/services/interfaces/IAuthService.test.ts +184 -0
- package/src/services/interfaces/IAuthService.ts +10 -9
- package/src/services/interfaces/IEventService.test.ts +176 -0
- package/src/services/interfaces/IInactivityService.test.ts +183 -0
- package/src/services/interfaces/IOrganisationService.test.ts +207 -0
- package/src/services/interfaces/IOrganisationService.ts +0 -1
- package/src/styles/core.css +244 -12
- package/src/theming/parseEventColours.test.ts +321 -0
- package/src/theming/parseEventColours.ts +18 -9
- package/src/theming/runtime.test.ts +495 -0
- package/src/theming/runtime.ts +72 -7
- package/src/types/api-result.ts +53 -0
- package/src/types/auth.ts +0 -1
- package/src/types/core.test.ts +397 -0
- package/src/types/database-generated.test.ts +78 -0
- package/src/types/database.generated.ts +45 -10
- package/src/types/event.ts +39 -19
- package/src/types/file-reference.test.ts +351 -0
- package/src/types/file-reference.ts +37 -12
- package/src/types/guards.test.ts +246 -0
- package/src/types/index.test.ts +265 -0
- package/src/types/index.ts +3 -0
- package/src/types/organisation.roles.test.ts +55 -0
- package/src/types/organisation.test.ts +1105 -0
- package/src/types/organisation.ts +15 -15
- package/src/types/rpc-responses.ts +33 -0
- package/src/types/supabase.ts +14 -6
- package/src/types/theme.test.ts +830 -0
- package/src/types/type-validation.test.ts +526 -0
- package/src/types/validation.test.ts +729 -0
- package/src/types/vitest-globals.d.ts +1 -1
- package/src/utils/app/appConfig.test.ts +235 -0
- package/src/utils/app/appIdResolver.test.ts +252 -57
- package/src/utils/app/appIdResolver.ts +31 -20
- package/src/utils/app/appNameResolver.test.ts +18 -10
- package/src/utils/app/appNameResolver.ts +11 -9
- package/src/utils/app/appPortMap.test.ts +125 -0
- package/src/utils/app/appPortMap.ts +51 -0
- package/src/utils/app/buildAppUrl.test.ts +273 -0
- package/src/utils/app/buildAppUrl.ts +114 -0
- package/src/utils/appConfig.unit.test.ts +55 -0
- package/src/utils/audit/audit.test.ts +354 -39
- package/src/utils/audit.unit.test.ts +69 -0
- package/src/utils/auth-utils.unit.test.ts +69 -0
- package/src/utils/bundleAnalysis.unit.test.ts +326 -0
- package/src/utils/cn.unit.test.ts +34 -0
- package/src/utils/context/organisationContext.test.ts +115 -95
- package/src/utils/context/organisationContext.ts +32 -43
- package/src/utils/context/sessionTracking.test.ts +354 -0
- package/src/utils/core/cn.test.ts +66 -0
- package/src/utils/core/debugLogger.test.ts +113 -0
- package/src/utils/core/debugLogger.ts +15 -8
- package/src/utils/core/logger.test.ts +217 -0
- package/src/utils/core/logger.ts +20 -16
- package/src/utils/core/mergeRefs.ts +24 -0
- package/src/utils/debugLogger.test.ts +417 -0
- package/src/utils/device/deviceFingerprint.test.ts +8 -5
- package/src/utils/device/deviceFingerprint.ts +3 -3
- package/src/utils/deviceFingerprint.unit.test.ts +818 -0
- package/src/utils/dynamic/createLazyComponent.tsx +46 -0
- package/src/utils/dynamic/dynamicUtils.test.ts +185 -0
- package/src/utils/dynamic/dynamicUtils.ts +6 -6
- package/src/utils/dynamic/lazyLoad.test.tsx +156 -0
- package/src/utils/dynamic/lazyLoad.tsx +8 -36
- package/src/utils/dynamic/papaparseLoader.ts +7 -0
- package/src/utils/dynamicUtils.unit.test.ts +331 -0
- package/src/utils/file-reference/file-reference.test.ts +1238 -0
- package/src/utils/file-reference/index.ts +330 -348
- package/src/utils/formatDate.unit.test.ts +109 -0
- package/src/utils/formatting/formatDate.test.ts +22 -148
- package/src/utils/formatting/formatDateTime.test.ts +41 -119
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +41 -85
- package/src/utils/formatting/formatNumber.test.ts +259 -0
- package/src/utils/formatting/formatTime.test.ts +36 -128
- package/src/utils/formatting/formatting.ts +1 -1
- package/src/utils/formatting.unit.test.ts +99 -0
- package/src/utils/google-places/googlePlacesUtils.test.ts +127 -36
- package/src/utils/google-places/googlePlacesUtils.ts +67 -86
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +68 -8
- package/src/utils/google-places/loadGoogleMapsScript.ts +140 -118
- package/src/utils/index.ts +52 -11
- package/src/utils/index.unit.test.ts +251 -0
- package/src/utils/lazyLoad.unit.test.tsx +319 -0
- package/src/utils/location/location.test.ts +19 -116
- package/src/utils/logger.unit.test.ts +398 -0
- package/src/utils/organisationContext.unit.test.ts +180 -0
- package/src/utils/performance/bundleAnalysis.test.ts +148 -0
- package/src/utils/performance/bundleAnalysis.ts +16 -22
- package/src/utils/performance/performanceBenchmark.test.ts +251 -0
- package/src/utils/performance/performanceBenchmark.ts +12 -4
- package/src/utils/performance/performanceBudgets.test.ts +241 -0
- package/src/utils/performance/performanceBudgets.ts +9 -6
- package/src/utils/performanceBenchmark.test.ts +174 -0
- package/src/utils/performanceBudgets.unit.test.ts +288 -0
- package/src/utils/permissionTypes.unit.test.ts +250 -0
- package/src/utils/permissionUtils.unit.test.ts +362 -0
- package/src/utils/permissions/permissionTypes.test.ts +149 -0
- package/src/utils/permissions/permissionUtils.test.ts +20 -42
- package/src/utils/persistence/keyDerivation.test.ts +306 -0
- package/src/utils/persistence/sensitiveFieldDetection.test.ts +271 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +2 -2
- package/src/utils/request-deduplication.test.ts +349 -0
- package/src/utils/request-deduplication.ts +6 -4
- package/src/utils/sanitization.unit.test.ts +346 -0
- package/src/utils/schemaUtils.unit.test.ts +441 -0
- package/src/utils/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/secureErrors.unit.test.ts +390 -0
- package/src/utils/secureStorage.unit.test.ts +289 -0
- package/src/utils/security/auth-utils.ts +38 -27
- package/src/utils/security/secureDataAccess.test.ts +22 -191
- package/src/utils/security/secureDataAccess.ts +241 -281
- package/src/utils/security/secureErrors.test.ts +163 -0
- package/src/utils/security/secureStorage.test.ts +156 -0
- package/src/utils/security/secureStorage.ts +1 -1
- package/src/utils/security/security.test.ts +212 -0
- package/src/utils/security/security.ts +15 -18
- package/src/utils/security/securityMonitor.test.ts +90 -0
- package/src/utils/security/securityMonitor.ts +1 -1
- package/src/utils/security.unit.test.ts +155 -0
- package/src/utils/securityMonitor.unit.test.ts +276 -0
- package/src/utils/sessionTracking.unit.test.ts +218 -0
- package/src/utils/storage/config.unit.test.ts +239 -0
- package/src/utils/storage/helpers.test.ts +769 -456
- package/src/utils/storage/helpers.ts +174 -253
- package/src/utils/storage/index.unit.test.ts +68 -0
- package/src/utils/storage/storageUtils.ts +32 -0
- package/src/utils/storage/types.ts +9 -2
- package/src/utils/supabase/createBaseClient.test.ts +201 -0
- package/src/utils/supabase/createBaseClient.ts +2 -1
- package/src/utils/timezone/timezone.test.ts +26 -44
- package/src/utils/timezone.test.ts +345 -0
- package/src/utils/validation/common.test.ts +115 -0
- package/src/utils/validation/csrf.test.ts +198 -0
- package/src/utils/validation/csrf.ts +42 -41
- package/src/utils/validation/htmlSanitization.ts +27 -31
- package/src/utils/validation/htmlSanitization.unit.test.ts +618 -0
- package/src/utils/validation/passwordSchema.test.ts +164 -0
- package/src/utils/validation/schema.test.ts +127 -0
- package/src/utils/validation/schema.ts +6 -3
- package/src/utils/validation/sqlInjectionProtection.test.ts +165 -0
- package/src/utils/validation/sqlInjectionProtection.ts +2 -2
- package/src/utils/validation/user.test.ts +173 -0
- package/src/utils/validation/validation.test.ts +197 -0
- package/src/utils/validation/validationUtils.test.ts +294 -0
- package/src/utils/validation.unit.test.ts +307 -0
- package/src/utils/validationUtils.unit.test.ts +558 -0
- package/src/vite-env.d.ts +6 -0
- package/dist/AuthService-DmfO5rGS.d.ts +0 -524
- package/dist/DataTable-DRUIgtUH.d.ts +0 -166
- package/dist/DataTable-SOAFXIWY.js +0 -15
- package/dist/PublicPageProvider-CIGSujI2.d.ts +0 -4147
- package/dist/UnifiedAuthProvider-7SNDOWYD.js +0 -7
- package/dist/UnifiedAuthProvider-CKvHP1MK.d.ts +0 -139
- package/dist/api-7P7DI652.js +0 -4
- package/dist/audit-MYQXYZFU.js +0 -3
- package/dist/auth-BZOJqrdd.d.ts +0 -49
- package/dist/chunk-4DDCYDQ3.js +0 -544
- package/dist/chunk-5HNSDQWH.js +0 -5046
- package/dist/chunk-5W2A3DRC.js +0 -164
- package/dist/chunk-6GLLNA6U.js +0 -31
- package/dist/chunk-7ILTDCL2.js +0 -80
- package/dist/chunk-A3W6LW53.js +0 -70
- package/dist/chunk-AHU7G2R5.js +0 -423
- package/dist/chunk-C7ZQ5O4C.js +0 -481
- package/dist/chunk-EF2UGZWY.js +0 -611
- package/dist/chunk-FEJLJNWA.js +0 -181
- package/dist/chunk-FYHN4DD5.js +0 -415
- package/dist/chunk-GS5672WG.js +0 -2003
- package/dist/chunk-HF6O3O37.js +0 -187
- package/dist/chunk-J2U36LHD.js +0 -8517
- package/dist/chunk-LX6U42O3.js +0 -2177
- package/dist/chunk-MPBLMWVR.js +0 -2161
- package/dist/chunk-OJ4SKRSV.js +0 -105
- package/dist/chunk-S6ZQKDY6.js +0 -62
- package/dist/chunk-S7DKJPLT.js +0 -699
- package/dist/chunk-T5CVK4R3.js +0 -2816
- package/dist/chunk-TTRFSOKR.js +0 -121
- package/dist/chunk-Z2FNRKF3.js +0 -994
- package/dist/database.generated-DT8JTZiP.d.ts +0 -9406
- package/dist/event-CW5YB_2p.d.ts +0 -239
- package/dist/file-reference-BavO2eQj.d.ts +0 -148
- package/dist/functions-lBy5L2ry.d.ts +0 -208
- package/dist/timezone-0AyangqX.d.ts +0 -697
- package/dist/types-BeoeWV5I.d.ts +0 -110
- package/dist/types-DXstZpNI.d.ts +0 -614
- package/dist/types-t9H8qKRw.d.ts +0 -55
- package/dist/usePublicRouteParams-DQLrDqDb.d.ts +0 -876
- package/dist/useToast-AyaT-x7p.d.ts +0 -68
- package/dist/validation-643vUDZW.d.ts +0 -177
- package/scripts/build-docs-incremental.js +0 -179
- package/scripts/eslint-audit.cjs +0 -123
- package/scripts/generate-docs.js +0 -157
- package/scripts/install-cursor-rules.cjs +0 -255
- package/scripts/install-eslint-config.cjs +0 -349
- package/scripts/setup-build-cache.js +0 -73
- package/scripts/validate-pre-publish.js +0 -145
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +0 -260
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +0 -224
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +0 -273
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +0 -99
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +0 -448
- package/src/__tests__/helpers/__tests__/timer-utils.test.ts +0 -371
- package/src/__tests__/hooks/usePermissions.test.ts +0 -268
- package/src/__tests__/integration/UserProfile.test.tsx +0 -124
- package/src/__tests__/public-recipe-view.test.ts +0 -228
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +0 -220
- package/src/__tests__/rls-policies.test.ts +0 -471
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +0 -759
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +0 -524
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +0 -705
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +0 -658
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +0 -192
- package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +0 -483
- package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +0 -220
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +0 -1474
- package/src/components/DataTable/__tests__/README.md +0 -145
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +0 -788
- package/src/components/DataTable/__tests__/keyboard.test.tsx +0 -756
- package/src/components/DataTable/__tests__/mocks/MockRBACProvider.tsx +0 -66
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +0 -730
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +0 -325
- package/src/components/DataTable/__tests__/styles.test.ts +0 -382
- package/src/components/DataTable/__tests__/test-utils/dataFactories.ts +0 -103
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +0 -380
- package/src/components/DataTable/__tests__/test-utils.ts +0 -94
- package/src/components/DataTable/components/AccessDeniedPage.tsx +0 -159
- package/src/components/DataTable/components/ActionButtons.tsx +0 -190
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +0 -160
- package/src/components/DataTable/components/ColumnFilter.tsx +0 -118
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +0 -114
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +0 -225
- package/src/components/DataTable/components/DataTableLayout.tsx +0 -573
- package/src/components/DataTable/components/DataTableModals.tsx +0 -245
- package/src/components/DataTable/components/DataTableToolbar.tsx +0 -271
- package/src/components/DataTable/components/EditFields.tsx +0 -327
- package/src/components/DataTable/components/EditableRow.tsx +0 -462
- package/src/components/DataTable/components/EmptyState.tsx +0 -79
- package/src/components/DataTable/components/FilterRow.tsx +0 -141
- package/src/components/DataTable/components/LoadingState.tsx +0 -17
- package/src/components/DataTable/components/PaginationControls.tsx +0 -289
- package/src/components/DataTable/components/RowComponent.tsx +0 -403
- package/src/components/DataTable/components/SortIndicator.tsx +0 -50
- package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -355
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +0 -657
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +0 -913
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +0 -572
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +0 -612
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +0 -708
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +0 -479
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -475
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +0 -157
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +0 -1061
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +0 -437
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +0 -474
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +0 -617
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -1093
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +0 -139
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +0 -519
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +0 -1004
- package/src/components/DataTable/components/cellValueUtils.ts +0 -40
- package/src/components/DataTable/components/hooks/useImportModalFocus.ts +0 -53
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -122
- package/src/components/DataTable/components/index.ts +0 -16
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +0 -342
- package/src/components/DataTable/core/ActionManager.ts +0 -235
- package/src/components/DataTable/core/ColumnManager.ts +0 -205
- package/src/components/DataTable/core/DataManager.ts +0 -188
- package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
- package/src/components/DataTable/core/PluginRegistry.ts +0 -229
- package/src/components/DataTable/core/StateManager.ts +0 -312
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -123
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +0 -305
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -84
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -115
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -100
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -120
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -104
- package/src/components/DataTable/core/index.ts +0 -1
- package/src/components/DataTable/core/interfaces.ts +0 -338
- package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +0 -521
- package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +0 -167
- package/src/components/DataTable/hooks/__tests__/useDataTableConfiguration.test.ts +0 -124
- package/src/components/DataTable/hooks/__tests__/useDataTableDataPipeline.test.ts +0 -117
- package/src/components/DataTable/hooks/__tests__/useDataTablePermissions.test.ts +0 -102
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +0 -596
- package/src/components/DataTable/hooks/__tests__/useEffectiveColumnOrder.test.ts +0 -53
- package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +0 -214
- package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +0 -448
- package/src/components/DataTable/hooks/index.ts +0 -13
- package/src/components/DataTable/types.ts +0 -761
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +0 -612
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +0 -94
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +0 -266
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +0 -954
- package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +0 -573
- package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +0 -247
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +0 -570
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +0 -470
- package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +0 -251
- package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +0 -207
- package/src/components/DataTable/utils/index.ts +0 -10
- package/src/components/PublicLayout/index.ts +0 -32
- package/src/components/Select/hooks/useSelectEvents.ts +0 -87
- package/src/components/Select/hooks/useSelectSearch.ts +0 -91
- package/src/components/Select/hooks/useSelectState.ts +0 -104
- package/src/components/Select/utils/text.ts +0 -26
- package/src/hooks/__tests__/ServiceHooks.test.tsx +0 -615
- package/src/hooks/__tests__/hooks.integration.test.tsx +0 -607
- package/src/hooks/__tests__/index.unit.test.ts +0 -220
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +0 -111
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +0 -347
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +0 -144
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +0 -776
- package/src/hooks/__tests__/useDataTableState.test.ts +0 -76
- package/src/hooks/__tests__/useDebounce.unit.test.ts +0 -82
- package/src/hooks/__tests__/useEvents.unit.test.ts +0 -252
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +0 -1112
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +0 -916
- package/src/hooks/__tests__/useFileUrlCache.test.ts +0 -129
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +0 -230
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +0 -828
- package/src/hooks/__tests__/useFormDialog.test.ts +0 -478
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +0 -446
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +0 -317
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +0 -910
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +0 -294
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +0 -961
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +0 -369
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +0 -694
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePreventTabReload.test.ts +0 -88
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -785
- package/src/hooks/__tests__/usePublicEvent.test.ts +0 -678
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -630
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +0 -951
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +0 -443
- package/src/hooks/__tests__/useQueryCache.test.ts +0 -144
- package/src/hooks/__tests__/useRBAC.unit.test.ts +0 -236
- package/src/hooks/__tests__/useSessionDraft.test.ts +0 -163
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +0 -390
- package/src/hooks/__tests__/useStorage.unit.test.ts +0 -751
- package/src/hooks/__tests__/useToast.unit.test.tsx +0 -481
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +0 -37
- package/src/hooks/public/index.ts +0 -36
- package/src/hooks/public/usePublicFileDisplay.ts +0 -504
- package/src/hooks/useFileDisplay.ts +0 -715
- package/src/providers/OrganisationProvider.tsx +0 -92
- package/src/providers/__tests__/AuthProvider.test.tsx +0 -287
- package/src/providers/__tests__/EventProvider.test.tsx +0 -551
- package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
- package/src/providers/__tests__/InactivityProvider.test.tsx +0 -572
- package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -617
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +0 -424
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +0 -596
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +0 -263
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +0 -294
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +0 -434
- package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +0 -313
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +0 -486
- package/src/rbac/__tests__/cache-invalidation.test.ts +0 -399
- package/src/rbac/__tests__/engine.comprehensive.test.ts +0 -813
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +0 -82
- package/src/rbac/__tests__/rbac-core.test.tsx +0 -276
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +0 -392
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +0 -258
- package/src/rbac/__tests__/rbac-functions.test.ts +0 -647
- package/src/rbac/__tests__/rbac-integration.test.ts +0 -524
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +0 -456
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +0 -282
- package/src/rbac/audit-enhanced.ts +0 -384
- package/src/rbac/compliance/database-validator.ts +0 -165
- package/src/rbac/compliance/index.ts +0 -48
- package/src/rbac/compliance/pattern-detector.ts +0 -553
- package/src/rbac/compliance/quick-fix-suggestions.ts +0 -209
- package/src/rbac/compliance/runtime-compliance.ts +0 -99
- package/src/rbac/compliance/setup-validator.ts +0 -131
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +0 -975
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +0 -248
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +0 -242
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +0 -1107
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +0 -184
- package/src/rbac/components/index.ts +0 -26
- package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +0 -432
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +0 -579
- package/src/rbac/hooks/index.ts +0 -34
- package/src/rbac/hooks/permissions/index.ts +0 -4
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/contextValidator.test.ts +0 -128
- package/src/rbac/utils/__tests__/deep-equal.test.ts +0 -53
- package/src/rbac/utils/__tests__/eventContext.test.ts +0 -433
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -490
- package/src/services/__tests__/AuthService.restoreSession.test.ts +0 -39
- package/src/services/__tests__/AuthService.test.ts +0 -1332
- package/src/services/__tests__/BaseService.test.ts +0 -314
- package/src/services/__tests__/EventService.eventColours.test.ts +0 -76
- package/src/services/__tests__/EventService.test.ts +0 -1025
- package/src/services/__tests__/InactivityService.lifecycle.test.ts +0 -411
- package/src/services/__tests__/InactivityService.test.ts +0 -654
- package/src/services/__tests__/OrganisationService.pagination.test.ts +0 -409
- package/src/services/__tests__/OrganisationService.test.ts +0 -1176
- package/src/theming/__tests__/parseEventColours.test.ts +0 -321
- package/src/theming/__tests__/runtime.test.ts +0 -569
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/types/__tests__/guards.test.ts +0 -246
- package/src/types/__tests__/organisation.roles.test.ts +0 -55
- package/src/types/__tests__/organisation.test.ts +0 -1133
- package/src/types/__tests__/theme.test.ts +0 -830
- package/src/types/__tests__/type-validation.test.ts +0 -526
- package/src/types/__tests__/validation.test.ts +0 -731
- package/src/utils/__tests__/appConfig.unit.test.ts +0 -55
- package/src/utils/__tests__/audit.unit.test.ts +0 -69
- package/src/utils/__tests__/auth-utils.unit.test.ts +0 -70
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +0 -339
- package/src/utils/__tests__/cn.unit.test.ts +0 -34
- package/src/utils/__tests__/debugLogger.test.ts +0 -417
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +0 -818
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +0 -318
- package/src/utils/__tests__/formatDate.unit.test.ts +0 -109
- package/src/utils/__tests__/formatting.unit.test.ts +0 -99
- package/src/utils/__tests__/index.unit.test.ts +0 -251
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +0 -321
- package/src/utils/__tests__/logger.unit.test.ts +0 -398
- package/src/utils/__tests__/organisationContext.unit.test.ts +0 -191
- package/src/utils/__tests__/performanceBenchmark.test.ts +0 -175
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +0 -253
- package/src/utils/__tests__/permissionTypes.unit.test.ts +0 -250
- package/src/utils/__tests__/permissionUtils.unit.test.ts +0 -362
- package/src/utils/__tests__/sanitization.unit.test.ts +0 -346
- package/src/utils/__tests__/schemaUtils.unit.test.ts +0 -441
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +0 -335
- package/src/utils/__tests__/secureErrors.unit.test.ts +0 -390
- package/src/utils/__tests__/secureStorage.unit.test.ts +0 -289
- package/src/utils/__tests__/security.unit.test.ts +0 -149
- package/src/utils/__tests__/securityMonitor.unit.test.ts +0 -276
- package/src/utils/__tests__/sessionTracking.unit.test.ts +0 -218
- package/src/utils/__tests__/timezone.test.ts +0 -345
- package/src/utils/__tests__/validation.unit.test.ts +0 -308
- package/src/utils/__tests__/validationUtils.unit.test.ts +0 -555
- package/src/utils/app/appNameResolver.simple.test.ts +0 -212
- package/src/utils/file-reference/__tests__/file-reference.test.ts +0 -875
- package/src/utils/google-places/index.ts +0 -26
- package/src/utils/location/index.ts +0 -16
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +0 -135
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +0 -123
- package/src/utils/storage/__tests__/helpers.unit.test.ts +0 -332
- package/src/utils/storage/__tests__/index.unit.test.ts +0 -16
- package/src/utils/storage/index.ts +0 -67
- package/src/utils/timezone/index.ts +0 -17
- package/src/utils/validation/__tests__/csrf.test.ts +0 -105
- package/src/utils/validation/__tests__/htmlSanitization.unit.test.ts +0 -598
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +0 -92
- package/src/utils/validation/__tests__/validationUtils.test.ts +0 -72
- package/src/utils/validation/index.ts +0 -73
- /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
- /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
- /package/src/providers/{__tests__/README.md → README.md} +0 -0
- /package/src/types/{__tests__/README.md → README.md} +0 -0
|
@@ -0,0 +1,2321 @@
|
|
|
1
|
+
import { emitAuditEvent, createAuditManager, setGlobalAuditManager } from './chunk-QRYSEPHB.js';
|
|
2
|
+
import { createLogger } from './chunk-BTHN5MKC.js';
|
|
3
|
+
import { ok, err } from './chunk-44CNXN4P.js';
|
|
4
|
+
|
|
5
|
+
// src/rbac/types.ts
|
|
6
|
+
var RBACError = class extends Error {
|
|
7
|
+
constructor(message, code, context) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.name = "RBACError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var PermissionDeniedError = class extends RBACError {
|
|
15
|
+
constructor(permission, context) {
|
|
16
|
+
super(
|
|
17
|
+
`Permission denied: ${permission}`,
|
|
18
|
+
"PERMISSION_DENIED",
|
|
19
|
+
{ permission, ...context }
|
|
20
|
+
);
|
|
21
|
+
this.name = "PermissionDeniedError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var OrganisationContextRequiredError = class extends RBACError {
|
|
25
|
+
constructor() {
|
|
26
|
+
super(
|
|
27
|
+
"Organisation context is required for this operation",
|
|
28
|
+
"ORGANISATION_CONTEXT_REQUIRED"
|
|
29
|
+
);
|
|
30
|
+
this.name = "OrganisationContextRequiredError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var EventContextRequiredError = class extends RBACError {
|
|
34
|
+
constructor() {
|
|
35
|
+
super(
|
|
36
|
+
"Event context is required for this operation",
|
|
37
|
+
"EVENT_CONTEXT_REQUIRED"
|
|
38
|
+
);
|
|
39
|
+
this.name = "EventContextRequiredError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var RBACNotInitializedError = class extends RBACError {
|
|
43
|
+
constructor() {
|
|
44
|
+
super(
|
|
45
|
+
"RBAC system not initialized. Please call setupRBAC(supabase) before using any RBAC components or hooks. See: https://docs.pace-core.dev/rbac/setup",
|
|
46
|
+
"RBAC_NOT_INITIALIZED"
|
|
47
|
+
);
|
|
48
|
+
this.name = "RBACNotInitializedError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var InvalidScopeError = class extends RBACError {
|
|
52
|
+
constructor(scope, reason) {
|
|
53
|
+
super(
|
|
54
|
+
`Invalid scope provided: ${JSON.stringify(scope)}. ${reason}`,
|
|
55
|
+
"INVALID_SCOPE",
|
|
56
|
+
{ scope, reason }
|
|
57
|
+
);
|
|
58
|
+
this.name = "InvalidScopeError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var MissingUserContextError = class extends RBACError {
|
|
62
|
+
constructor() {
|
|
63
|
+
super(
|
|
64
|
+
"User context is required but not available. Make sure to wrap your app with an auth provider.",
|
|
65
|
+
"MISSING_USER_CONTEXT"
|
|
66
|
+
);
|
|
67
|
+
this.name = "MissingUserContextError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/rbac/cache.ts
|
|
72
|
+
var RBACCache = class {
|
|
73
|
+
constructor() {
|
|
74
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
75
|
+
this.sessionCache = /* @__PURE__ */ new Map();
|
|
76
|
+
this.TTL = 120 * 1e3;
|
|
77
|
+
// 120 seconds (short-term) - increased from 60s
|
|
78
|
+
this.SESSION_TTL = 15 * 60 * 1e3;
|
|
79
|
+
// 15 minutes (session-level) - increased from 5min
|
|
80
|
+
this.invalidationCallbacks = /* @__PURE__ */ new Set();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get a value from the cache
|
|
84
|
+
*
|
|
85
|
+
* Checks both short-term cache and session cache.
|
|
86
|
+
*
|
|
87
|
+
* @param key - Cache key
|
|
88
|
+
* @param useSessionCache - Whether to check session cache (default: true)
|
|
89
|
+
* @returns Cached value or null if not found/expired
|
|
90
|
+
*/
|
|
91
|
+
get(key, useSessionCache = true) {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const entry = this.cache.get(key);
|
|
94
|
+
if (entry && now <= entry.expires) {
|
|
95
|
+
return entry.data;
|
|
96
|
+
}
|
|
97
|
+
if (entry && now > entry.expires) {
|
|
98
|
+
this.cache.delete(key);
|
|
99
|
+
}
|
|
100
|
+
if (useSessionCache) {
|
|
101
|
+
const sessionEntry = this.sessionCache.get(key);
|
|
102
|
+
if (sessionEntry && now <= sessionEntry.expires) {
|
|
103
|
+
return sessionEntry.data;
|
|
104
|
+
}
|
|
105
|
+
if (sessionEntry && now > sessionEntry.expires) {
|
|
106
|
+
this.sessionCache.delete(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Set a value in the cache
|
|
113
|
+
*
|
|
114
|
+
* @param key - Cache key
|
|
115
|
+
* @param data - Data to cache
|
|
116
|
+
* @param ttl - Time to live in milliseconds (defaults to 60s)
|
|
117
|
+
* @param useSessionCache - Whether to also store in session cache (default: false for page-level checks)
|
|
118
|
+
*/
|
|
119
|
+
set(key, data, ttl = this.TTL, useSessionCache = false) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const expires = ttl <= 0 ? now - 1 : now + ttl;
|
|
122
|
+
this.cache.set(key, {
|
|
123
|
+
data,
|
|
124
|
+
expires
|
|
125
|
+
});
|
|
126
|
+
if (useSessionCache) {
|
|
127
|
+
const sessionExpires = ttl <= 0 ? now - 1 : now + this.SESSION_TTL;
|
|
128
|
+
this.sessionCache.set(key, {
|
|
129
|
+
data,
|
|
130
|
+
expires: sessionExpires
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Delete a specific key from the cache
|
|
136
|
+
*
|
|
137
|
+
* @param key - Cache key to delete
|
|
138
|
+
*/
|
|
139
|
+
delete(key) {
|
|
140
|
+
this.cache.delete(key);
|
|
141
|
+
this.sessionCache.delete(key);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Invalidate cache entries matching a pattern
|
|
145
|
+
*
|
|
146
|
+
* @param pattern - Pattern to match against cache keys
|
|
147
|
+
*/
|
|
148
|
+
invalidate(pattern) {
|
|
149
|
+
const trimmedPattern = pattern?.trim();
|
|
150
|
+
if (!trimmedPattern) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const matcher = this.createMatcher(trimmedPattern);
|
|
154
|
+
const keysToDelete = [];
|
|
155
|
+
for (const key of this.cache.keys()) {
|
|
156
|
+
if (matcher(key)) {
|
|
157
|
+
keysToDelete.push(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const key of this.sessionCache.keys()) {
|
|
161
|
+
if (matcher(key) && !keysToDelete.includes(key)) {
|
|
162
|
+
keysToDelete.push(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
keysToDelete.forEach((key) => {
|
|
166
|
+
this.cache.delete(key);
|
|
167
|
+
this.sessionCache.delete(key);
|
|
168
|
+
});
|
|
169
|
+
this.invalidationCallbacks.forEach((callback) => callback(trimmedPattern));
|
|
170
|
+
}
|
|
171
|
+
createMatcher(pattern) {
|
|
172
|
+
if (pattern.includes("*")) {
|
|
173
|
+
const escapedSegments = pattern.split("*").map((segment) => segment.replace(/[|\\{}()[\]^$+?.-]/g, "\\$&"));
|
|
174
|
+
const regexPattern = escapedSegments.join(".*");
|
|
175
|
+
const regex = new RegExp(regexPattern);
|
|
176
|
+
return (key) => regex.test(key);
|
|
177
|
+
}
|
|
178
|
+
return (key) => key.includes(pattern);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Clear all cache entries
|
|
182
|
+
*/
|
|
183
|
+
clear() {
|
|
184
|
+
this.cache.clear();
|
|
185
|
+
this.sessionCache.clear();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get cache statistics
|
|
189
|
+
*/
|
|
190
|
+
getStats() {
|
|
191
|
+
return {
|
|
192
|
+
size: this.cache.size,
|
|
193
|
+
sessionSize: this.sessionCache.size,
|
|
194
|
+
ttl: this.TTL,
|
|
195
|
+
sessionTtl: this.SESSION_TTL,
|
|
196
|
+
keys: Array.from(this.cache.keys())
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Add an invalidation callback
|
|
201
|
+
*
|
|
202
|
+
* @param callback - Function to call when cache is invalidated
|
|
203
|
+
*/
|
|
204
|
+
onInvalidate(callback) {
|
|
205
|
+
this.invalidationCallbacks.add(callback);
|
|
206
|
+
return () => {
|
|
207
|
+
this.invalidationCallbacks.delete(callback);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate cache key for permission check (simplified signature)
|
|
212
|
+
*
|
|
213
|
+
* @param userId - User ID
|
|
214
|
+
* @param permission - Permission string
|
|
215
|
+
* @param organisationId - Organisation ID (optional)
|
|
216
|
+
* @param eventId - Event ID (optional)
|
|
217
|
+
* @param appId - App ID (optional)
|
|
218
|
+
* @param pageId - Page ID (optional)
|
|
219
|
+
* @returns String cache key
|
|
220
|
+
*/
|
|
221
|
+
static generateKey(userId, permission, organisationId, eventId, appId, pageId) {
|
|
222
|
+
const parts = [
|
|
223
|
+
"perm",
|
|
224
|
+
userId,
|
|
225
|
+
organisationId || "null",
|
|
226
|
+
eventId || "null",
|
|
227
|
+
appId || "null",
|
|
228
|
+
permission || "null",
|
|
229
|
+
pageId || "null"
|
|
230
|
+
];
|
|
231
|
+
return parts.join(":");
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Generate cache key for permission check (object signature)
|
|
235
|
+
*
|
|
236
|
+
* @param key - Permission cache key object
|
|
237
|
+
* @returns String cache key
|
|
238
|
+
*/
|
|
239
|
+
static generatePermissionKey(key) {
|
|
240
|
+
const parts = [
|
|
241
|
+
"perm",
|
|
242
|
+
key.userId,
|
|
243
|
+
key.organisationId || "null",
|
|
244
|
+
key.eventId || "null",
|
|
245
|
+
key.appId || "null",
|
|
246
|
+
key.permission || "null",
|
|
247
|
+
key.pageId || "null"
|
|
248
|
+
];
|
|
249
|
+
return parts.join(":");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Generate cache key for access level
|
|
253
|
+
*
|
|
254
|
+
* @param userId - User ID
|
|
255
|
+
* @param organisationId - Organisation ID
|
|
256
|
+
* @param eventId - Event ID (optional)
|
|
257
|
+
* @param appId - App ID (optional)
|
|
258
|
+
* @returns String cache key
|
|
259
|
+
*/
|
|
260
|
+
static generateAccessLevelKey(userId, organisationId, eventId, appId) {
|
|
261
|
+
const parts = [
|
|
262
|
+
"access",
|
|
263
|
+
userId,
|
|
264
|
+
organisationId,
|
|
265
|
+
eventId || "null",
|
|
266
|
+
appId || "null"
|
|
267
|
+
];
|
|
268
|
+
return parts.join(":");
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Generate cache key for permission map
|
|
272
|
+
*
|
|
273
|
+
* @param userId - User ID
|
|
274
|
+
* @param organisationId - Organisation ID
|
|
275
|
+
* @param eventId - Event ID (optional)
|
|
276
|
+
* @param appId - App ID (optional)
|
|
277
|
+
* @returns String cache key
|
|
278
|
+
*/
|
|
279
|
+
static generatePermissionMapKey(userId, organisationId, eventId, appId) {
|
|
280
|
+
const parts = [
|
|
281
|
+
"map",
|
|
282
|
+
userId,
|
|
283
|
+
organisationId,
|
|
284
|
+
eventId || "null",
|
|
285
|
+
appId || "null"
|
|
286
|
+
];
|
|
287
|
+
return parts.join(":");
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var rbacCache = new RBACCache();
|
|
291
|
+
var CACHE_PATTERNS = {
|
|
292
|
+
USER: (userId) => `:${userId}:`,
|
|
293
|
+
ORGANISATION: (organisationId) => `:${organisationId}:`,
|
|
294
|
+
EVENT: (eventId) => `:${eventId}:`,
|
|
295
|
+
APP: (appId) => `:${appId}`,
|
|
296
|
+
PERMISSION: (userId, organisationId) => `perm:${userId}:${organisationId}:`
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/rbac/cache-invalidation.ts
|
|
300
|
+
var log = createLogger("RBACCache");
|
|
301
|
+
var INVALIDATION_PATTERNS = {
|
|
302
|
+
// User-level invalidation
|
|
303
|
+
USER_ROLES_CHANGED: (userId) => [
|
|
304
|
+
CACHE_PATTERNS.USER(userId),
|
|
305
|
+
`perm:${userId}:*`,
|
|
306
|
+
`access:${userId}:*`,
|
|
307
|
+
`map:${userId}:*`
|
|
308
|
+
],
|
|
309
|
+
// Organisation-level invalidation
|
|
310
|
+
ORGANISATION_PERMISSIONS_CHANGED: (organisationId) => [
|
|
311
|
+
CACHE_PATTERNS.ORGANISATION(organisationId),
|
|
312
|
+
`perm:*:${organisationId}:*`,
|
|
313
|
+
`access:*:${organisationId}:*`,
|
|
314
|
+
`map:*:${organisationId}:*`
|
|
315
|
+
],
|
|
316
|
+
// Event-level invalidation
|
|
317
|
+
EVENT_PERMISSIONS_CHANGED: (eventId) => [
|
|
318
|
+
CACHE_PATTERNS.EVENT(eventId),
|
|
319
|
+
`perm:*:*:${eventId}:*`,
|
|
320
|
+
`access:*:*:${eventId}:*`,
|
|
321
|
+
`map:*:*:${eventId}:*`
|
|
322
|
+
],
|
|
323
|
+
// App-level invalidation
|
|
324
|
+
APP_PERMISSIONS_CHANGED: (appId) => [
|
|
325
|
+
CACHE_PATTERNS.APP(appId),
|
|
326
|
+
`perm:*:*:*:${appId}:*`,
|
|
327
|
+
`access:*:*:*:${appId}`,
|
|
328
|
+
`map:*:*:*:${appId}`
|
|
329
|
+
],
|
|
330
|
+
// Page-level invalidation
|
|
331
|
+
PAGE_PERMISSIONS_CHANGED: (pageId) => [
|
|
332
|
+
`perm:*:*:*:*:${pageId}`,
|
|
333
|
+
`map:*:*:*:*`
|
|
334
|
+
]
|
|
335
|
+
};
|
|
336
|
+
var RBACCacheInvalidationManager = class {
|
|
337
|
+
constructor(supabase) {
|
|
338
|
+
this.invalidationCallbacks = /* @__PURE__ */ new Set();
|
|
339
|
+
this.channels = [];
|
|
340
|
+
this.supabase = supabase;
|
|
341
|
+
this.setupRealtimeSubscriptions();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Add a callback for cache invalidation events
|
|
345
|
+
*
|
|
346
|
+
* @param callback - Function to call when cache is invalidated
|
|
347
|
+
* @returns Unsubscribe function
|
|
348
|
+
*/
|
|
349
|
+
onInvalidation(callback) {
|
|
350
|
+
this.invalidationCallbacks.add(callback);
|
|
351
|
+
return () => this.invalidationCallbacks.delete(callback);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Invalidate cache for a specific user
|
|
355
|
+
*
|
|
356
|
+
* @param userId - User ID
|
|
357
|
+
* @param reason - Reason for invalidation
|
|
358
|
+
*/
|
|
359
|
+
invalidateUser(userId, reason) {
|
|
360
|
+
const patterns = INVALIDATION_PATTERNS.USER_ROLES_CHANGED(userId);
|
|
361
|
+
this.invalidatePatterns(patterns, reason);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Invalidate cache for a specific organisation
|
|
365
|
+
*
|
|
366
|
+
* @param organisationId - Organisation ID
|
|
367
|
+
* @param reason - Reason for invalidation
|
|
368
|
+
*/
|
|
369
|
+
invalidateOrganisation(organisationId, reason) {
|
|
370
|
+
const patterns = INVALIDATION_PATTERNS.ORGANISATION_PERMISSIONS_CHANGED(organisationId);
|
|
371
|
+
this.invalidatePatterns(patterns, reason);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Invalidate cache for a specific event
|
|
375
|
+
*
|
|
376
|
+
* @param eventId - Event ID
|
|
377
|
+
* @param reason - Reason for invalidation
|
|
378
|
+
*/
|
|
379
|
+
invalidateEvent(eventId, reason) {
|
|
380
|
+
const patterns = INVALIDATION_PATTERNS.EVENT_PERMISSIONS_CHANGED(eventId);
|
|
381
|
+
this.invalidatePatterns(patterns, reason);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Invalidate cache for a specific app
|
|
385
|
+
*
|
|
386
|
+
* @param appId - App ID
|
|
387
|
+
* @param reason - Reason for invalidation
|
|
388
|
+
*/
|
|
389
|
+
invalidateApp(appId, reason) {
|
|
390
|
+
const patterns = INVALIDATION_PATTERNS.APP_PERMISSIONS_CHANGED(appId);
|
|
391
|
+
this.invalidatePatterns(patterns, reason);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Invalidate cache for a specific page
|
|
395
|
+
*
|
|
396
|
+
* @param pageId - Page ID
|
|
397
|
+
* @param reason - Reason for invalidation
|
|
398
|
+
*/
|
|
399
|
+
invalidatePage(pageId, reason) {
|
|
400
|
+
const patterns = INVALIDATION_PATTERNS.PAGE_PERMISSIONS_CHANGED(pageId);
|
|
401
|
+
this.invalidatePatterns(patterns, reason);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Invalidate cache patterns and notify callbacks
|
|
405
|
+
*
|
|
406
|
+
* @param patterns - Array of cache patterns to invalidate
|
|
407
|
+
* @param reason - Reason for invalidation
|
|
408
|
+
*/
|
|
409
|
+
invalidatePatterns(patterns, reason) {
|
|
410
|
+
log.debug(`Invalidating patterns: ${patterns.join(", ")} (${reason})`);
|
|
411
|
+
patterns.forEach((pattern) => {
|
|
412
|
+
rbacCache.invalidate(pattern);
|
|
413
|
+
});
|
|
414
|
+
this.invalidationCallbacks.forEach((callback) => {
|
|
415
|
+
patterns.forEach((pattern) => callback(pattern));
|
|
416
|
+
});
|
|
417
|
+
emitAuditEvent({
|
|
418
|
+
type: "permission_check",
|
|
419
|
+
userId: "system",
|
|
420
|
+
organisationId: "00000000-0000-0000-0000-000000000000",
|
|
421
|
+
permission: "cache:invalidate",
|
|
422
|
+
decision: true,
|
|
423
|
+
source: "api",
|
|
424
|
+
duration_ms: 0,
|
|
425
|
+
metadata: {
|
|
426
|
+
reason,
|
|
427
|
+
patterns,
|
|
428
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
429
|
+
cache_invalidation: true
|
|
430
|
+
}
|
|
431
|
+
}).catch((error) => {
|
|
432
|
+
log.warn("Failed to log cache invalidation audit event:", error);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Cleanup subscriptions only (not all callbacks)
|
|
437
|
+
* Used internally to cleanup before setting up new subscriptions
|
|
438
|
+
*/
|
|
439
|
+
cleanupSubscriptions() {
|
|
440
|
+
this.channels.forEach((channel) => {
|
|
441
|
+
try {
|
|
442
|
+
if (channel && typeof channel.unsubscribe === "function") {
|
|
443
|
+
channel.unsubscribe();
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
log.warn("Failed to unsubscribe from channel:", error);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
this.channels = [];
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Setup realtime subscriptions for automatic cache invalidation
|
|
453
|
+
* Always cleans up existing subscriptions before setting up new ones to prevent duplicates
|
|
454
|
+
*/
|
|
455
|
+
setupRealtimeSubscriptions() {
|
|
456
|
+
this.cleanupSubscriptions();
|
|
457
|
+
if (!this.supabase.channel || typeof this.supabase.channel !== "function") {
|
|
458
|
+
log.debug("Realtime not available, skipping subscriptions");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const orgRolesChannel = this.supabase.channel("rbac_organisation_roles_changes").on("postgres_changes", {
|
|
462
|
+
event: "*",
|
|
463
|
+
schema: "public",
|
|
464
|
+
table: "rbac_organisation_roles"
|
|
465
|
+
}, (payload) => {
|
|
466
|
+
const payloadData = payload;
|
|
467
|
+
const { organisation_id, user_id } = payloadData.new || payloadData.old || {};
|
|
468
|
+
if (organisation_id) {
|
|
469
|
+
this.invalidateOrganisation(organisation_id, `organisation_role_${payloadData.eventType || "unknown"}`);
|
|
470
|
+
}
|
|
471
|
+
if (user_id) {
|
|
472
|
+
this.invalidateUser(user_id, `organisation_role_${payloadData.eventType || "unknown"}`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
const orgRolesSubscription = orgRolesChannel.subscribe();
|
|
476
|
+
this.channels.push(orgRolesSubscription);
|
|
477
|
+
const eventAppRolesChannel = this.supabase.channel("rbac_event_app_roles_changes").on("postgres_changes", {
|
|
478
|
+
event: "*",
|
|
479
|
+
schema: "public",
|
|
480
|
+
table: "rbac_event_app_roles"
|
|
481
|
+
}, (payload) => {
|
|
482
|
+
const payloadData = payload;
|
|
483
|
+
const { organisation_id, user_id, event_id, app_id } = payloadData.new || payloadData.old || {};
|
|
484
|
+
if (organisation_id) {
|
|
485
|
+
this.invalidateOrganisation(organisation_id, `event_app_role_${payloadData.eventType || "unknown"}`);
|
|
486
|
+
}
|
|
487
|
+
if (user_id) {
|
|
488
|
+
this.invalidateUser(user_id, `event_app_role_${payloadData.eventType || "unknown"}`);
|
|
489
|
+
}
|
|
490
|
+
if (event_id) {
|
|
491
|
+
this.invalidateEvent(event_id, `event_app_role_${payloadData.eventType || "unknown"}`);
|
|
492
|
+
}
|
|
493
|
+
if (app_id) {
|
|
494
|
+
this.invalidateApp(app_id, `event_app_role_${payloadData.eventType || "unknown"}`);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
const eventAppRolesSubscription = eventAppRolesChannel.subscribe();
|
|
498
|
+
this.channels.push(eventAppRolesSubscription);
|
|
499
|
+
const globalRolesChannel = this.supabase.channel("rbac_global_roles_changes").on("postgres_changes", {
|
|
500
|
+
event: "*",
|
|
501
|
+
schema: "public",
|
|
502
|
+
table: "rbac_global_roles"
|
|
503
|
+
}, (payload) => {
|
|
504
|
+
const payloadData = payload;
|
|
505
|
+
const { user_id } = payloadData.new || payloadData.old || {};
|
|
506
|
+
if (user_id) {
|
|
507
|
+
this.invalidateUser(user_id, `global_role_${payloadData.eventType || "unknown"}`);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
const globalRolesSubscription = globalRolesChannel.subscribe();
|
|
511
|
+
this.channels.push(globalRolesSubscription);
|
|
512
|
+
const pagePermissionsChannel = this.supabase.channel("rbac_page_permissions_changes").on("postgres_changes", {
|
|
513
|
+
event: "*",
|
|
514
|
+
schema: "public",
|
|
515
|
+
table: "rbac_page_permissions"
|
|
516
|
+
}, (payload) => {
|
|
517
|
+
const { organisation_id, app_page_id } = payload.new || payload.old || {};
|
|
518
|
+
if (organisation_id) {
|
|
519
|
+
this.invalidateOrganisation(organisation_id, `page_permission_${payload.eventType}`);
|
|
520
|
+
}
|
|
521
|
+
if (app_page_id) {
|
|
522
|
+
this.invalidatePage(app_page_id, `page_permission_${payload.eventType}`);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
const pagePermissionsSubscription = pagePermissionsChannel.subscribe();
|
|
526
|
+
this.channels.push(pagePermissionsSubscription);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Cleanup all realtime subscriptions
|
|
530
|
+
* Call this when the manager is no longer needed to prevent memory leaks
|
|
531
|
+
*/
|
|
532
|
+
cleanup() {
|
|
533
|
+
this.channels.forEach((channel) => {
|
|
534
|
+
try {
|
|
535
|
+
if (channel && typeof channel.unsubscribe === "function") {
|
|
536
|
+
channel.unsubscribe();
|
|
537
|
+
}
|
|
538
|
+
} catch (error) {
|
|
539
|
+
log.warn("Failed to unsubscribe from channel:", error);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
this.channels = [];
|
|
543
|
+
this.invalidationCallbacks.clear();
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Manually trigger cache invalidation for all users in an organisation
|
|
547
|
+
*
|
|
548
|
+
* @param organisationId - Organisation ID
|
|
549
|
+
* @param reason - Reason for invalidation
|
|
550
|
+
*/
|
|
551
|
+
async invalidateAllUsersInOrganisation(organisationId, reason) {
|
|
552
|
+
const { data: users } = await this.supabase.from("rbac_organisation_roles").select("user_id").eq("organisation_id", organisationId).eq("is_active", true);
|
|
553
|
+
if (users) {
|
|
554
|
+
users.forEach(({ user_id }) => {
|
|
555
|
+
this.invalidateUser(user_id, reason);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Clear all cache entries
|
|
561
|
+
*/
|
|
562
|
+
clearAllCache() {
|
|
563
|
+
log.debug("Clearing all cache entries");
|
|
564
|
+
rbacCache.clear();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var globalCacheInvalidationManager = null;
|
|
568
|
+
function initializeCacheInvalidation(supabase) {
|
|
569
|
+
if (globalCacheInvalidationManager) {
|
|
570
|
+
globalCacheInvalidationManager.cleanup();
|
|
571
|
+
}
|
|
572
|
+
globalCacheInvalidationManager = new RBACCacheInvalidationManager(supabase);
|
|
573
|
+
return globalCacheInvalidationManager;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/rbac/errors.ts
|
|
577
|
+
var RATE_LIMIT_STATUS_CODES = /* @__PURE__ */ new Set([429]);
|
|
578
|
+
var AUTH_STATUS_CODES = /* @__PURE__ */ new Set([401]);
|
|
579
|
+
var AUTHZ_STATUS_CODES = /* @__PURE__ */ new Set([403]);
|
|
580
|
+
function normalize(value) {
|
|
581
|
+
if (!value) {
|
|
582
|
+
return "";
|
|
583
|
+
}
|
|
584
|
+
if (typeof value === "string") {
|
|
585
|
+
return value.toLowerCase();
|
|
586
|
+
}
|
|
587
|
+
if (value instanceof Error && typeof value.message === "string") {
|
|
588
|
+
return value.message.toLowerCase();
|
|
589
|
+
}
|
|
590
|
+
return String(value).toLowerCase();
|
|
591
|
+
}
|
|
592
|
+
function categorizeByErrorClass(error) {
|
|
593
|
+
if (error instanceof PermissionDeniedError) {
|
|
594
|
+
return "authorization_error" /* AUTHORIZATION */;
|
|
595
|
+
}
|
|
596
|
+
if (error instanceof OrganisationContextRequiredError || error instanceof InvalidScopeError) {
|
|
597
|
+
return "validation_error" /* VALIDATION */;
|
|
598
|
+
}
|
|
599
|
+
if (error instanceof MissingUserContextError) {
|
|
600
|
+
return "authentication_error" /* AUTHENTICATION */;
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
function categorizeByRBACErrorCode(error) {
|
|
605
|
+
if (!(error instanceof RBACError)) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
switch (error.code) {
|
|
609
|
+
case "PERMISSION_DENIED":
|
|
610
|
+
return "authorization_error" /* AUTHORIZATION */;
|
|
611
|
+
case "ORGANISATION_CONTEXT_REQUIRED":
|
|
612
|
+
case "INVALID_SCOPE":
|
|
613
|
+
return "validation_error" /* VALIDATION */;
|
|
614
|
+
case "MISSING_USER_CONTEXT":
|
|
615
|
+
return "authentication_error" /* AUTHENTICATION */;
|
|
616
|
+
default:
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function categorizeByHttpStatus(error) {
|
|
621
|
+
if (!error || typeof error !== "object") {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const status = error.status;
|
|
625
|
+
if (typeof status !== "number") {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
if (RATE_LIMIT_STATUS_CODES.has(status)) {
|
|
629
|
+
return "rate_limit_error" /* RATE_LIMIT */;
|
|
630
|
+
}
|
|
631
|
+
if (AUTH_STATUS_CODES.has(status)) {
|
|
632
|
+
return "authentication_error" /* AUTHENTICATION */;
|
|
633
|
+
}
|
|
634
|
+
if (AUTHZ_STATUS_CODES.has(status)) {
|
|
635
|
+
return "authorization_error" /* AUTHORIZATION */;
|
|
636
|
+
}
|
|
637
|
+
if (status >= 500) {
|
|
638
|
+
return "database_error" /* DATABASE */;
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
function categorizeByCodeString(error) {
|
|
643
|
+
if (!error || typeof error !== "object") {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
const codeValue = normalize(error.code);
|
|
647
|
+
if (!codeValue) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
if (codeValue.includes("network")) {
|
|
651
|
+
return "network_error" /* NETWORK */;
|
|
652
|
+
}
|
|
653
|
+
if (codeValue.includes("postgres") || codeValue.includes("database") || codeValue.includes("db")) {
|
|
654
|
+
return "database_error" /* DATABASE */;
|
|
655
|
+
}
|
|
656
|
+
if (codeValue.includes("rate")) {
|
|
657
|
+
return "rate_limit_error" /* RATE_LIMIT */;
|
|
658
|
+
}
|
|
659
|
+
if (codeValue.includes("auth")) {
|
|
660
|
+
return "authentication_error" /* AUTHENTICATION */;
|
|
661
|
+
}
|
|
662
|
+
if (codeValue.includes("permission")) {
|
|
663
|
+
return "authorization_error" /* AUTHORIZATION */;
|
|
664
|
+
}
|
|
665
|
+
if (codeValue.includes("scope") || codeValue.includes("invalid")) {
|
|
666
|
+
return "validation_error" /* VALIDATION */;
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
function categorizeByMessage(error) {
|
|
671
|
+
const message = normalize(error);
|
|
672
|
+
if (message.includes("timeout") || message.includes("network") || message.includes("fetch")) {
|
|
673
|
+
return "network_error" /* NETWORK */;
|
|
674
|
+
}
|
|
675
|
+
if (message.includes("postgres") || message.includes("database") || message.includes("connection")) {
|
|
676
|
+
return "database_error" /* DATABASE */;
|
|
677
|
+
}
|
|
678
|
+
if (message.includes("rate limit") || message.includes("too many requests")) {
|
|
679
|
+
return "rate_limit_error" /* RATE_LIMIT */;
|
|
680
|
+
}
|
|
681
|
+
if (message.includes("permission") || message.includes("forbidden")) {
|
|
682
|
+
return "authorization_error" /* AUTHORIZATION */;
|
|
683
|
+
}
|
|
684
|
+
if (message.includes("auth") || message.includes("token") || message.includes("session")) {
|
|
685
|
+
return "authentication_error" /* AUTHENTICATION */;
|
|
686
|
+
}
|
|
687
|
+
if (message.includes("invalid") || message.includes("scope") || message.includes("validation")) {
|
|
688
|
+
return "validation_error" /* VALIDATION */;
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
function categorizeError(error) {
|
|
693
|
+
const byClass = categorizeByErrorClass(error);
|
|
694
|
+
if (byClass !== null) return byClass;
|
|
695
|
+
const byCode = categorizeByRBACErrorCode(error);
|
|
696
|
+
if (byCode !== null) return byCode;
|
|
697
|
+
const byStatus = categorizeByHttpStatus(error);
|
|
698
|
+
if (byStatus !== null) return byStatus;
|
|
699
|
+
const byCodeString = categorizeByCodeString(error);
|
|
700
|
+
if (byCodeString !== null) return byCodeString;
|
|
701
|
+
const byMessage = categorizeByMessage(error);
|
|
702
|
+
if (byMessage !== null) return byMessage;
|
|
703
|
+
return "unknown_error" /* UNKNOWN */;
|
|
704
|
+
}
|
|
705
|
+
function mapErrorCategoryToSecurityEventType(category) {
|
|
706
|
+
switch (category) {
|
|
707
|
+
case "authorization_error" /* AUTHORIZATION */:
|
|
708
|
+
return "permission_denied";
|
|
709
|
+
case "network_error" /* NETWORK */:
|
|
710
|
+
return "network_error";
|
|
711
|
+
case "database_error" /* DATABASE */:
|
|
712
|
+
return "database_error";
|
|
713
|
+
case "validation_error" /* VALIDATION */:
|
|
714
|
+
return "validation_error";
|
|
715
|
+
case "rate_limit_error" /* RATE_LIMIT */:
|
|
716
|
+
return "rate_limit_error";
|
|
717
|
+
case "authentication_error" /* AUTHENTICATION */:
|
|
718
|
+
return "authentication_error";
|
|
719
|
+
case "unknown_error" /* UNKNOWN */:
|
|
720
|
+
default:
|
|
721
|
+
return "unknown_error";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/rbac/security.ts
|
|
726
|
+
var log2 = createLogger("RBACSecurity");
|
|
727
|
+
var RBACSecurityValidator = class {
|
|
728
|
+
/**
|
|
729
|
+
* Validate permission string format
|
|
730
|
+
* @param permission - Permission string to validate
|
|
731
|
+
* @returns True if valid, false otherwise
|
|
732
|
+
*/
|
|
733
|
+
static validatePermission(permission) {
|
|
734
|
+
if (typeof permission !== "string" || permission.length === 0) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const permissionRegex = /^(read|create|update|delete):[a-z0-9._-]+$/;
|
|
738
|
+
return permissionRegex.test(permission);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Validate UUID format
|
|
742
|
+
* @param uuid - UUID string to validate
|
|
743
|
+
* @returns True if valid, false otherwise
|
|
744
|
+
*/
|
|
745
|
+
static validateUUID(uuid) {
|
|
746
|
+
if (typeof uuid !== "string" || uuid.length === 0) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
750
|
+
return uuidRegex.test(uuid);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Validate scope object
|
|
754
|
+
* @param scope - Scope object to validate
|
|
755
|
+
* @returns True if valid, false otherwise
|
|
756
|
+
*/
|
|
757
|
+
static validateScope(scope) {
|
|
758
|
+
if (!scope || typeof scope !== "object") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
if (scope.organisationId !== void 0) {
|
|
762
|
+
if (typeof scope.organisationId === "string" && scope.organisationId.trim() === "") {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
if (scope.organisationId && !this.validateUUID(scope.organisationId)) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (scope.eventId && typeof scope.eventId !== "string") {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (scope.appId && !this.validateUUID(scope.appId)) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
return !!(scope.organisationId || scope.eventId || scope.appId);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Sanitize input string to prevent injection attacks
|
|
779
|
+
* @param input - Input string to sanitize
|
|
780
|
+
* @returns Sanitized string
|
|
781
|
+
*/
|
|
782
|
+
static sanitizeInput(input) {
|
|
783
|
+
if (typeof input !== "string") {
|
|
784
|
+
return "";
|
|
785
|
+
}
|
|
786
|
+
return input.replace(/<[^>]*>/g, "").replace(/[<>\"'&]/g, "").replace(/[;()]/g, "").replace(/javascript:/gi, "").replace(/data:/gi, "").trim();
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Validate user ID format
|
|
790
|
+
* @param userId - User ID to validate
|
|
791
|
+
* @returns True if valid, false otherwise
|
|
792
|
+
*/
|
|
793
|
+
static validateUserId(userId) {
|
|
794
|
+
return this.validateUUID(userId);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Check if permission is a wildcard permission
|
|
798
|
+
* @param permission - Permission string to check
|
|
799
|
+
* @returns True if wildcard, false otherwise
|
|
800
|
+
*/
|
|
801
|
+
static isWildcardPermission(permission) {
|
|
802
|
+
return permission.includes("*") || permission.endsWith(":*");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Validate permission hierarchy
|
|
806
|
+
* @param permission - Permission to validate
|
|
807
|
+
* @param requiredOperation - Required operation
|
|
808
|
+
* @returns True if permission matches or is higher in hierarchy
|
|
809
|
+
*/
|
|
810
|
+
static validatePermissionHierarchy(permission, requiredOperation) {
|
|
811
|
+
if (!this.validatePermission(permission)) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
const [operation] = permission.split(":");
|
|
815
|
+
const hierarchy = ["read", "create", "update", "delete"];
|
|
816
|
+
const permissionLevel = hierarchy.indexOf(operation);
|
|
817
|
+
const requiredLevel = hierarchy.indexOf(requiredOperation);
|
|
818
|
+
if (permissionLevel === -1 || requiredLevel === -1) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
return permissionLevel >= requiredLevel;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Rate limiting check (placeholder for future implementation)
|
|
825
|
+
* @param userId - User ID
|
|
826
|
+
* @param operation - Operation being performed
|
|
827
|
+
* @returns True if within rate limit, false otherwise
|
|
828
|
+
*/
|
|
829
|
+
static async checkRateLimit(_userId, _operation) {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
// Only warn once per 5 seconds per user
|
|
833
|
+
static logSecurityEvent(event) {
|
|
834
|
+
const securityEvent = {
|
|
835
|
+
...event,
|
|
836
|
+
timestamp: event.timestamp || /* @__PURE__ */ new Date(),
|
|
837
|
+
severity: this.getEventSeverity(event.type)
|
|
838
|
+
};
|
|
839
|
+
if (event.type === "rate_limit_exceeded") {
|
|
840
|
+
const now = Date.now();
|
|
841
|
+
const userWarning = this.rateLimitWarningCount.get(event.userId);
|
|
842
|
+
if (userWarning) {
|
|
843
|
+
const timeSinceLastWarning = now - userWarning.lastWarning;
|
|
844
|
+
if (timeSinceLastWarning < this.RATE_LIMIT_WARNING_THROTTLE_MS) {
|
|
845
|
+
userWarning.count++;
|
|
846
|
+
this.rateLimitWarningCount.set(event.userId, userWarning);
|
|
847
|
+
return;
|
|
848
|
+
} else {
|
|
849
|
+
log2.warn("Security event (throttled):", {
|
|
850
|
+
...securityEvent,
|
|
851
|
+
details: {
|
|
852
|
+
...securityEvent.details,
|
|
853
|
+
suppressedWarnings: userWarning.count,
|
|
854
|
+
message: `Rate limit exceeded (${userWarning.count + 1} times in last ${Math.round(timeSinceLastWarning / 1e3)}s)`
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
this.rateLimitWarningCount.set(event.userId, { count: 0, lastWarning: now });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
this.rateLimitWarningCount.set(event.userId, { count: 0, lastWarning: now });
|
|
862
|
+
log2.warn("Security event:", securityEvent);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
log2.warn("Security event:", securityEvent);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get severity level for security event
|
|
870
|
+
* @param eventType - Type of security event
|
|
871
|
+
* @returns Severity level
|
|
872
|
+
*/
|
|
873
|
+
static getEventSeverity(eventType) {
|
|
874
|
+
switch (eventType) {
|
|
875
|
+
case "permission_denied":
|
|
876
|
+
return "low";
|
|
877
|
+
case "invalid_input":
|
|
878
|
+
case "rate_limit_exceeded":
|
|
879
|
+
case "rate_limit_error":
|
|
880
|
+
case "network_error":
|
|
881
|
+
return "medium";
|
|
882
|
+
case "validation_error":
|
|
883
|
+
return "high";
|
|
884
|
+
case "authentication_error":
|
|
885
|
+
case "database_error":
|
|
886
|
+
return "critical";
|
|
887
|
+
case "suspicious_activity":
|
|
888
|
+
case "unknown_error":
|
|
889
|
+
return "high";
|
|
890
|
+
default:
|
|
891
|
+
return "low";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
/**
|
|
896
|
+
* Log security event for monitoring
|
|
897
|
+
* @param event - Security event details
|
|
898
|
+
*/
|
|
899
|
+
RBACSecurityValidator.rateLimitWarningCount = /* @__PURE__ */ new Map();
|
|
900
|
+
RBACSecurityValidator.RATE_LIMIT_WARNING_THROTTLE_MS = 5e3;
|
|
901
|
+
var DEFAULT_SECURITY_CONFIG = {
|
|
902
|
+
enableInputValidation: true,
|
|
903
|
+
enableRateLimiting: true,
|
|
904
|
+
enableAuditLogging: true,
|
|
905
|
+
maxPermissionChecksPerMinute: 1e3,
|
|
906
|
+
// Increased from 100 to 1000 for normal app usage
|
|
907
|
+
suspiciousActivityThreshold: 10
|
|
908
|
+
};
|
|
909
|
+
var RBACSecurityMiddleware = class {
|
|
910
|
+
constructor(config = DEFAULT_SECURITY_CONFIG) {
|
|
911
|
+
/**
|
|
912
|
+
* In-memory rate limiting cache (sliding window)
|
|
913
|
+
* Note: For production, this should use Redis or Supabase Edge Functions
|
|
914
|
+
*/
|
|
915
|
+
this.rateLimitCache = /* @__PURE__ */ new Map();
|
|
916
|
+
this.config = config;
|
|
917
|
+
this._startCleanupInterval();
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Start periodic cleanup of expired entries
|
|
921
|
+
*/
|
|
922
|
+
_startCleanupInterval() {
|
|
923
|
+
setInterval(() => {
|
|
924
|
+
this.clearExpiredEntries();
|
|
925
|
+
}, 5 * 60 * 1e3);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Validate input before processing
|
|
929
|
+
* @param input - Input to validate
|
|
930
|
+
* @param context - Security context
|
|
931
|
+
* @returns Validation result
|
|
932
|
+
*/
|
|
933
|
+
async validateInput(input, context) {
|
|
934
|
+
const errors = [];
|
|
935
|
+
if (!RBACSecurityValidator.validateUserId(context.userId)) {
|
|
936
|
+
errors.push("Invalid user ID format");
|
|
937
|
+
}
|
|
938
|
+
const permission = typeof input.permission === "string" ? input.permission : "";
|
|
939
|
+
const isPagePermission = permission.includes(":page.") || !!input.pageId;
|
|
940
|
+
const requiresOrgId = !isPagePermission;
|
|
941
|
+
if (requiresOrgId) {
|
|
942
|
+
if (!context.organisationId) {
|
|
943
|
+
errors.push("Organisation ID is required for resource-level permissions");
|
|
944
|
+
} else if (!RBACSecurityValidator.validateUUID(context.organisationId)) {
|
|
945
|
+
errors.push("Invalid organisation ID format");
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
if (context.organisationId && !RBACSecurityValidator.validateUUID(context.organisationId)) {
|
|
949
|
+
errors.push("Invalid organisation ID format");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (permission && !RBACSecurityValidator.validatePermission(permission)) {
|
|
953
|
+
errors.push("Invalid permission format");
|
|
954
|
+
}
|
|
955
|
+
if (input.scope && !RBACSecurityValidator.validateScope(input.scope)) {
|
|
956
|
+
errors.push("Invalid scope format");
|
|
957
|
+
}
|
|
958
|
+
if (this.config.enableInputValidation) {
|
|
959
|
+
if (context.ipAddress && typeof context.ipAddress !== "string") {
|
|
960
|
+
errors.push("Invalid IP address format");
|
|
961
|
+
}
|
|
962
|
+
if (context.userAgent && typeof context.userAgent !== "string") {
|
|
963
|
+
errors.push("Invalid user agent format");
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (errors.length > 0) {
|
|
967
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
968
|
+
type: "invalid_input",
|
|
969
|
+
userId: context.userId,
|
|
970
|
+
details: { errors, input: this.sanitizeInput(JSON.stringify(input)) }
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
isValid: errors.length === 0,
|
|
975
|
+
errors
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Check rate limiting
|
|
980
|
+
* @param context - Security context
|
|
981
|
+
* @returns Rate limit check result
|
|
982
|
+
*/
|
|
983
|
+
async checkRateLimit(context) {
|
|
984
|
+
if (!this.config.enableRateLimiting) {
|
|
985
|
+
return { isAllowed: true, remaining: this.config.maxPermissionChecksPerMinute };
|
|
986
|
+
}
|
|
987
|
+
const isAllowed = await this._checkRateLimitInternal(context.userId);
|
|
988
|
+
const remaining = isAllowed ? this.config.maxPermissionChecksPerMinute - this._getRequestCount(context.userId) : 0;
|
|
989
|
+
return {
|
|
990
|
+
isAllowed,
|
|
991
|
+
remaining: Math.max(0, remaining)
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
async _checkRateLimitInternal(userId) {
|
|
995
|
+
const now = Date.now();
|
|
996
|
+
const windowMs = 60 * 1e3;
|
|
997
|
+
const entries = this.rateLimitCache.get(userId) || [];
|
|
998
|
+
const validEntries = entries.filter((entry) => now - entry.timestamp < windowMs);
|
|
999
|
+
const requestCount = validEntries.length;
|
|
1000
|
+
const isAllowed = requestCount < this.config.maxPermissionChecksPerMinute;
|
|
1001
|
+
if (isAllowed) {
|
|
1002
|
+
validEntries.push({ timestamp: now });
|
|
1003
|
+
}
|
|
1004
|
+
this.rateLimitCache.set(userId, validEntries);
|
|
1005
|
+
return isAllowed;
|
|
1006
|
+
}
|
|
1007
|
+
_getRequestCount(userId) {
|
|
1008
|
+
const now = Date.now();
|
|
1009
|
+
const windowMs = 60 * 1e3;
|
|
1010
|
+
const entries = this.rateLimitCache.get(userId) || [];
|
|
1011
|
+
const validEntries = entries.filter((entry) => now - entry.timestamp < windowMs);
|
|
1012
|
+
return validEntries.length;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Clear old rate limit entries to prevent memory leaks
|
|
1016
|
+
* Should be called periodically (e.g., every 5 minutes)
|
|
1017
|
+
*/
|
|
1018
|
+
clearExpiredEntries() {
|
|
1019
|
+
const now = Date.now();
|
|
1020
|
+
const windowMs = 60 * 1e3;
|
|
1021
|
+
for (const [userId, entries] of this.rateLimitCache.entries()) {
|
|
1022
|
+
const validEntries = entries.filter((entry) => now - entry.timestamp < windowMs);
|
|
1023
|
+
if (validEntries.length === 0) {
|
|
1024
|
+
this.rateLimitCache.delete(userId);
|
|
1025
|
+
} else {
|
|
1026
|
+
this.rateLimitCache.set(userId, validEntries);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Sanitize input data
|
|
1032
|
+
* @param input - Input to sanitize
|
|
1033
|
+
* @returns Sanitized input
|
|
1034
|
+
*/
|
|
1035
|
+
sanitizeInput(input) {
|
|
1036
|
+
return RBACSecurityValidator.sanitizeInput(input);
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/rbac/config.ts
|
|
1041
|
+
var logger = createLogger("RBAC");
|
|
1042
|
+
var RBACConfigManager = class {
|
|
1043
|
+
constructor() {
|
|
1044
|
+
this.config = null;
|
|
1045
|
+
this.logger = null;
|
|
1046
|
+
}
|
|
1047
|
+
setConfig(config) {
|
|
1048
|
+
this.config = config;
|
|
1049
|
+
this.setupLogger();
|
|
1050
|
+
}
|
|
1051
|
+
getConfig() {
|
|
1052
|
+
return this.config;
|
|
1053
|
+
}
|
|
1054
|
+
getLogger() {
|
|
1055
|
+
if (!this.logger) {
|
|
1056
|
+
this.logger = this.createDefaultLogger();
|
|
1057
|
+
}
|
|
1058
|
+
return this.logger;
|
|
1059
|
+
}
|
|
1060
|
+
setupLogger() {
|
|
1061
|
+
if (!this.config) return;
|
|
1062
|
+
const { debug = false, logLevel = "warn" } = this.config;
|
|
1063
|
+
this.logger = {
|
|
1064
|
+
error: (message, ...args) => {
|
|
1065
|
+
logger.error(message, ...args);
|
|
1066
|
+
},
|
|
1067
|
+
warn: (message, ...args) => {
|
|
1068
|
+
if (logLevel === "warn" || logLevel === "info" || logLevel === "debug") {
|
|
1069
|
+
logger.warn(message, ...args);
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
info: (message, ...args) => {
|
|
1073
|
+
if (logLevel === "info" || logLevel === "debug") {
|
|
1074
|
+
logger.info(message, ...args);
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
debug: (message, ...args) => {
|
|
1078
|
+
if (debug && logLevel === "debug") {
|
|
1079
|
+
logger.debug(message, ...args);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
createDefaultLogger() {
|
|
1085
|
+
return {
|
|
1086
|
+
error: (message, ...args) => logger.error(message, ...args),
|
|
1087
|
+
warn: (message, ...args) => logger.warn(message, ...args),
|
|
1088
|
+
info: (message, ...args) => logger.info(message, ...args),
|
|
1089
|
+
debug: (message, ...args) => logger.debug(message, ...args)
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
isDebugMode() {
|
|
1093
|
+
return this.config?.debug ?? false;
|
|
1094
|
+
}
|
|
1095
|
+
isDevelopmentMode() {
|
|
1096
|
+
return this.config?.developmentMode ?? false;
|
|
1097
|
+
}
|
|
1098
|
+
getMockPermissions() {
|
|
1099
|
+
return this.config?.mockPermissions ?? null;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
var configManager = new RBACConfigManager();
|
|
1103
|
+
function createRBACConfig(config) {
|
|
1104
|
+
configManager.setConfig(config);
|
|
1105
|
+
return config;
|
|
1106
|
+
}
|
|
1107
|
+
function getRBACConfig() {
|
|
1108
|
+
return configManager.getConfig();
|
|
1109
|
+
}
|
|
1110
|
+
function getRBACLogger() {
|
|
1111
|
+
return configManager.getLogger();
|
|
1112
|
+
}
|
|
1113
|
+
function isDebugMode() {
|
|
1114
|
+
return configManager.isDebugMode();
|
|
1115
|
+
}
|
|
1116
|
+
function isDevelopmentMode() {
|
|
1117
|
+
return configManager.isDevelopmentMode();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/rbac/engine.ts
|
|
1121
|
+
var RBACEngine = class {
|
|
1122
|
+
constructor(supabase, securityConfig) {
|
|
1123
|
+
this.supabase = supabase;
|
|
1124
|
+
const mergedSecurityConfig = {
|
|
1125
|
+
...DEFAULT_SECURITY_CONFIG,
|
|
1126
|
+
...securityConfig
|
|
1127
|
+
};
|
|
1128
|
+
this.securityMiddleware = new RBACSecurityMiddleware(mergedSecurityConfig);
|
|
1129
|
+
initializeCacheInvalidation(supabase);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Check if a user has a specific permission
|
|
1133
|
+
*
|
|
1134
|
+
* This method now delegates to the database RPC function for all the heavy lifting.
|
|
1135
|
+
*
|
|
1136
|
+
* @param input - Permission check input
|
|
1137
|
+
* @param securityContext - Security context for validation (required)
|
|
1138
|
+
* @returns Promise resolving to permission result
|
|
1139
|
+
*/
|
|
1140
|
+
async isPermitted(input, securityContext) {
|
|
1141
|
+
const startTime = Date.now();
|
|
1142
|
+
const { userId, permission, scope, pageId } = input;
|
|
1143
|
+
let cacheHit = false;
|
|
1144
|
+
let cacheSource = "rpc";
|
|
1145
|
+
try {
|
|
1146
|
+
const validation = await this.securityMiddleware.validateInput(input, securityContext);
|
|
1147
|
+
if (!validation.isValid) {
|
|
1148
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1149
|
+
type: "invalid_input",
|
|
1150
|
+
userId,
|
|
1151
|
+
details: { errors: validation.errors, input: JSON.stringify(input) }
|
|
1152
|
+
});
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
const rateLimit = await this.securityMiddleware.checkRateLimit(securityContext);
|
|
1156
|
+
if (!rateLimit.isAllowed) {
|
|
1157
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1158
|
+
type: "rate_limit_exceeded",
|
|
1159
|
+
userId,
|
|
1160
|
+
details: { remaining: rateLimit.remaining }
|
|
1161
|
+
});
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
if (!RBACSecurityValidator.validateUserId(userId)) {
|
|
1165
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1166
|
+
type: "invalid_input",
|
|
1167
|
+
userId,
|
|
1168
|
+
details: { error: "Invalid user ID format" }
|
|
1169
|
+
});
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
if (!RBACSecurityValidator.validatePermission(permission)) {
|
|
1173
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1174
|
+
type: "invalid_input",
|
|
1175
|
+
userId,
|
|
1176
|
+
details: { error: "Invalid permission format", permission }
|
|
1177
|
+
});
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
if (!RBACSecurityValidator.validateScope(scope)) {
|
|
1181
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1182
|
+
type: "invalid_input",
|
|
1183
|
+
userId,
|
|
1184
|
+
details: { error: "Invalid scope format", scope }
|
|
1185
|
+
});
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
const cacheKey = RBACCache.generateKey(
|
|
1189
|
+
userId,
|
|
1190
|
+
permission,
|
|
1191
|
+
scope.organisationId,
|
|
1192
|
+
scope.eventId,
|
|
1193
|
+
scope.appId,
|
|
1194
|
+
pageId
|
|
1195
|
+
);
|
|
1196
|
+
const cached = rbacCache.get(cacheKey);
|
|
1197
|
+
if (cached !== null) {
|
|
1198
|
+
cacheHit = true;
|
|
1199
|
+
cacheSource = "memory";
|
|
1200
|
+
return cached;
|
|
1201
|
+
}
|
|
1202
|
+
const { data, error } = await this.supabase.rpc("rbac_check_permission_simplified", {
|
|
1203
|
+
p_user_id: userId,
|
|
1204
|
+
p_permission: permission,
|
|
1205
|
+
p_organisation_id: scope.organisationId || void 0,
|
|
1206
|
+
p_event_id: scope.eventId || void 0,
|
|
1207
|
+
p_app_id: scope.appId || void 0,
|
|
1208
|
+
p_page_id: pageId || void 0
|
|
1209
|
+
});
|
|
1210
|
+
if (error) {
|
|
1211
|
+
const logger2 = getRBACLogger();
|
|
1212
|
+
logger2.error("RPC error:", error);
|
|
1213
|
+
const category = categorizeError(error);
|
|
1214
|
+
const eventType = mapErrorCategoryToSecurityEventType(category);
|
|
1215
|
+
const errorDetails = error;
|
|
1216
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1217
|
+
type: eventType,
|
|
1218
|
+
userId,
|
|
1219
|
+
details: {
|
|
1220
|
+
error: errorDetails?.message || "RPC call failed",
|
|
1221
|
+
code: errorDetails?.code,
|
|
1222
|
+
hint: errorDetails?.hint,
|
|
1223
|
+
details: errorDetails?.details,
|
|
1224
|
+
permission,
|
|
1225
|
+
scope: JSON.stringify(scope),
|
|
1226
|
+
category
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
const hasPermission = data === true;
|
|
1232
|
+
rbacCache.set(cacheKey, hasPermission, 6e4);
|
|
1233
|
+
const duration = Date.now() - startTime;
|
|
1234
|
+
if (scope.organisationId) {
|
|
1235
|
+
const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
|
|
1236
|
+
await emitAuditEvent({
|
|
1237
|
+
type: hasPermission ? "permission_check" : "permission_denied",
|
|
1238
|
+
userId,
|
|
1239
|
+
organisationId: scope.organisationId,
|
|
1240
|
+
eventId: scope.eventId,
|
|
1241
|
+
appId: scope.appId,
|
|
1242
|
+
pageId: resolvedPageId,
|
|
1243
|
+
permission,
|
|
1244
|
+
decision: hasPermission,
|
|
1245
|
+
source: "api",
|
|
1246
|
+
duration_ms: duration,
|
|
1247
|
+
cache_hit: cacheHit,
|
|
1248
|
+
cache_source: cacheSource
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
return hasPermission;
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
const category = categorizeError(error);
|
|
1254
|
+
const eventType = mapErrorCategoryToSecurityEventType(category);
|
|
1255
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1256
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
1257
|
+
type: eventType,
|
|
1258
|
+
userId,
|
|
1259
|
+
details: {
|
|
1260
|
+
error: errorMessage,
|
|
1261
|
+
permission,
|
|
1262
|
+
scope: JSON.stringify(scope),
|
|
1263
|
+
category
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
const logger2 = getRBACLogger();
|
|
1267
|
+
logger2.error("Permission check failed:", error);
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Get user's access level in a scope
|
|
1273
|
+
*
|
|
1274
|
+
* This is derived from roles, not permissions.
|
|
1275
|
+
*
|
|
1276
|
+
* @param input - Access level input
|
|
1277
|
+
* @returns Promise resolving to access level
|
|
1278
|
+
*/
|
|
1279
|
+
async getAccessLevel(input) {
|
|
1280
|
+
const { userId, scope } = input;
|
|
1281
|
+
const cacheKey = RBACCache.generateAccessLevelKey(
|
|
1282
|
+
userId,
|
|
1283
|
+
scope.organisationId || "",
|
|
1284
|
+
scope.eventId,
|
|
1285
|
+
scope.appId
|
|
1286
|
+
);
|
|
1287
|
+
const cached = rbacCache.get(cacheKey);
|
|
1288
|
+
if (cached) {
|
|
1289
|
+
return cached;
|
|
1290
|
+
}
|
|
1291
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1292
|
+
const isSuperAdmin2 = await this.checkSuperAdmin(userId);
|
|
1293
|
+
if (isSuperAdmin2) {
|
|
1294
|
+
rbacCache.set(cacheKey, "super", 6e4);
|
|
1295
|
+
return "super";
|
|
1296
|
+
}
|
|
1297
|
+
if (scope.organisationId) {
|
|
1298
|
+
const { data: orgRoles } = await this.supabase.from("rbac_organisation_roles").select("role").eq("user_id", userId).eq("organisation_id", scope.organisationId).eq("status", "active").is("revoked_at", null).lte("valid_from", now).or(`valid_to.is.null,valid_to.gte.${now}`).limit(1);
|
|
1299
|
+
const orgRole = orgRoles?.[0];
|
|
1300
|
+
if (orgRole?.role === "org_admin") {
|
|
1301
|
+
rbacCache.set(cacheKey, "admin", 6e4);
|
|
1302
|
+
return "admin";
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (scope.eventId && scope.appId) {
|
|
1306
|
+
const { data: eventRole } = await this.supabase.from("rbac_event_app_roles").select("role").eq("user_id", userId).eq("event_id", scope.eventId).eq("app_id", scope.appId).eq("status", "active").lte("valid_from", now).or(`valid_to.is.null,valid_to.gte.${now}`).single();
|
|
1307
|
+
if (eventRole?.role === "event_admin") {
|
|
1308
|
+
rbacCache.set(cacheKey, "admin", 6e4);
|
|
1309
|
+
return "admin";
|
|
1310
|
+
}
|
|
1311
|
+
if (eventRole?.role === "planner") {
|
|
1312
|
+
rbacCache.set(cacheKey, "planner", 6e4);
|
|
1313
|
+
return "planner";
|
|
1314
|
+
}
|
|
1315
|
+
if (eventRole?.role === "participant") {
|
|
1316
|
+
rbacCache.set(cacheKey, "participant", 6e4);
|
|
1317
|
+
return "participant";
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
rbacCache.set(cacheKey, "viewer", 6e4);
|
|
1321
|
+
return "viewer";
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Get user's permission map for a scope
|
|
1325
|
+
*
|
|
1326
|
+
* This builds a map of page IDs to allowed operations.
|
|
1327
|
+
* Uses the simplified RPC for each permission check.
|
|
1328
|
+
*
|
|
1329
|
+
* @param input - Permission map input
|
|
1330
|
+
* @returns Promise resolving to permission map
|
|
1331
|
+
*/
|
|
1332
|
+
async getPermissionMap(input) {
|
|
1333
|
+
const { userId, scope } = input;
|
|
1334
|
+
const cacheKey = RBACCache.generatePermissionMapKey(
|
|
1335
|
+
userId,
|
|
1336
|
+
scope.organisationId || "",
|
|
1337
|
+
scope.eventId,
|
|
1338
|
+
scope.appId
|
|
1339
|
+
);
|
|
1340
|
+
const isSuperAdmin2 = await this.checkSuperAdmin(userId);
|
|
1341
|
+
if (isSuperAdmin2) {
|
|
1342
|
+
const wildcardMap = { "*": true };
|
|
1343
|
+
rbacCache.set(cacheKey, wildcardMap, 6e4);
|
|
1344
|
+
return wildcardMap;
|
|
1345
|
+
}
|
|
1346
|
+
if (!scope.organisationId) {
|
|
1347
|
+
return {};
|
|
1348
|
+
}
|
|
1349
|
+
const cached = rbacCache.get(cacheKey);
|
|
1350
|
+
if (cached) {
|
|
1351
|
+
return cached;
|
|
1352
|
+
}
|
|
1353
|
+
const permissionMap = {};
|
|
1354
|
+
if (scope.appId) {
|
|
1355
|
+
const { data: pages } = await this.supabase.from("rbac_app_pages").select("id, page_name").eq("app_id", scope.appId);
|
|
1356
|
+
if (pages) {
|
|
1357
|
+
if (!scope.organisationId) {
|
|
1358
|
+
rbacCache.set(cacheKey, permissionMap, 6e4);
|
|
1359
|
+
return permissionMap;
|
|
1360
|
+
}
|
|
1361
|
+
const securityContext = {
|
|
1362
|
+
userId,
|
|
1363
|
+
organisationId: scope.organisationId,
|
|
1364
|
+
// Required
|
|
1365
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1366
|
+
};
|
|
1367
|
+
for (const page of pages) {
|
|
1368
|
+
for (const operation of ["read", "create", "update", "delete"]) {
|
|
1369
|
+
const permissionString = `${operation}:page.${page.page_name}`;
|
|
1370
|
+
const hasPermission = await this.isPermitted(
|
|
1371
|
+
{
|
|
1372
|
+
userId,
|
|
1373
|
+
scope,
|
|
1374
|
+
permission: permissionString,
|
|
1375
|
+
pageId: page.id
|
|
1376
|
+
},
|
|
1377
|
+
securityContext
|
|
1378
|
+
);
|
|
1379
|
+
const permissionKey = permissionString;
|
|
1380
|
+
permissionMap[permissionKey] = hasPermission;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
rbacCache.set(cacheKey, permissionMap, 6e4);
|
|
1386
|
+
return permissionMap;
|
|
1387
|
+
}
|
|
1388
|
+
async resolveAppContext(input) {
|
|
1389
|
+
try {
|
|
1390
|
+
const { userId, appName } = input;
|
|
1391
|
+
const { data, error } = await this.supabase.rpc("data_app_resolve", {
|
|
1392
|
+
p_user_id: userId,
|
|
1393
|
+
p_app_name: appName
|
|
1394
|
+
});
|
|
1395
|
+
if (error) {
|
|
1396
|
+
const logger2 = getRBACLogger();
|
|
1397
|
+
logger2.error("Failed to resolve app context:", error);
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
const appData = data[0];
|
|
1404
|
+
if (!appData?.app_id) {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
appId: appData.app_id,
|
|
1409
|
+
hasAccess: appData.has_access !== false
|
|
1410
|
+
};
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
const logger2 = getRBACLogger();
|
|
1413
|
+
logger2.error("Unexpected error resolving app context:", error);
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
async getRoleContext(input) {
|
|
1418
|
+
const result = {
|
|
1419
|
+
globalRole: null,
|
|
1420
|
+
organisationRole: null,
|
|
1421
|
+
eventAppRole: null
|
|
1422
|
+
};
|
|
1423
|
+
try {
|
|
1424
|
+
const { userId, scope } = input;
|
|
1425
|
+
const { data, error } = await this.supabase.rpc("rbac_permissions_get", {
|
|
1426
|
+
p_user_id: userId,
|
|
1427
|
+
p_organisation_id: scope.organisationId || null,
|
|
1428
|
+
p_event_id: scope.eventId || null,
|
|
1429
|
+
p_app_id: scope.appId || null,
|
|
1430
|
+
p_page_id: null
|
|
1431
|
+
// Optional: can filter to specific page if needed
|
|
1432
|
+
});
|
|
1433
|
+
if (error) {
|
|
1434
|
+
const logger2 = getRBACLogger();
|
|
1435
|
+
logger2.error("Failed to load role context:", error);
|
|
1436
|
+
return result;
|
|
1437
|
+
}
|
|
1438
|
+
if (!Array.isArray(data)) {
|
|
1439
|
+
return result;
|
|
1440
|
+
}
|
|
1441
|
+
const mapToOrganisationRole = (level) => {
|
|
1442
|
+
switch (level) {
|
|
1443
|
+
case "supporter":
|
|
1444
|
+
return "supporter";
|
|
1445
|
+
case "member":
|
|
1446
|
+
return "member";
|
|
1447
|
+
case "leader":
|
|
1448
|
+
return "leader";
|
|
1449
|
+
case "org_admin":
|
|
1450
|
+
case "admin":
|
|
1451
|
+
case "super":
|
|
1452
|
+
return "org_admin";
|
|
1453
|
+
default:
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
const mapToEventAppRole = (level) => {
|
|
1458
|
+
switch (level) {
|
|
1459
|
+
case "viewer":
|
|
1460
|
+
return "viewer";
|
|
1461
|
+
case "participant":
|
|
1462
|
+
return "participant";
|
|
1463
|
+
case "planner":
|
|
1464
|
+
return "planner";
|
|
1465
|
+
case "admin":
|
|
1466
|
+
case "super":
|
|
1467
|
+
return "event_admin";
|
|
1468
|
+
default:
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
for (const permission of data) {
|
|
1473
|
+
if (permission.permission_type === "all_permissions") {
|
|
1474
|
+
result.globalRole = "super_admin";
|
|
1475
|
+
}
|
|
1476
|
+
if (permission.permission_type === "organisation_access") {
|
|
1477
|
+
result.organisationRole = mapToOrganisationRole(permission.role_name);
|
|
1478
|
+
}
|
|
1479
|
+
if (permission.permission_type === "event_app_access") {
|
|
1480
|
+
result.eventAppRole = mapToEventAppRole(permission.role_name);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return result;
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
const logger2 = getRBACLogger();
|
|
1486
|
+
logger2.error("Unexpected error loading role context:", error);
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Check if user is super admin
|
|
1492
|
+
*
|
|
1493
|
+
* @param userId - User ID
|
|
1494
|
+
* @returns Promise resolving to super admin status
|
|
1495
|
+
*/
|
|
1496
|
+
async checkSuperAdmin(userId) {
|
|
1497
|
+
const cacheKey = `super_admin:${userId}`;
|
|
1498
|
+
const cached = rbacCache.get(cacheKey);
|
|
1499
|
+
if (cached !== null) {
|
|
1500
|
+
return cached;
|
|
1501
|
+
}
|
|
1502
|
+
const startTime = Date.now();
|
|
1503
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1504
|
+
try {
|
|
1505
|
+
const { data, error } = await this.supabase.from("rbac_global_roles").select("role").eq("user_id", userId).eq("role", "super_admin").lte("valid_from", now).or(`valid_to.is.null,valid_to.gte.${now}`).limit(1);
|
|
1506
|
+
const elapsed = Date.now() - startTime;
|
|
1507
|
+
if (elapsed > 2e3) {
|
|
1508
|
+
const logger2 = getRBACLogger();
|
|
1509
|
+
const errorMessage = error && typeof error === "object" && "message" in error ? String(error.message) : void 0;
|
|
1510
|
+
logger2.warn("[RBACEngine] Super admin check took longer than expected", {
|
|
1511
|
+
userId,
|
|
1512
|
+
elapsedMs: elapsed,
|
|
1513
|
+
error: errorMessage
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
const isSuperAdmin2 = !error && data && data.length > 0;
|
|
1517
|
+
rbacCache.set(cacheKey, isSuperAdmin2, 6e4);
|
|
1518
|
+
return Boolean(isSuperAdmin2);
|
|
1519
|
+
} catch (err2) {
|
|
1520
|
+
const elapsed = Date.now() - startTime;
|
|
1521
|
+
const logger2 = getRBACLogger();
|
|
1522
|
+
logger2.error("[RBACEngine] Error checking super admin", {
|
|
1523
|
+
userId,
|
|
1524
|
+
error: err2,
|
|
1525
|
+
elapsedMs: elapsed
|
|
1526
|
+
});
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Resolve a page ID to UUID if it's a page name
|
|
1532
|
+
*
|
|
1533
|
+
* @param pageId - Page ID (UUID) or page name (string)
|
|
1534
|
+
* @param appId - App ID to look up the page
|
|
1535
|
+
* @returns Resolved page ID (UUID) or original pageId
|
|
1536
|
+
*/
|
|
1537
|
+
async resolvePageId(pageId, appId) {
|
|
1538
|
+
if (!pageId) {
|
|
1539
|
+
return void 0;
|
|
1540
|
+
}
|
|
1541
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1542
|
+
if (uuidRegex.test(pageId)) {
|
|
1543
|
+
return pageId;
|
|
1544
|
+
}
|
|
1545
|
+
if (!appId) {
|
|
1546
|
+
return pageId;
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
const { data: page, error: pageError } = await this.supabase.from("rbac_app_pages").select("id").eq("app_id", appId).eq("page_name", pageId).maybeSingle();
|
|
1550
|
+
if (pageError) {
|
|
1551
|
+
const logger2 = getRBACLogger();
|
|
1552
|
+
if (pageError.code !== "PGRST116") {
|
|
1553
|
+
logger2.warn("Failed to resolve page name to UUID:", { pageId, appId, error: pageError });
|
|
1554
|
+
}
|
|
1555
|
+
return pageId;
|
|
1556
|
+
}
|
|
1557
|
+
return page?.id || pageId;
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
const logger2 = getRBACLogger();
|
|
1560
|
+
logger2.warn("Failed to resolve page name to UUID:", { pageId, appId, error });
|
|
1561
|
+
return pageId;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
function createRBACEngine(supabase, securityConfig) {
|
|
1566
|
+
return new RBACEngine(supabase, securityConfig);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/rbac/performance.ts
|
|
1570
|
+
var RBACPerformanceMonitor = class {
|
|
1571
|
+
constructor() {
|
|
1572
|
+
this.metrics = {
|
|
1573
|
+
totalChecks: 0,
|
|
1574
|
+
cacheHits: 0,
|
|
1575
|
+
cacheMisses: 0,
|
|
1576
|
+
cacheHitRate: 0,
|
|
1577
|
+
deduplicatedRequests: 0,
|
|
1578
|
+
networkRequests: 0,
|
|
1579
|
+
averageResponseTime: 0,
|
|
1580
|
+
totalResponseTime: 0,
|
|
1581
|
+
batchedAuditEvents: 0,
|
|
1582
|
+
individualAuditEvents: 0
|
|
1583
|
+
};
|
|
1584
|
+
this.enabled = false;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Enable or disable performance monitoring
|
|
1588
|
+
*/
|
|
1589
|
+
setEnabled(enabled) {
|
|
1590
|
+
this.enabled = enabled;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Check if performance monitoring is enabled
|
|
1594
|
+
*/
|
|
1595
|
+
isEnabled() {
|
|
1596
|
+
return this.enabled;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Record a permission check
|
|
1600
|
+
*/
|
|
1601
|
+
recordCheck(cacheHit, responseTime, wasDeduplicated = false) {
|
|
1602
|
+
if (!this.enabled) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
this.metrics.totalChecks++;
|
|
1606
|
+
if (cacheHit) {
|
|
1607
|
+
this.metrics.cacheHits++;
|
|
1608
|
+
} else {
|
|
1609
|
+
this.metrics.cacheMisses++;
|
|
1610
|
+
this.metrics.networkRequests++;
|
|
1611
|
+
}
|
|
1612
|
+
if (wasDeduplicated) {
|
|
1613
|
+
this.metrics.deduplicatedRequests++;
|
|
1614
|
+
}
|
|
1615
|
+
this.metrics.totalResponseTime += responseTime;
|
|
1616
|
+
this.metrics.averageResponseTime = this.metrics.totalResponseTime / this.metrics.totalChecks;
|
|
1617
|
+
this.metrics.cacheHitRate = this.metrics.cacheHits / this.metrics.totalChecks;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Record an audit event
|
|
1621
|
+
*/
|
|
1622
|
+
recordAuditEvent(batched) {
|
|
1623
|
+
if (!this.enabled) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (batched) {
|
|
1627
|
+
this.metrics.batchedAuditEvents++;
|
|
1628
|
+
} else {
|
|
1629
|
+
this.metrics.individualAuditEvents++;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Get current metrics
|
|
1634
|
+
*/
|
|
1635
|
+
getMetrics() {
|
|
1636
|
+
return { ...this.metrics };
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Reset all metrics
|
|
1640
|
+
*/
|
|
1641
|
+
reset() {
|
|
1642
|
+
this.metrics = {
|
|
1643
|
+
totalChecks: 0,
|
|
1644
|
+
cacheHits: 0,
|
|
1645
|
+
cacheMisses: 0,
|
|
1646
|
+
cacheHitRate: 0,
|
|
1647
|
+
deduplicatedRequests: 0,
|
|
1648
|
+
networkRequests: 0,
|
|
1649
|
+
averageResponseTime: 0,
|
|
1650
|
+
totalResponseTime: 0,
|
|
1651
|
+
batchedAuditEvents: 0,
|
|
1652
|
+
individualAuditEvents: 0
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Get metrics summary as a formatted string
|
|
1657
|
+
*/
|
|
1658
|
+
getSummary() {
|
|
1659
|
+
const m = this.metrics;
|
|
1660
|
+
return `
|
|
1661
|
+
RBAC Performance Metrics:
|
|
1662
|
+
Total Checks: ${m.totalChecks}
|
|
1663
|
+
Cache Hits: ${m.cacheHits} (${(m.cacheHitRate * 100).toFixed(1)}%)
|
|
1664
|
+
Cache Misses: ${m.cacheMisses}
|
|
1665
|
+
Deduplicated Requests: ${m.deduplicatedRequests}
|
|
1666
|
+
Network Requests: ${m.networkRequests}
|
|
1667
|
+
Average Response Time: ${m.averageResponseTime.toFixed(2)}ms
|
|
1668
|
+
Batched Audit Events: ${m.batchedAuditEvents}
|
|
1669
|
+
Individual Audit Events: ${m.individualAuditEvents}
|
|
1670
|
+
`;
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
var performanceMonitor = new RBACPerformanceMonitor();
|
|
1674
|
+
function enablePerformanceMonitoring() {
|
|
1675
|
+
performanceMonitor.setEnabled(true);
|
|
1676
|
+
}
|
|
1677
|
+
function disablePerformanceMonitoring() {
|
|
1678
|
+
performanceMonitor.setEnabled(false);
|
|
1679
|
+
}
|
|
1680
|
+
function isPerformanceMonitoringEnabled() {
|
|
1681
|
+
return performanceMonitor.isEnabled();
|
|
1682
|
+
}
|
|
1683
|
+
function recordPermissionCheck(cacheHit, responseTime, wasDeduplicated = false) {
|
|
1684
|
+
performanceMonitor.recordCheck(cacheHit, responseTime, wasDeduplicated);
|
|
1685
|
+
}
|
|
1686
|
+
function recordAuditEvent(batched) {
|
|
1687
|
+
performanceMonitor.recordAuditEvent(batched);
|
|
1688
|
+
}
|
|
1689
|
+
function getPerformanceMetrics() {
|
|
1690
|
+
return performanceMonitor.getMetrics();
|
|
1691
|
+
}
|
|
1692
|
+
function resetPerformanceMetrics() {
|
|
1693
|
+
performanceMonitor.reset();
|
|
1694
|
+
}
|
|
1695
|
+
function getPerformanceSummary() {
|
|
1696
|
+
return performanceMonitor.getSummary();
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/rbac/request-deduplication.ts
|
|
1700
|
+
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
1701
|
+
function generateDeduplicationKey(input) {
|
|
1702
|
+
return RBACCache.generatePermissionKey({
|
|
1703
|
+
userId: input.userId,
|
|
1704
|
+
organisationId: input.scope.organisationId,
|
|
1705
|
+
// Can be undefined for page-level permissions
|
|
1706
|
+
eventId: input.scope.eventId,
|
|
1707
|
+
appId: input.scope.appId,
|
|
1708
|
+
permission: input.permission,
|
|
1709
|
+
pageId: input.pageId
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
async function getOrCreateRequest(input, checkFn) {
|
|
1713
|
+
const key = generateDeduplicationKey(input);
|
|
1714
|
+
const existingRequest = inFlightRequests.get(key);
|
|
1715
|
+
if (existingRequest) {
|
|
1716
|
+
return existingRequest;
|
|
1717
|
+
}
|
|
1718
|
+
const requestPromise = checkFn(input).finally(() => {
|
|
1719
|
+
inFlightRequests.delete(key);
|
|
1720
|
+
});
|
|
1721
|
+
inFlightRequests.set(key, requestPromise);
|
|
1722
|
+
return requestPromise;
|
|
1723
|
+
}
|
|
1724
|
+
function clearInFlightRequests() {
|
|
1725
|
+
inFlightRequests.clear();
|
|
1726
|
+
}
|
|
1727
|
+
function getInFlightRequestCount() {
|
|
1728
|
+
return inFlightRequests.size;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/rbac/utils/eventContext.ts
|
|
1732
|
+
var orgDerivationCache = /* @__PURE__ */ new Map();
|
|
1733
|
+
var MAX_CACHE_SIZE = 100;
|
|
1734
|
+
async function getOrganisationFromEvent(supabase, eventId) {
|
|
1735
|
+
if (orgDerivationCache.has(eventId)) {
|
|
1736
|
+
return ok(orgDerivationCache.get(eventId) ?? null);
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
const { data, error } = await supabase.from("core_events").select("organisation_id").eq("event_id", eventId).single();
|
|
1740
|
+
let organisationId = null;
|
|
1741
|
+
if (error) {
|
|
1742
|
+
return err({ code: "ORG_FROM_EVENT_FAILED", message: error.message ?? "Failed to get organisation from event" });
|
|
1743
|
+
}
|
|
1744
|
+
if (data?.organisation_id) {
|
|
1745
|
+
organisationId = data.organisation_id;
|
|
1746
|
+
}
|
|
1747
|
+
if (orgDerivationCache.size >= MAX_CACHE_SIZE) {
|
|
1748
|
+
const firstKey = orgDerivationCache.keys().next().value;
|
|
1749
|
+
if (firstKey) {
|
|
1750
|
+
orgDerivationCache.delete(firstKey);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
orgDerivationCache.set(eventId, organisationId);
|
|
1754
|
+
return ok(organisationId);
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
return err({ code: "ORG_FROM_EVENT_FAILED", message: e instanceof Error ? e.message : String(e) });
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/rbac/utils/contextValidator.ts
|
|
1761
|
+
var log3 = createLogger("ContextValidator");
|
|
1762
|
+
function allowsOptionalContexts(appName) {
|
|
1763
|
+
return appName === "PORTAL" || appName === "ADMIN";
|
|
1764
|
+
}
|
|
1765
|
+
var ContextValidator = class {
|
|
1766
|
+
/**
|
|
1767
|
+
* Derive organisation ID from event ID
|
|
1768
|
+
*
|
|
1769
|
+
* @param supabase - Supabase client
|
|
1770
|
+
* @param eventId - Event ID
|
|
1771
|
+
* @returns Organisation ID or null
|
|
1772
|
+
*/
|
|
1773
|
+
static async deriveOrgFromEvent(supabase, eventId) {
|
|
1774
|
+
const result = await getOrganisationFromEvent(supabase, eventId);
|
|
1775
|
+
if (!result.ok) {
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
return result.data;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Resolve scope based on page-level scope_type
|
|
1782
|
+
*
|
|
1783
|
+
* This method handles page-level scoping. All pages have explicit scope_type set.
|
|
1784
|
+
* Used for hybrid apps that have both event and organisation pages.
|
|
1785
|
+
*
|
|
1786
|
+
* @param scope - Current scope
|
|
1787
|
+
* @param pageScopeType - Page scope type ('event', 'organisation', or 'both')
|
|
1788
|
+
* @param appName - App name (for PORTAL/ADMIN special case)
|
|
1789
|
+
* @param supabase - Supabase client (for deriving org from event, only if not already provided)
|
|
1790
|
+
* @param immediateOrganisationId - Optional immediate organisation ID (from selectedEvent.organisation_id) - avoids querying
|
|
1791
|
+
* @returns Resolved scope with all required context
|
|
1792
|
+
*/
|
|
1793
|
+
static async resolveScopeForPage(scope, pageScopeType, appName, supabase, immediateOrganisationId) {
|
|
1794
|
+
const effectiveScopeType = pageScopeType;
|
|
1795
|
+
if (effectiveScopeType === "both") {
|
|
1796
|
+
if (!scope.organisationId && !scope.eventId) {
|
|
1797
|
+
if (allowsOptionalContexts(appName)) {
|
|
1798
|
+
return {
|
|
1799
|
+
isValid: true,
|
|
1800
|
+
resolvedScope: {
|
|
1801
|
+
organisationId: void 0,
|
|
1802
|
+
eventId: void 0,
|
|
1803
|
+
appId: scope.appId
|
|
1804
|
+
},
|
|
1805
|
+
error: null
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
return {
|
|
1809
|
+
isValid: false,
|
|
1810
|
+
resolvedScope: null,
|
|
1811
|
+
error: new Error("Page requires either organisation or event context")
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
let organisationId = scope.organisationId || immediateOrganisationId || void 0;
|
|
1815
|
+
if (!organisationId && scope.eventId && supabase) {
|
|
1816
|
+
try {
|
|
1817
|
+
const derivedOrgId = await this.deriveOrgFromEvent(supabase, scope.eventId);
|
|
1818
|
+
organisationId = derivedOrgId || void 0;
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
log3.warn("Failed to derive org from event for both-scope page:", error);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return {
|
|
1824
|
+
isValid: true,
|
|
1825
|
+
resolvedScope: {
|
|
1826
|
+
organisationId,
|
|
1827
|
+
eventId: scope.eventId,
|
|
1828
|
+
appId: scope.appId
|
|
1829
|
+
},
|
|
1830
|
+
error: null
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
if (effectiveScopeType === "event") {
|
|
1834
|
+
if (!scope.eventId) {
|
|
1835
|
+
if (allowsOptionalContexts(appName)) {
|
|
1836
|
+
return {
|
|
1837
|
+
isValid: true,
|
|
1838
|
+
resolvedScope: {
|
|
1839
|
+
organisationId: scope.organisationId,
|
|
1840
|
+
eventId: void 0,
|
|
1841
|
+
appId: scope.appId
|
|
1842
|
+
},
|
|
1843
|
+
error: null
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
isValid: false,
|
|
1848
|
+
resolvedScope: null,
|
|
1849
|
+
error: new EventContextRequiredError()
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
let organisationId = scope.organisationId || immediateOrganisationId || void 0;
|
|
1853
|
+
if (!organisationId && supabase && scope.eventId) {
|
|
1854
|
+
try {
|
|
1855
|
+
const derivedOrgId = await this.deriveOrgFromEvent(supabase, scope.eventId);
|
|
1856
|
+
organisationId = derivedOrgId || void 0;
|
|
1857
|
+
if (!organisationId) {
|
|
1858
|
+
return {
|
|
1859
|
+
isValid: false,
|
|
1860
|
+
resolvedScope: null,
|
|
1861
|
+
error: new Error("Could not resolve organisation from event context")
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
log3.error("Failed to derive org from event:", error);
|
|
1866
|
+
return {
|
|
1867
|
+
isValid: false,
|
|
1868
|
+
resolvedScope: null,
|
|
1869
|
+
error: error instanceof Error ? error : new Error("Failed to derive organisation from event")
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
isValid: true,
|
|
1875
|
+
resolvedScope: {
|
|
1876
|
+
organisationId,
|
|
1877
|
+
eventId: scope.eventId,
|
|
1878
|
+
appId: scope.appId
|
|
1879
|
+
},
|
|
1880
|
+
error: null
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
if (effectiveScopeType === "organisation") {
|
|
1884
|
+
if (!scope.organisationId) {
|
|
1885
|
+
if (allowsOptionalContexts(appName)) {
|
|
1886
|
+
return {
|
|
1887
|
+
isValid: true,
|
|
1888
|
+
resolvedScope: {
|
|
1889
|
+
organisationId: void 0,
|
|
1890
|
+
eventId: scope.eventId,
|
|
1891
|
+
appId: scope.appId
|
|
1892
|
+
},
|
|
1893
|
+
error: null
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
return {
|
|
1897
|
+
isValid: false,
|
|
1898
|
+
resolvedScope: null,
|
|
1899
|
+
error: new OrganisationContextRequiredError()
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
return {
|
|
1903
|
+
isValid: true,
|
|
1904
|
+
resolvedScope: {
|
|
1905
|
+
organisationId: scope.organisationId,
|
|
1906
|
+
eventId: scope.eventId,
|
|
1907
|
+
// Event is optional for org-scoped pages
|
|
1908
|
+
appId: scope.appId
|
|
1909
|
+
},
|
|
1910
|
+
error: null
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
return {
|
|
1914
|
+
isValid: false,
|
|
1915
|
+
resolvedScope: null,
|
|
1916
|
+
error: new Error("Invalid scope type")
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// src/rbac/api.ts
|
|
1922
|
+
var log4 = createLogger("RBACAPI");
|
|
1923
|
+
function toApiError(error) {
|
|
1924
|
+
if (error instanceof RBACNotInitializedError) {
|
|
1925
|
+
return { code: "RBAC_NOT_INITIALIZED", message: error.message };
|
|
1926
|
+
}
|
|
1927
|
+
if (error instanceof OrganisationContextRequiredError) {
|
|
1928
|
+
return { code: "ORGANISATION_CONTEXT_REQUIRED", message: error.message };
|
|
1929
|
+
}
|
|
1930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1931
|
+
return { code: "RBAC_ERROR", message };
|
|
1932
|
+
}
|
|
1933
|
+
var globalEngine = null;
|
|
1934
|
+
function setupRBAC(supabase, config) {
|
|
1935
|
+
const isDevelopment = import.meta.env.MODE === "development";
|
|
1936
|
+
const fullConfig = {
|
|
1937
|
+
supabase,
|
|
1938
|
+
debug: isDevelopment,
|
|
1939
|
+
logLevel: "warn",
|
|
1940
|
+
developmentMode: isDevelopment,
|
|
1941
|
+
...config
|
|
1942
|
+
};
|
|
1943
|
+
createRBACConfig(fullConfig);
|
|
1944
|
+
const securityConfig = config === void 0 && !isDevelopment ? void 0 : {
|
|
1945
|
+
// Default: disable rate limiting in development
|
|
1946
|
+
...isDevelopment && config?.security?.enableRateLimiting === void 0 ? { enableRateLimiting: false } : {},
|
|
1947
|
+
// Explicit config overrides defaults
|
|
1948
|
+
...config?.security
|
|
1949
|
+
};
|
|
1950
|
+
globalEngine = createRBACEngine(supabase, securityConfig);
|
|
1951
|
+
const useBatchedAudit = config?.audit?.batched !== false && config?.performance?.enableBatchedAuditLogging !== false;
|
|
1952
|
+
const batchConfig = useBatchedAudit ? {
|
|
1953
|
+
batchWindow: config?.audit?.batchWindow,
|
|
1954
|
+
batchSize: config?.audit?.batchSize
|
|
1955
|
+
} : void 0;
|
|
1956
|
+
const auditManager = createAuditManager(supabase, useBatchedAudit, batchConfig);
|
|
1957
|
+
setGlobalAuditManager(auditManager);
|
|
1958
|
+
if (config?.performance?.enablePerformanceTracking) {
|
|
1959
|
+
enablePerformanceMonitoring();
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function isRBACInitialized() {
|
|
1963
|
+
return globalEngine !== null;
|
|
1964
|
+
}
|
|
1965
|
+
function getEngine() {
|
|
1966
|
+
if (!globalEngine) {
|
|
1967
|
+
throw new RBACNotInitializedError();
|
|
1968
|
+
}
|
|
1969
|
+
return globalEngine;
|
|
1970
|
+
}
|
|
1971
|
+
async function getAccessLevel(input, appName) {
|
|
1972
|
+
try {
|
|
1973
|
+
const engine = getEngine();
|
|
1974
|
+
const isSuperAdminUser = await engine["checkSuperAdmin"](input.userId);
|
|
1975
|
+
if (isSuperAdminUser) {
|
|
1976
|
+
return ok("super");
|
|
1977
|
+
}
|
|
1978
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
1979
|
+
input.scope,
|
|
1980
|
+
"organisation",
|
|
1981
|
+
// Default to organisation scope when no page context
|
|
1982
|
+
appName,
|
|
1983
|
+
engine["supabase"]
|
|
1984
|
+
);
|
|
1985
|
+
if (!validation.isValid || !validation.resolvedScope) {
|
|
1986
|
+
throw validation.error || new OrganisationContextRequiredError();
|
|
1987
|
+
}
|
|
1988
|
+
const accessLevel = await engine.getAccessLevel({
|
|
1989
|
+
...input,
|
|
1990
|
+
scope: validation.resolvedScope
|
|
1991
|
+
});
|
|
1992
|
+
return ok(accessLevel);
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
return err(toApiError(error));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
async function getPermissionMap(input, appName) {
|
|
1998
|
+
try {
|
|
1999
|
+
const engine = getEngine();
|
|
2000
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
2001
|
+
input.scope,
|
|
2002
|
+
"organisation",
|
|
2003
|
+
// Default to organisation scope when no page context
|
|
2004
|
+
appName,
|
|
2005
|
+
engine["supabase"]
|
|
2006
|
+
);
|
|
2007
|
+
if (!validation.isValid || !validation.resolvedScope) {
|
|
2008
|
+
throw validation.error || new OrganisationContextRequiredError();
|
|
2009
|
+
}
|
|
2010
|
+
const permissionMap = await engine.getPermissionMap({
|
|
2011
|
+
...input,
|
|
2012
|
+
scope: validation.resolvedScope
|
|
2013
|
+
});
|
|
2014
|
+
return ok(permissionMap);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
return err(toApiError(error));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async function resolveAppContext(input) {
|
|
2020
|
+
try {
|
|
2021
|
+
const engine = getEngine();
|
|
2022
|
+
const context = await engine.resolveAppContext(input);
|
|
2023
|
+
return ok(context);
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
return err(toApiError(error));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
async function getRoleContext(input, appName) {
|
|
2029
|
+
try {
|
|
2030
|
+
const engine = getEngine();
|
|
2031
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
2032
|
+
input.scope,
|
|
2033
|
+
"organisation",
|
|
2034
|
+
// Default to organisation scope when no page context
|
|
2035
|
+
appName,
|
|
2036
|
+
engine["supabase"]
|
|
2037
|
+
);
|
|
2038
|
+
if (!validation.isValid || !validation.resolvedScope) {
|
|
2039
|
+
throw validation.error || new OrganisationContextRequiredError();
|
|
2040
|
+
}
|
|
2041
|
+
const roleContext = await engine.getRoleContext({
|
|
2042
|
+
...input,
|
|
2043
|
+
scope: validation.resolvedScope
|
|
2044
|
+
});
|
|
2045
|
+
return ok(roleContext);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
return err(toApiError(error));
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async function isPermitted(input, appName, precomputedSuperAdmin = null) {
|
|
2051
|
+
try {
|
|
2052
|
+
const engine = getEngine();
|
|
2053
|
+
if (precomputedSuperAdmin === true) {
|
|
2054
|
+
return ok(true);
|
|
2055
|
+
}
|
|
2056
|
+
if (precomputedSuperAdmin === null) {
|
|
2057
|
+
const isSuperAdminUser = await engine["checkSuperAdmin"](input.userId);
|
|
2058
|
+
if (isSuperAdminUser) {
|
|
2059
|
+
return ok(true);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
let resolvedAppName = appName;
|
|
2063
|
+
if (!resolvedAppName && input.scope.appId) {
|
|
2064
|
+
try {
|
|
2065
|
+
const { data } = await engine["supabase"].from("rbac_apps").select("name").eq("id", input.scope.appId).eq("is_active", true).single();
|
|
2066
|
+
if (data) {
|
|
2067
|
+
resolvedAppName = data.name;
|
|
2068
|
+
}
|
|
2069
|
+
} catch (_err) {
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
let pageScopeType;
|
|
2073
|
+
if (input.pageId) {
|
|
2074
|
+
const scopeResult = await getPageScopeType(
|
|
2075
|
+
input.pageId,
|
|
2076
|
+
input.scope.appId,
|
|
2077
|
+
resolvedAppName
|
|
2078
|
+
);
|
|
2079
|
+
if (!scopeResult.ok) {
|
|
2080
|
+
log4.error("Failed to get page scope type:", scopeResult.error);
|
|
2081
|
+
return err(scopeResult.error);
|
|
2082
|
+
}
|
|
2083
|
+
if (!scopeResult.data) {
|
|
2084
|
+
return err({ code: "PAGE_SCOPE_TYPE_MISSING", message: `Page ${input.pageId} does not have scope_type set` });
|
|
2085
|
+
}
|
|
2086
|
+
pageScopeType = scopeResult.data;
|
|
2087
|
+
} else {
|
|
2088
|
+
pageScopeType = "organisation";
|
|
2089
|
+
}
|
|
2090
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
2091
|
+
input.scope,
|
|
2092
|
+
pageScopeType,
|
|
2093
|
+
resolvedAppName,
|
|
2094
|
+
engine["supabase"]
|
|
2095
|
+
);
|
|
2096
|
+
if (!validation.isValid || !validation.resolvedScope) {
|
|
2097
|
+
throw validation.error || new OrganisationContextRequiredError();
|
|
2098
|
+
}
|
|
2099
|
+
const validatedScope = validation.resolvedScope;
|
|
2100
|
+
if (pageScopeType === "both" && input.pageId) {
|
|
2101
|
+
const eventScope = {
|
|
2102
|
+
organisationId: validatedScope.organisationId,
|
|
2103
|
+
// Org derived from event
|
|
2104
|
+
eventId: validatedScope.eventId,
|
|
2105
|
+
appId: validatedScope.appId
|
|
2106
|
+
};
|
|
2107
|
+
const eventSecurityContext = {
|
|
2108
|
+
userId: input.userId,
|
|
2109
|
+
organisationId: eventScope.organisationId || null,
|
|
2110
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2111
|
+
};
|
|
2112
|
+
const eventInput = {
|
|
2113
|
+
...input,
|
|
2114
|
+
scope: eventScope
|
|
2115
|
+
};
|
|
2116
|
+
const hasEventPermission = await engine.isPermitted(eventInput, eventSecurityContext);
|
|
2117
|
+
if (validatedScope.organisationId && validatedScope.eventId) {
|
|
2118
|
+
const orgScope = {
|
|
2119
|
+
organisationId: validatedScope.organisationId,
|
|
2120
|
+
eventId: void 0,
|
|
2121
|
+
// Clear event for org-only check
|
|
2122
|
+
appId: validatedScope.appId
|
|
2123
|
+
};
|
|
2124
|
+
const orgSecurityContext = {
|
|
2125
|
+
userId: input.userId,
|
|
2126
|
+
organisationId: orgScope.organisationId || null,
|
|
2127
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2128
|
+
};
|
|
2129
|
+
const orgInput = {
|
|
2130
|
+
...input,
|
|
2131
|
+
scope: orgScope
|
|
2132
|
+
};
|
|
2133
|
+
const hasOrgPermission = await engine.isPermitted(orgInput, orgSecurityContext);
|
|
2134
|
+
return ok(hasEventPermission || hasOrgPermission);
|
|
2135
|
+
}
|
|
2136
|
+
return ok(hasEventPermission);
|
|
2137
|
+
}
|
|
2138
|
+
const securityContext = {
|
|
2139
|
+
userId: input.userId,
|
|
2140
|
+
organisationId: validatedScope.organisationId || null,
|
|
2141
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2142
|
+
};
|
|
2143
|
+
const validatedInput = {
|
|
2144
|
+
...input,
|
|
2145
|
+
scope: validatedScope
|
|
2146
|
+
};
|
|
2147
|
+
const permitted = await engine.isPermitted(validatedInput, securityContext);
|
|
2148
|
+
return ok(permitted);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
return err(toApiError(error));
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
async function isPermittedCached(input, appName) {
|
|
2154
|
+
const { userId, scope, permission, pageId } = input;
|
|
2155
|
+
const cacheKey = RBACCache.generatePermissionKey({
|
|
2156
|
+
userId,
|
|
2157
|
+
organisationId: scope.organisationId,
|
|
2158
|
+
eventId: scope.eventId,
|
|
2159
|
+
appId: scope.appId,
|
|
2160
|
+
permission,
|
|
2161
|
+
pageId
|
|
2162
|
+
});
|
|
2163
|
+
const cached = rbacCache.get(cacheKey, true);
|
|
2164
|
+
if (cached !== null) {
|
|
2165
|
+
return ok(cached);
|
|
2166
|
+
}
|
|
2167
|
+
return getOrCreateRequest(input, async (checkInput) => {
|
|
2168
|
+
const result = await isPermitted(checkInput, appName, null);
|
|
2169
|
+
if (!result.ok) {
|
|
2170
|
+
return result;
|
|
2171
|
+
}
|
|
2172
|
+
const isPageLevelCheck = !!pageId || permission.includes("page.");
|
|
2173
|
+
rbacCache.set(cacheKey, result.data, void 0, isPageLevelCheck);
|
|
2174
|
+
return ok(result.data);
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
async function hasAnyPermission(input) {
|
|
2178
|
+
const { permissions, ...baseInput } = input;
|
|
2179
|
+
for (const permission of permissions) {
|
|
2180
|
+
const result = await isPermitted({
|
|
2181
|
+
...baseInput,
|
|
2182
|
+
permission
|
|
2183
|
+
});
|
|
2184
|
+
if (!result.ok) {
|
|
2185
|
+
return result;
|
|
2186
|
+
}
|
|
2187
|
+
if (result.data) {
|
|
2188
|
+
return ok(true);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return ok(false);
|
|
2192
|
+
}
|
|
2193
|
+
async function hasAllPermissions(input) {
|
|
2194
|
+
const { permissions, ...baseInput } = input;
|
|
2195
|
+
for (const permission of permissions) {
|
|
2196
|
+
const result = await isPermitted({
|
|
2197
|
+
...baseInput,
|
|
2198
|
+
permission
|
|
2199
|
+
});
|
|
2200
|
+
if (!result.ok) {
|
|
2201
|
+
return result;
|
|
2202
|
+
}
|
|
2203
|
+
if (!result.data) {
|
|
2204
|
+
return ok(false);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return ok(true);
|
|
2208
|
+
}
|
|
2209
|
+
async function isSuperAdmin(userId) {
|
|
2210
|
+
try {
|
|
2211
|
+
const engine = getEngine();
|
|
2212
|
+
const value = await engine["checkSuperAdmin"](userId);
|
|
2213
|
+
return ok(value);
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
return err(toApiError(error));
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async function getPageScopeType(pageId, appId, appName) {
|
|
2219
|
+
try {
|
|
2220
|
+
const engine = getEngine();
|
|
2221
|
+
let resolvedAppId = appId;
|
|
2222
|
+
if (!resolvedAppId && appName) {
|
|
2223
|
+
const { data: app, error: appError } = await engine["supabase"].from("rbac_apps").select("id, name, is_active").eq("name", appName).eq("is_active", true).maybeSingle();
|
|
2224
|
+
if (appError) {
|
|
2225
|
+
log4.error("Error resolving appId from appName", {
|
|
2226
|
+
appName,
|
|
2227
|
+
pageId,
|
|
2228
|
+
error: appError,
|
|
2229
|
+
errorMessage: appError instanceof Error ? appError.message : String(appError)
|
|
2230
|
+
});
|
|
2231
|
+
return err({ code: "APP_RESOLVE_FAILED", message: `Failed to resolve appId from appName "${appName}": ${appError instanceof Error ? appError.message : String(appError)}` });
|
|
2232
|
+
}
|
|
2233
|
+
if (!app) {
|
|
2234
|
+
log4.error("App not found or inactive", { appName, pageId });
|
|
2235
|
+
return err({ code: "APP_NOT_FOUND", message: `Could not resolve appId for appName "${appName}" - app not found or not active` });
|
|
2236
|
+
}
|
|
2237
|
+
resolvedAppId = app.id;
|
|
2238
|
+
}
|
|
2239
|
+
if (!resolvedAppId) {
|
|
2240
|
+
log4.error("No appId resolved", { pageId, appId, appName });
|
|
2241
|
+
return err({ code: "APP_ID_REQUIRED", message: `Could not resolve appId for page ${pageId} - appId and appName both missing or invalid` });
|
|
2242
|
+
}
|
|
2243
|
+
let resolvedPageId = pageId;
|
|
2244
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2245
|
+
if (!uuidRegex.test(pageId)) {
|
|
2246
|
+
const { data: page, error: pageError } = await engine["supabase"].from("rbac_app_pages").select("id, page_name, app_id").eq("app_id", resolvedAppId).eq("page_name", pageId).maybeSingle();
|
|
2247
|
+
if (pageError) {
|
|
2248
|
+
log4.error("Error resolving pageId from page_name", { pageId, appId: resolvedAppId, appName, error: pageError });
|
|
2249
|
+
return err({ code: "PAGE_RESOLVE_FAILED", message: `Failed to resolve pageId "${pageId}" for appId ${resolvedAppId}: ${pageError instanceof Error ? pageError.message : String(pageError)}` });
|
|
2250
|
+
}
|
|
2251
|
+
if (!page) {
|
|
2252
|
+
log4.error("Page not found in database", { pageId, appId: resolvedAppId, appName });
|
|
2253
|
+
return err({ code: "PAGE_NOT_FOUND", message: `Could not resolve pageId "${pageId}" to a valid UUID - page not found for appId ${resolvedAppId}` });
|
|
2254
|
+
}
|
|
2255
|
+
resolvedPageId = page.id;
|
|
2256
|
+
}
|
|
2257
|
+
if (!uuidRegex.test(resolvedPageId)) {
|
|
2258
|
+
log4.error("PageId resolution failed - not a valid UUID", { originalPageId: pageId, resolvedPageId, appId: resolvedAppId, appName });
|
|
2259
|
+
return err({ code: "PAGE_ID_INVALID", message: `Could not resolve pageId ${pageId} to a valid UUID` });
|
|
2260
|
+
}
|
|
2261
|
+
const { data: pageData, error } = await engine["supabase"].from("rbac_app_pages").select("scope_type, page_name, app_id").eq("id", resolvedPageId).single();
|
|
2262
|
+
if (error) {
|
|
2263
|
+
log4.error("Error fetching page scope type from database", { pageId: resolvedPageId, originalPageId: pageId, appId: resolvedAppId, appName, error });
|
|
2264
|
+
return err({ code: "PAGE_SCOPE_FETCH_FAILED", message: `Failed to get page scope type: ${error instanceof Error ? error.message : String(error)}` });
|
|
2265
|
+
}
|
|
2266
|
+
if (!pageData || !pageData.scope_type) {
|
|
2267
|
+
log4.error("Page found but scope_type is missing", { pageId: resolvedPageId, originalPageId: pageId, appId: resolvedAppId, appName, pageData });
|
|
2268
|
+
return err({ code: "PAGE_SCOPE_TYPE_MISSING", message: `Page ${resolvedPageId} does not have scope_type set` });
|
|
2269
|
+
}
|
|
2270
|
+
return ok(pageData.scope_type);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
log4.error("Error fetching page scope type (catch block)", { pageId, appId, appName, error });
|
|
2273
|
+
return err(toApiError(error));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function isOrganisationAdmin(userId, organisationId) {
|
|
2277
|
+
const result = await getAccessLevel({
|
|
2278
|
+
userId,
|
|
2279
|
+
scope: { organisationId }
|
|
2280
|
+
});
|
|
2281
|
+
if (!result.ok) {
|
|
2282
|
+
return result;
|
|
2283
|
+
}
|
|
2284
|
+
return ok(result.data === "admin" || result.data === "super");
|
|
2285
|
+
}
|
|
2286
|
+
async function isEventAdmin(userId, scope) {
|
|
2287
|
+
if (!scope.eventId || !scope.appId) {
|
|
2288
|
+
return ok(false);
|
|
2289
|
+
}
|
|
2290
|
+
const result = await getAccessLevel({ userId, scope });
|
|
2291
|
+
if (!result.ok) {
|
|
2292
|
+
return result;
|
|
2293
|
+
}
|
|
2294
|
+
return ok(result.data === "admin" || result.data === "super");
|
|
2295
|
+
}
|
|
2296
|
+
function invalidateUserCache(userId, organisationId) {
|
|
2297
|
+
const patterns = organisationId ? [
|
|
2298
|
+
CACHE_PATTERNS.PERMISSION(userId, organisationId),
|
|
2299
|
+
`access:${userId}:${organisationId}:`,
|
|
2300
|
+
`map:${userId}:${organisationId}:`
|
|
2301
|
+
] : [
|
|
2302
|
+
`perm:${userId}:`,
|
|
2303
|
+
`access:${userId}:`,
|
|
2304
|
+
`map:${userId}:`
|
|
2305
|
+
];
|
|
2306
|
+
patterns.forEach((pattern) => rbacCache.invalidate(pattern));
|
|
2307
|
+
}
|
|
2308
|
+
function invalidateOrganisationCache(organisationId) {
|
|
2309
|
+
rbacCache.invalidate(CACHE_PATTERNS.ORGANISATION(organisationId));
|
|
2310
|
+
}
|
|
2311
|
+
function invalidateEventCache(eventId) {
|
|
2312
|
+
rbacCache.invalidate(CACHE_PATTERNS.EVENT(eventId));
|
|
2313
|
+
}
|
|
2314
|
+
function invalidateAppCache(appId) {
|
|
2315
|
+
rbacCache.invalidate(CACHE_PATTERNS.APP(appId));
|
|
2316
|
+
}
|
|
2317
|
+
function clearCache() {
|
|
2318
|
+
rbacCache.clear();
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
export { CACHE_PATTERNS, ContextValidator, EventContextRequiredError, OrganisationContextRequiredError, RBACCache, RBACEngine, clearCache, clearInFlightRequests, createRBACConfig, createRBACEngine, disablePerformanceMonitoring, enablePerformanceMonitoring, getAccessLevel, getInFlightRequestCount, getPageScopeType, getPerformanceMetrics, getPerformanceSummary, getPermissionMap, getRBACConfig, getRBACLogger, getRoleContext, hasAllPermissions, hasAnyPermission, invalidateAppCache, invalidateEventCache, invalidateOrganisationCache, invalidateUserCache, isDebugMode, isDevelopmentMode, isEventAdmin, isOrganisationAdmin, isPerformanceMonitoringEnabled, isPermitted, isPermittedCached, isRBACInitialized, isSuperAdmin, rbacCache, recordAuditEvent, recordPermissionCheck, resetPerformanceMetrics, resolveAppContext, setupRBAC };
|