@jmruthers/pace-core 0.6.6 → 0.6.7

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.
Files changed (246) hide show
  1. package/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +12 -13
  2. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  3. package/audit-tool/audits/02-project-structure.cjs +255 -0
  4. package/audit-tool/audits/03-architecture.cjs +196 -0
  5. package/audit-tool/audits/04-code-quality.cjs +149 -0
  6. package/audit-tool/audits/05-styling.cjs +224 -0
  7. package/audit-tool/audits/06-security-rbac.cjs +544 -0
  8. package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
  9. package/audit-tool/audits/08-testing-documentation.cjs +202 -0
  10. package/audit-tool/audits/09-operations.cjs +208 -0
  11. package/audit-tool/index.cjs +291 -0
  12. package/audit-tool/utils/code-utils.cjs +218 -0
  13. package/audit-tool/utils/file-utils.cjs +230 -0
  14. package/audit-tool/utils/report-utils.cjs +241 -0
  15. package/cursor-rules/00-standards-overview.mdc +156 -0
  16. package/cursor-rules/{00-pace-core-compliance.mdc → 01-pace-core-compliance.mdc} +187 -34
  17. package/cursor-rules/02-project-structure.mdc +37 -5
  18. package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +125 -11
  19. package/cursor-rules/04-code-quality.mdc +419 -0
  20. package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +55 -10
  21. package/cursor-rules/{09-rbac-compliance.mdc → 06-security-rbac.mdc} +62 -6
  22. package/cursor-rules/07-api-tech-stack.mdc +377 -0
  23. package/cursor-rules/08-testing-documentation.mdc +324 -0
  24. package/cursor-rules/09-operations.mdc +365 -0
  25. package/dist/DataTable-7PMH7XN7.js +15 -0
  26. package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
  27. package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +26 -16
  28. package/dist/{chunk-FENMYN2U.js → chunk-5X4QLXRG.js} +1 -3
  29. package/dist/{chunk-4T7OBVTU.js → chunk-6F3IILHI.js} +1 -1
  30. package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
  31. package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
  32. package/dist/{chunk-7TYHROIV.js → chunk-BM4CQ5P3.js} +50 -8
  33. package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
  34. package/dist/{chunk-OHIK3MIO.js → chunk-GHYHJTYV.js} +2 -2
  35. package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
  36. package/dist/{chunk-LAZMKTTF.js → chunk-JGWDVX64.js} +281 -347
  37. package/dist/{chunk-MAGBIDNS.js → chunk-L4XMVJKY.js} +2 -2
  38. package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
  39. package/dist/{chunk-ZS5VO5JB.js → chunk-Q7Q7V5NV.js} +406 -451
  40. package/dist/{chunk-3O3WHILE.js → chunk-VBCS3DUA.js} +236 -60
  41. package/dist/{chunk-BVP2BCJF.js → chunk-ZKAWKYT4.js} +8 -8
  42. package/dist/components.d.ts +5 -4
  43. package/dist/components.js +27 -32
  44. package/dist/eslint-rules/index.cjs +22 -9
  45. package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
  46. package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
  47. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  48. package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +26 -10
  49. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
  50. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  51. package/dist/hooks.d.ts +5 -5
  52. package/dist/hooks.js +6 -6
  53. package/dist/index.d.ts +6 -6
  54. package/dist/index.js +18 -17
  55. package/dist/rbac/index.js +6 -6
  56. package/dist/theming/runtime.d.ts +14 -1
  57. package/dist/theming/runtime.js +1 -1
  58. package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
  59. package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
  60. package/dist/utils.d.ts +2 -2
  61. package/dist/utils.js +8 -8
  62. package/docs/README.md +1 -1
  63. package/docs/api/modules.md +47 -31
  64. package/docs/api-reference/components.md +18 -20
  65. package/docs/api-reference/hooks.md +80 -80
  66. package/docs/api-reference/types.md +1 -1
  67. package/docs/api-reference/utilities.md +1 -1
  68. package/docs/architecture/README.md +1 -1
  69. package/docs/core-concepts/events.md +3 -3
  70. package/docs/core-concepts/organisations.md +6 -6
  71. package/docs/core-concepts/permissions.md +6 -6
  72. package/docs/documentation-index.md +12 -18
  73. package/docs/getting-started/documentation-index.md +1 -1
  74. package/docs/getting-started/examples/README.md +4 -4
  75. package/docs/getting-started/examples/full-featured-app.md +1 -1
  76. package/docs/getting-started/faq.md +2 -2
  77. package/docs/getting-started/quick-reference.md +4 -4
  78. package/docs/implementation-guides/authentication.md +15 -15
  79. package/docs/implementation-guides/component-styling.md +1 -1
  80. package/docs/implementation-guides/data-tables.md +126 -33
  81. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  82. package/docs/implementation-guides/dynamic-colors.md +3 -3
  83. package/docs/implementation-guides/file-upload-storage.md +2 -2
  84. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  85. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  86. package/docs/implementation-guides/large-datasets.md +3 -2
  87. package/docs/implementation-guides/organisation-security.md +2 -2
  88. package/docs/implementation-guides/performance.md +2 -2
  89. package/docs/implementation-guides/permission-enforcement.md +1 -1
  90. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  91. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  92. package/docs/rbac/README.md +5 -5
  93. package/docs/rbac/advanced-patterns.md +6 -6
  94. package/docs/rbac/api-reference.md +20 -20
  95. package/docs/rbac/event-based-apps.md +3 -3
  96. package/docs/rbac/examples.md +41 -41
  97. package/docs/rbac/getting-started.md +37 -37
  98. package/docs/rbac/performance.md +1 -1
  99. package/docs/rbac/quick-start.md +52 -52
  100. package/docs/rbac/secure-client-protection.md +1 -1
  101. package/docs/rbac/troubleshooting.md +1 -1
  102. package/docs/security/README.md +5 -5
  103. package/docs/standards/0-standards-overview.md +220 -0
  104. package/docs/standards/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +204 -185
  105. package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
  106. package/docs/standards/3-architecture-standards.md +606 -0
  107. package/docs/standards/4-code-quality-standards.md +728 -0
  108. package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
  109. package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
  110. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  111. package/docs/standards/8-testing-documentation-standards.md +401 -0
  112. package/docs/standards/9-operations-standards.md +1102 -0
  113. package/docs/standards/README.md +203 -104
  114. package/docs/troubleshooting/README.md +4 -4
  115. package/docs/troubleshooting/common-issues.md +2 -2
  116. package/docs/troubleshooting/debugging.md +9 -9
  117. package/docs/troubleshooting/migration.md +4 -4
  118. package/eslint-config-pace-core.cjs +21 -10
  119. package/package.json +6 -5
  120. package/scripts/install-cursor-rules.cjs +11 -243
  121. package/scripts/install-eslint-config.cjs +284 -0
  122. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
  123. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  124. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +10 -10
  125. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  126. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  127. package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
  128. package/src/__tests__/templates/component.test.template.tsx +18 -15
  129. package/src/components/Calendar/Calendar.tsx +201 -47
  130. package/src/components/ContextSelector/ContextSelector.tsx +137 -153
  131. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  132. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
  133. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
  134. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  135. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  136. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  137. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  138. package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
  139. package/src/components/DataTable/components/EditableRow.tsx +5 -7
  140. package/src/components/DataTable/components/EmptyState.tsx +10 -9
  141. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  142. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  143. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  144. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  145. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  146. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  147. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  148. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  149. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  150. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
  151. package/src/components/DataTable/components/index.ts +2 -1
  152. package/src/components/DataTable/types.ts +0 -18
  153. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  154. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
  155. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  156. package/src/components/DateTimeField/DateTimeField.tsx +7 -8
  157. package/src/components/Dialog/Dialog.test.tsx +1 -0
  158. package/src/components/Dialog/Dialog.tsx +25 -8
  159. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  160. package/src/components/FileUpload/FileUpload.test.tsx +52 -14
  161. package/src/components/FileUpload/FileUpload.tsx +112 -130
  162. package/src/components/Progress/Progress.tsx +2 -4
  163. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  164. package/src/components/Select/Select.tsx +86 -77
  165. package/src/components/Select/types.ts +3 -0
  166. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  167. package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
  168. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
  169. package/src/hooks/public/usePublicEvent.ts +5 -5
  170. package/src/hooks/public/usePublicEventLogo.ts +5 -5
  171. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  172. package/src/hooks/public/usePublicRouteParams.ts +5 -5
  173. package/src/hooks/useAppConfig.ts +2 -2
  174. package/src/hooks/useEventTheme.test.ts +7 -7
  175. package/src/hooks/useEventTheme.ts +1 -4
  176. package/src/hooks/useFileDisplay.ts +2 -2
  177. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  178. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  179. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  180. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  181. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  182. package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
  183. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  184. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  185. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
  186. package/src/styles/core.css +7 -0
  187. package/src/theming/__tests__/parseEventColours.test.ts +9 -3
  188. package/src/theming/parseEventColours.ts +22 -10
  189. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  190. package/src/utils/storage/README.md +1 -1
  191. package/cursor-rules/01-standards-compliance.mdc +0 -285
  192. package/cursor-rules/04-testing-standards.mdc +0 -270
  193. package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
  194. package/cursor-rules/06-code-quality.mdc +0 -311
  195. package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
  196. package/cursor-rules/10-error-handling-patterns.mdc +0 -179
  197. package/cursor-rules/11-performance-optimization.mdc +0 -169
  198. package/cursor-rules/12-ci-cd-integration.mdc +0 -150
  199. package/dist/DataTable-LRJL4IRV.js +0 -15
  200. package/dist/eslint-rules/rules/compliance.cjs +0 -348
  201. package/dist/eslint-rules/rules/components.cjs +0 -113
  202. package/dist/eslint-rules/rules/imports.cjs +0 -102
  203. package/docs/best-practices/README.md +0 -472
  204. package/docs/best-practices/accessibility.md +0 -604
  205. package/docs/best-practices/common-patterns.md +0 -516
  206. package/docs/best-practices/deployment.md +0 -1103
  207. package/docs/best-practices/performance.md +0 -1328
  208. package/docs/best-practices/security.md +0 -940
  209. package/docs/best-practices/testing.md +0 -1034
  210. package/docs/rbac/compliance/compliance-guide.md +0 -544
  211. package/docs/standards/01-standards-compliance.md +0 -188
  212. package/docs/standards/03-solid-principles.md +0 -39
  213. package/docs/standards/04-testing-standards.md +0 -36
  214. package/docs/standards/05-bug-reports-and-features.md +0 -27
  215. package/docs/standards/06-code-quality.md +0 -34
  216. package/docs/standards/07-tech-stack-compliance.md +0 -30
  217. package/docs/standards/10-error-handling-patterns.md +0 -401
  218. package/docs/standards/11-performance-optimization.md +0 -348
  219. package/docs/standards/12-ci-cd-integration.md +0 -370
  220. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
  221. package/scripts/audit/audit-compliance.cjs +0 -1295
  222. package/scripts/audit/audit-components.cjs +0 -260
  223. package/scripts/audit/audit-rbac.cjs +0 -954
  224. package/scripts/audit/audit-standards.cjs +0 -1268
  225. package/scripts/audit/index.cjs +0 -1927
  226. package/src/components/DataTable/components/DataTableBody.tsx +0 -478
  227. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  228. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  229. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  230. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  231. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  232. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  233. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  234. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  235. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  236. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  237. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  238. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  239. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  240. package/src/components/DataTable/utils/debugTools.ts +0 -514
  241. package/src/eslint-rules/index.cjs +0 -22
  242. package/src/eslint-rules/rules/components.cjs +0 -113
  243. package/src/eslint-rules/rules/imports.cjs +0 -102
  244. package/src/eslint-rules/rules/rbac.cjs +0 -790
  245. package/src/eslint-rules/utils/helpers.cjs +0 -42
  246. package/src/eslint-rules/utils/manifest-loader.cjs +0 -75
@@ -1,790 +0,0 @@
1
- /**
2
- * RBAC-specific rules
3
- * @package @jmruthers/pace-core
4
- * @module ESLintRules/rules/rbac
5
- */
6
-
7
- const { isPaceCoreSourceFile } = require('../utils/helpers.cjs');
8
-
9
- module.exports = {
10
- rules: {
11
- /**
12
- * Disallow direct Supabase client creation - must use useSecureSupabase
13
- */
14
- 'no-direct-supabase-client': {
15
- meta: {
16
- type: 'problem',
17
- docs: {
18
- description: 'Disallow direct createClient calls from @supabase/supabase-js. Use useSecureSupabase() from pace-core instead to ensure organisation context and RLS policies are enforced.',
19
- category: 'Security',
20
- recommended: true
21
- },
22
- messages: {
23
- directClientCreation: "Direct Supabase client creation detected. You MUST use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead to ensure organisation context and RLS policies are enforced. This prevents cross-organisation data access.",
24
- directClientImport: "Direct import of createClient from @supabase/supabase-js is not allowed. Use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead."
25
- },
26
- hasSuggestions: true
27
- },
28
- create(context) {
29
- const filename = context.getFilename();
30
-
31
- // Exclude pace-core source files - these rules are for consuming apps
32
- if (isPaceCoreSourceFile(filename)) {
33
- return {};
34
- }
35
-
36
- // Allow createClient in specific config files (supabaseClient.ts/js, etc.)
37
- const isConfigFile = /(supabase|client)\.(ts|js|tsx|jsx)$/i.test(filename) &&
38
- (filename.includes('supabase') || filename.includes('client'));
39
-
40
- return {
41
- ImportDeclaration(node) {
42
- const importSource = node.source.value;
43
-
44
- // Check for @supabase/supabase-js import
45
- if (importSource === '@supabase/supabase-js') {
46
- // Check if createClient is imported
47
- const hasCreateClient = node.specifiers.some(spec => {
48
- if (spec.type === 'ImportSpecifier') {
49
- return spec.imported.name === 'createClient';
50
- }
51
- if (spec.type === 'ImportNamespaceSpecifier') {
52
- return true; // import * as supabase
53
- }
54
- return false;
55
- });
56
-
57
- if (hasCreateClient && !isConfigFile) {
58
- context.report({
59
- node: node.source,
60
- messageId: 'directClientImport',
61
- suggest: [{
62
- desc: 'Replace with useSecureSupabase hook',
63
- fix(fixer) {
64
- return fixer.remove(node);
65
- }
66
- }]
67
- });
68
- }
69
- }
70
- },
71
-
72
- CallExpression(node) {
73
- // Check for createClient() calls
74
- if (node.callee.type === 'Identifier' && node.callee.name === 'createClient') {
75
- if (!isConfigFile) {
76
- context.report({
77
- node,
78
- messageId: 'directClientCreation',
79
- suggest: [{
80
- desc: 'Use useSecureSupabase() hook instead',
81
- fix(fixer) {
82
- return null;
83
- }
84
- }]
85
- });
86
- }
87
- }
88
-
89
- // Check for supabase.createClient() or similar patterns
90
- if (node.callee.type === 'MemberExpression' &&
91
- node.callee.property?.name === 'createClient') {
92
- if (!isConfigFile) {
93
- context.report({
94
- node,
95
- messageId: 'directClientCreation',
96
- suggest: [{
97
- desc: 'Use useSecureSupabase() hook instead',
98
- fix(fixer) {
99
- return null;
100
- }
101
- }]
102
- });
103
- }
104
- }
105
- }
106
- };
107
- }
108
- },
109
-
110
- /**
111
- * Check RBAC permission loading state usage
112
- */
113
- 'rbac-permission-loading': {
114
- meta: {
115
- type: 'problem',
116
- docs: {
117
- description: 'Require isLoading extraction from useResourcePermissions and check it before permission calls in mutations.',
118
- category: 'Security',
119
- recommended: true
120
- },
121
- messages: {
122
- missingIsLoading: "useResourcePermissions is used but 'isLoading' is not extracted. Permission checks may fail if scope resolution is still in progress.",
123
- missingLoadingCheck: "Permission check '{{permission}}()' is called without checking isLoading first. This can cause false negatives when scope resolution is still in progress."
124
- },
125
- hasSuggestions: true
126
- },
127
- create(context) {
128
- let useResourcePermissionsFound = false;
129
- let hasIsLoadingExtraction = false;
130
- let isLoadingVarName = null;
131
-
132
- return {
133
- CallExpression(node) {
134
- // Find useResourcePermissions calls
135
- if (node.callee.type === 'Identifier' &&
136
- node.callee.name === 'useResourcePermissions') {
137
- useResourcePermissionsFound = true;
138
-
139
- // Check parent to see if isLoading is destructured
140
- const parent = node.parent;
141
- if (parent && parent.type === 'VariableDeclarator' &&
142
- parent.id && parent.id.type === 'ObjectPattern') {
143
- const properties = parent.id.properties;
144
- const isLoadingProp = properties.find(prop => {
145
- if (prop.type === 'Property') {
146
- const key = prop.key;
147
- if (key.type === 'Identifier' && key.name === 'isLoading') {
148
- return true;
149
- }
150
- // Also check for renamed: isLoading: permissionsLoading
151
- if (key.type === 'Identifier' && key.name === 'isLoading') {
152
- if (prop.value && prop.value.type === 'Identifier') {
153
- isLoadingVarName = prop.value.name;
154
- }
155
- return true;
156
- }
157
- }
158
- return false;
159
- });
160
-
161
- if (isLoadingProp) {
162
- hasIsLoadingExtraction = true;
163
- if (!isLoadingVarName) {
164
- isLoadingVarName = 'isLoading';
165
- }
166
- }
167
- }
168
- }
169
-
170
- // Check for permission function calls in mutations
171
- if (useResourcePermissionsFound &&
172
- node.callee.type === 'Identifier' &&
173
- ['canCreate', 'canUpdate', 'canDelete', 'canRead'].includes(node.callee.name)) {
174
-
175
- // Check if we're in a mutation context
176
- const sourceCode = context.sourceCode || context.getSourceCode();
177
- // ESLint 9: use sourceCode.getAncestors(node), ESLint 8: use context.getAncestors()
178
- const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : (context.getAncestors ? context.getAncestors() : []);
179
- const isInMutation = ancestors.some(ancestor => {
180
- if (ancestor.type === 'Property' && ancestor.key) {
181
- const keyName = ancestor.key.name || (ancestor.key.type === 'Identifier' && ancestor.key.name);
182
- return keyName === 'mutationFn' || keyName === 'mutateAsync';
183
- }
184
- return false;
185
- });
186
-
187
- if (isInMutation) {
188
- // Check if isLoading is checked before this call
189
- const nodeText = sourceCode.getText(node);
190
- const beforeNode = sourceCode.getText().substring(0, sourceCode.getIndexFromLoc(node.loc.start));
191
-
192
- // Look for isLoading check before this call
193
- const hasLoadingCheck = new RegExp(
194
- `(if\\s*\\(\\s*!?\\s*(${isLoadingVarName || 'isLoading'})|if\\s*\\(\\s*(${isLoadingVarName || 'isLoading'})\\s*===\\s*false|if\\s*\\(\\s*(${isLoadingVarName || 'isLoading'})\\s*!==\\s*true|await)`,
195
- 'i'
196
- ).test(beforeNode);
197
-
198
- if (!hasIsLoadingExtraction) {
199
- context.report({
200
- node,
201
- messageId: 'missingIsLoading',
202
- suggest: [{
203
- desc: `Extract 'isLoading' from useResourcePermissions`,
204
- fix(fixer) {
205
- return null; // Complex fix
206
- }
207
- }]
208
- });
209
- } else if (!hasLoadingCheck) {
210
- context.report({
211
- node,
212
- messageId: 'missingLoadingCheck',
213
- data: {
214
- permission: node.callee.name
215
- },
216
- suggest: [{
217
- desc: `Check ${isLoadingVarName || 'isLoading'} before calling permission function`,
218
- fix(fixer) {
219
- return null; // Complex fix
220
- }
221
- }]
222
- });
223
- }
224
- }
225
- }
226
- }
227
- };
228
- }
229
- },
230
-
231
- /**
232
- * Disallow direct RPC calls to RBAC functions
233
- */
234
- 'no-direct-rbac-rpc': {
235
- meta: {
236
- type: 'problem',
237
- docs: {
238
- description: 'Disallow direct calls to RBAC RPC functions. Use pace-core RBAC hooks instead.',
239
- category: 'Security',
240
- recommended: true
241
- },
242
- messages: {
243
- directRbacRpc: "Direct RPC call to '{{rpcName}}' detected. Use pace-core RBAC hooks from '@jmruthers/pace-core/rbac' instead."
244
- },
245
- hasSuggestions: true
246
- },
247
- create(context) {
248
- const filename = context.getFilename();
249
-
250
- // Exclude pace-core source files - these rules are for consuming apps
251
- if (isPaceCoreSourceFile(filename)) {
252
- return {};
253
- }
254
-
255
- const rbacRpcPatterns = ['rbac_check_permission_simplified', 'rbac_'];
256
-
257
- return {
258
- CallExpression(node) {
259
- // Check for supabase.rpc('rbac_*', ...)
260
- if (node.callee.type === 'MemberExpression' &&
261
- node.callee.property &&
262
- node.callee.property.name === 'rpc' &&
263
- node.arguments.length > 0) {
264
-
265
- const firstArg = node.arguments[0];
266
- if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
267
- const rpcName = firstArg.value;
268
- if (rpcName.startsWith('rbac_')) {
269
- context.report({
270
- node: firstArg,
271
- messageId: 'directRbacRpc',
272
- data: {
273
- rpcName
274
- },
275
- suggest: [{
276
- desc: 'Use pace-core RBAC hooks instead',
277
- fix(fixer) {
278
- return null;
279
- }
280
- }]
281
- });
282
- }
283
- }
284
- }
285
- }
286
- };
287
- }
288
- },
289
-
290
- /**
291
- * Disallow direct queries to RBAC tables
292
- */
293
- 'no-direct-rbac-table': {
294
- meta: {
295
- type: 'problem',
296
- docs: {
297
- description: 'Disallow direct queries to RBAC tables. Use pace-core RBAC API functions or useSecureSupabase instead.',
298
- category: 'Security',
299
- recommended: true
300
- },
301
- messages: {
302
- directRbacTable: "Direct query to RBAC table '{{tableName}}' detected. Use pace-core RBAC API functions from '@jmruthers/pace-core/rbac' or useSecureSupabase hook instead."
303
- },
304
- hasSuggestions: true
305
- },
306
- create(context) {
307
- const filename = context.getFilename();
308
-
309
- // Exclude pace-core source files - these rules are for consuming apps
310
- if (isPaceCoreSourceFile(filename)) {
311
- return {};
312
- }
313
-
314
- const rbacTables = [
315
- 'rbac_organisation_roles',
316
- 'rbac_event_app_roles',
317
- 'rbac_global_roles',
318
- 'rbac_apps',
319
- 'rbac_app_pages',
320
- 'rbac_page_permissions',
321
- 'rbac_user_profiles'
322
- ];
323
-
324
- let hasSecureSupabase = false;
325
- let secureSupabaseVarName = null;
326
-
327
- return {
328
- ImportDeclaration(node) {
329
- const importSource = node.source.value;
330
- if (importSource === '@jmruthers/pace-core/rbac' ||
331
- importSource.startsWith('@jmruthers/pace-core/rbac/')) {
332
- if (node.specifiers.some(spec =>
333
- spec.type === 'ImportSpecifier' && spec.imported.name === 'useSecureSupabase'
334
- )) {
335
- hasSecureSupabase = true;
336
- }
337
- }
338
- },
339
- VariableDeclarator(node) {
340
- if (node.init &&
341
- node.init.type === 'CallExpression' &&
342
- node.init.callee &&
343
- node.init.callee.name === 'useSecureSupabase') {
344
- if (node.id.type === 'Identifier') {
345
- secureSupabaseVarName = node.id.name;
346
- }
347
- }
348
- },
349
- CallExpression(node) {
350
- if (node.callee.type === 'MemberExpression' &&
351
- node.callee.property &&
352
- node.callee.property.name === 'from' &&
353
- node.arguments.length > 0) {
354
-
355
- // Check if this is called on secureSupabase
356
- let isSecureClient = false;
357
- if (node.callee.object.type === 'Identifier') {
358
- const objName = node.callee.object.name;
359
- if (objName === 'secureSupabase' ||
360
- objName === secureSupabaseVarName ||
361
- (hasSecureSupabase && objName.includes('secure'))) {
362
- isSecureClient = true;
363
- }
364
- }
365
-
366
- // Allow queries through secureSupabase
367
- if (isSecureClient) {
368
- return;
369
- }
370
-
371
- const firstArg = node.arguments[0];
372
- if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
373
- const tableName = firstArg.value;
374
- if (rbacTables.includes(tableName) || tableName.startsWith('rbac_')) {
375
- context.report({
376
- node: firstArg,
377
- messageId: 'directRbacTable',
378
- data: {
379
- tableName
380
- },
381
- suggest: [{
382
- desc: 'Use useSecureSupabase hook or pace-core RBAC API functions',
383
- fix(fixer) {
384
- return null;
385
- }
386
- }]
387
- });
388
- }
389
- }
390
- }
391
- }
392
- };
393
- }
394
- },
395
-
396
- /**
397
- * Disallow hardcoded role checks
398
- */
399
- 'no-hardcoded-role-checks': {
400
- meta: {
401
- type: 'problem',
402
- docs: {
403
- description: 'Disallow hardcoded role checks. Use useAccessLevel hook or getRoleContext API from pace-core instead.',
404
- category: 'Security',
405
- recommended: true
406
- },
407
- messages: {
408
- hardcodedRoleCheck: "Hardcoded role check detected. Use useAccessLevel hook or getRoleContext API from '@jmruthers/pace-core/rbac' instead."
409
- },
410
- hasSuggestions: true
411
- },
412
- create(context) {
413
- const filename = context.getFilename();
414
-
415
- // Exclude pace-core source files - these rules are for consuming apps
416
- if (isPaceCoreSourceFile(filename)) {
417
- return {};
418
- }
419
-
420
- const roleNames = ['admin', 'org_admin', 'event_admin', 'user', 'member', 'viewer', 'editor'];
421
- const roleCheckPattern = new RegExp(
422
- `(?:role|user\\.role|userRole|currentRole|userRoleName)\\s*(?:===|!==|==|!=)\\s*['"](${roleNames.join('|')})['"]`,
423
- 'i'
424
- );
425
-
426
- let hasPaceCoreImport = false;
427
-
428
- return {
429
- ImportDeclaration(node) {
430
- const importSource = node.source.value;
431
- if (importSource === '@jmruthers/pace-core/rbac' ||
432
- importSource.startsWith('@jmruthers/pace-core/rbac/')) {
433
- hasPaceCoreImport = true;
434
- }
435
- },
436
- BinaryExpression(node) {
437
- if (node.operator === '===' || node.operator === '!==' || node.operator === '==' || node.operator === '!=') {
438
- const sourceCode = context.getSourceCode();
439
- const leftText = sourceCode.getText(node.left);
440
- const rightText = sourceCode.getText(node.right);
441
-
442
- // Check if it's a role comparison
443
- if (roleCheckPattern.test(leftText + ' ' + node.operator + ' ' + rightText)) {
444
- // Check if using pace-core APIs
445
- if (!hasPaceCoreImport ||
446
- (!leftText.includes('useAccessLevel') && !leftText.includes('getRoleContext'))) {
447
- context.report({
448
- node,
449
- messageId: 'hardcodedRoleCheck',
450
- suggest: [{
451
- desc: 'Use useAccessLevel hook or getRoleContext API',
452
- fix(fixer) {
453
- return null;
454
- }
455
- }]
456
- });
457
- }
458
- }
459
- }
460
- }
461
- };
462
- }
463
- },
464
-
465
- /**
466
- * Require RESOURCE_NAMES constants in useResourcePermissions
467
- */
468
- 'rbac-use-resource-names-constants': {
469
- meta: {
470
- type: 'problem',
471
- docs: {
472
- description: 'Require RESOURCE_NAMES constants instead of string literals in useResourcePermissions calls.',
473
- category: 'Best Practices',
474
- recommended: true
475
- },
476
- messages: {
477
- resourcePermissionStringLiteral: "Resource permission string literal detected. Use RESOURCE_NAMES constant object instead (e.g., RESOURCE_NAMES.ORGANISATIONS, RESOURCE_NAMES.EVENTS)."
478
- },
479
- hasSuggestions: true
480
- },
481
- create(context) {
482
- const filename = context.getFilename();
483
-
484
- // Exclude pace-core source files - these rules are for consuming apps
485
- if (isPaceCoreSourceFile(filename)) {
486
- return {};
487
- }
488
-
489
- let hasResourceNamesImport = false;
490
-
491
- return {
492
- ImportDeclaration(node) {
493
- const importSource = node.source.value;
494
- if (node.specifiers.some(spec =>
495
- spec.type === 'ImportSpecifier' && spec.imported.name === 'RESOURCE_NAMES'
496
- )) {
497
- hasResourceNamesImport = true;
498
- }
499
- },
500
- CallExpression(node) {
501
- if (node.callee.type === 'Identifier' &&
502
- node.callee.name === 'useResourcePermissions') {
503
- // Check if argument is a string literal
504
- if (node.arguments.length > 0 &&
505
- node.arguments[0].type === 'Literal' &&
506
- typeof node.arguments[0].value === 'string') {
507
- // Check if RESOURCE_NAMES is used in the file
508
- const sourceCode = context.getSourceCode();
509
- const fileText = sourceCode.getText();
510
-
511
- if (!hasResourceNamesImport || !fileText.includes('RESOURCE_NAMES.')) {
512
- context.report({
513
- node: node.arguments[0],
514
- messageId: 'resourcePermissionStringLiteral',
515
- suggest: [{
516
- desc: 'Use RESOURCE_NAMES constant instead',
517
- fix(fixer) {
518
- return null; // Complex fix
519
- }
520
- }]
521
- });
522
- }
523
- }
524
- }
525
- }
526
- };
527
- }
528
- },
529
-
530
- /**
531
- * Disallow wrapper components around PagePermissionGuard
532
- */
533
- 'no-rbac-wrapper-components': {
534
- meta: {
535
- type: 'problem',
536
- docs: {
537
- description: 'Disallow wrapper components around PagePermissionGuard. Use PagePermissionGuard directly.',
538
- category: 'Security',
539
- recommended: true
540
- },
541
- messages: {
542
- wrapperComponent: "Wrapper component '{{componentName}}' detected around PagePermissionGuard. Must use PagePermissionGuard directly, not through wrappers."
543
- },
544
- hasSuggestions: true
545
- },
546
- create(context) {
547
- const filename = context.getFilename();
548
-
549
- // Allow in pace-core package itself
550
- if (filename.includes('packages/core/src/rbac') || filename.includes('packages\\core\\src\\rbac')) {
551
- return {};
552
- }
553
-
554
- let hasPagePermissionGuard = false;
555
- let wrapperComponentName = null;
556
-
557
- return {
558
- JSXOpeningElement(node) {
559
- if (node.name.type === 'JSXIdentifier' && node.name.name === 'PagePermissionGuard') {
560
- hasPagePermissionGuard = true;
561
-
562
- // Check if this is inside a wrapper component
563
- const sourceCode = context.sourceCode || context.getSourceCode();
564
- // ESLint 9: use sourceCode.getAncestors(node), ESLint 8: use context.getAncestors()
565
- const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : (context.getAncestors ? context.getAncestors() : []);
566
- const componentAncestor = ancestors.find(ancestor =>
567
- ancestor.type === 'FunctionDeclaration' ||
568
- (ancestor.type === 'VariableDeclarator' && ancestor.init &&
569
- (ancestor.init.type === 'ArrowFunctionExpression' || ancestor.init.type === 'FunctionExpression'))
570
- );
571
-
572
- if (componentAncestor) {
573
- let componentName = null;
574
- let componentParams = null;
575
-
576
- if (componentAncestor.type === 'FunctionDeclaration' && componentAncestor.id) {
577
- componentName = componentAncestor.id.name;
578
- componentParams = componentAncestor.params;
579
- } else if (componentAncestor.type === 'VariableDeclarator' && componentAncestor.id.type === 'Identifier') {
580
- componentName = componentAncestor.id.name;
581
- // For arrow functions and function expressions, get params from init
582
- if (componentAncestor.init) {
583
- if (componentAncestor.init.type === 'ArrowFunctionExpression' ||
584
- componentAncestor.init.type === 'FunctionExpression') {
585
- componentParams = componentAncestor.init.params;
586
- }
587
- }
588
- }
589
-
590
- // Check if component accepts pageName as a FUNCTION PARAMETER (not just uses it in JSX)
591
- // This distinguishes wrapper components from legitimate page components
592
- // Wrapper pattern: function Wrapper({ pageName }) { return <PagePermissionGuard pageName={pageName}> }
593
- // Legitimate pattern: function Page() { return <PagePermissionGuard pageName={PAGE_NAMES.PAGE}> }
594
- const acceptsPageNameAsParam = componentParams && componentParams.some(param => {
595
- if (param.type === 'Identifier') {
596
- return param.name === 'pageName';
597
- }
598
- // Handle destructured params: { pageName } or { pageName: pn }
599
- if (param.type === 'ObjectPattern') {
600
- return param.properties.some(prop => {
601
- if (prop.type === 'Property') {
602
- const key = prop.key;
603
- if (key.type === 'Identifier') {
604
- return key.name === 'pageName';
605
- }
606
- }
607
- return false;
608
- });
609
- }
610
- return false;
611
- });
612
-
613
- // Only flag if component accepts pageName as a parameter (wrapper pattern)
614
- // Legitimate page components use constants like PAGE_NAMES.CONTACTS, not accept it as prop
615
- if (acceptsPageNameAsParam) {
616
- context.report({
617
- node,
618
- messageId: 'wrapperComponent',
619
- data: {
620
- componentName: componentName || 'Unknown'
621
- },
622
- suggest: [{
623
- desc: 'Remove wrapper component and use PagePermissionGuard directly in pages',
624
- fix(fixer) {
625
- return null;
626
- }
627
- }]
628
- });
629
- }
630
- }
631
- }
632
- }
633
- };
634
- }
635
- },
636
-
637
- /**
638
- * Disallow wrapper functions around permission hooks
639
- */
640
- 'no-rbac-wrapper-functions': {
641
- meta: {
642
- type: 'problem',
643
- docs: {
644
- description: 'Disallow wrapper functions around pace-core permission hooks. Use hooks directly in components.',
645
- category: 'Security',
646
- recommended: true
647
- },
648
- messages: {
649
- wrapperFunction: "Permission wrapper function '{{functionName}}' detected. Use pace-core hooks (useCan, useResourcePermissions) directly in components instead of wrapping them."
650
- },
651
- hasSuggestions: true
652
- },
653
- create(context) {
654
- const filename = context.getFilename();
655
-
656
- // Exclude pace-core source files - these rules are for consuming apps
657
- if (isPaceCoreSourceFile(filename)) {
658
- return {};
659
- }
660
-
661
- const permissionHookNames = ['useCan', 'useResourcePermissions', 'usePermissions', 'useMultiplePermissions'];
662
- const permissionFunctionNames = ['canCreate', 'canUpdate', 'canDelete', 'canRead', 'can'];
663
-
664
- let hasPaceCoreRBACImport = false;
665
-
666
- return {
667
- ImportDeclaration(node) {
668
- const importSource = node.source.value;
669
- if (importSource === '@jmruthers/pace-core/rbac' ||
670
- importSource.startsWith('@jmruthers/pace-core/rbac/')) {
671
- hasPaceCoreRBACImport = true;
672
- }
673
- },
674
- FunctionDeclaration(node) {
675
- if (!node.id || !hasPaceCoreRBACImport) return;
676
-
677
- const functionName = node.id.name;
678
- if (!functionName) return;
679
-
680
- // Check if function body uses permission hooks/functions
681
- const sourceCode = context.getSourceCode();
682
- const functionText = sourceCode.getText(node.body);
683
-
684
- // Check if function uses permission hooks or functions
685
- const usesPermissionHooks = permissionHookNames.some(hook => functionText.includes(hook));
686
- const usesPermissionFunctions = permissionFunctionNames.some(fn => functionText.includes(fn));
687
-
688
- // Check if function has additional logic beyond permission checking
689
- const hasAdditionalLogic = (
690
- functionText.includes('&&') ||
691
- functionText.includes('||') ||
692
- functionText.includes('if') ||
693
- (functionText.match(/return/g) || []).length > 1 ||
694
- functionText.includes('.find') ||
695
- functionText.includes('.filter') ||
696
- functionText.includes('.map')
697
- );
698
-
699
- // Pattern: Functions that start with 'can' and use permission hooks/functions
700
- // and have additional logic
701
- const isPermissionWrapper = (
702
- (functionName.toLowerCase().startsWith('can') ||
703
- functionName.toLowerCase().includes('permission') ||
704
- functionName.toLowerCase().includes('access')) &&
705
- (usesPermissionHooks || usesPermissionFunctions) &&
706
- hasAdditionalLogic &&
707
- node.params.length > 0
708
- );
709
-
710
- if (isPermissionWrapper) {
711
- context.report({
712
- node: node.id,
713
- messageId: 'wrapperFunction',
714
- data: {
715
- functionName
716
- },
717
- suggest: [{
718
- desc: 'Use permission hooks directly in components',
719
- fix(fixer) {
720
- return null;
721
- }
722
- }]
723
- });
724
- }
725
- },
726
- VariableDeclarator(node) {
727
- if (!hasPaceCoreRBACImport) return;
728
- if (node.id.type !== 'Identifier') return;
729
-
730
- const varName = node.id.name;
731
- if (!varName) return;
732
-
733
- // Only check arrow functions and function expressions
734
- if (!node.init ||
735
- (node.init.type !== 'ArrowFunctionExpression' &&
736
- node.init.type !== 'FunctionExpression')) {
737
- return;
738
- }
739
-
740
- const sourceCode = context.getSourceCode();
741
- const functionText = sourceCode.getText(node.init);
742
-
743
- // Check if function uses permission hooks or functions
744
- const usesPermissionHooks = permissionHookNames.some(hook => functionText.includes(hook));
745
- const usesPermissionFunctions = permissionFunctionNames.some(fn => functionText.includes(fn));
746
-
747
- // Check if function has additional logic beyond permission checking
748
- const hasAdditionalLogic = (
749
- functionText.includes('&&') ||
750
- functionText.includes('||') ||
751
- functionText.includes('if') ||
752
- (functionText.match(/return/g) || []).length > 1 ||
753
- functionText.includes('.find') ||
754
- functionText.includes('.filter') ||
755
- functionText.includes('.map')
756
- );
757
-
758
- // Pattern: Variables that start with 'can' and use permission hooks/functions
759
- // and have additional logic
760
- const isPermissionWrapper = (
761
- (varName.toLowerCase().startsWith('can') ||
762
- varName.toLowerCase().includes('permission') ||
763
- varName.toLowerCase().includes('access')) &&
764
- (usesPermissionHooks || usesPermissionFunctions) &&
765
- hasAdditionalLogic &&
766
- node.init.params && node.init.params.length > 0
767
- );
768
-
769
- if (isPermissionWrapper) {
770
- context.report({
771
- node: node.id,
772
- messageId: 'wrapperFunction',
773
- data: {
774
- functionName: varName
775
- },
776
- suggest: [{
777
- desc: 'Use permission hooks directly in components',
778
- fix(fixer) {
779
- return null;
780
- }
781
- }]
782
- });
783
- }
784
- }
785
- };
786
- }
787
- }
788
- }
789
- };
790
-