@jmruthers/pace-core 0.5.185 → 0.5.186

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 (205) hide show
  1. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +4 -2
  2. package/dist/{chunk-STTZQK2I.js → chunk-DAGICKHT.js} +7 -5
  3. package/dist/chunk-DAGICKHT.js.map +1 -0
  4. package/dist/{chunk-AISXLWGZ.js → chunk-GRIQLQ52.js} +2 -2
  5. package/dist/{chunk-HC67NW5K.js → chunk-HDCUMOOI.js} +125 -47
  6. package/dist/chunk-HDCUMOOI.js.map +1 -0
  7. package/dist/{chunk-OKI34GZD.js → chunk-OALXJH4Y.js} +2 -2
  8. package/dist/{chunk-MX3EIJGQ.js → chunk-TC7D3CR3.js} +86 -7
  9. package/dist/chunk-TC7D3CR3.js.map +1 -0
  10. package/dist/{chunk-IXSNYUCT.js → chunk-UQWSHFVX.js} +1 -1
  11. package/dist/chunk-UQWSHFVX.js.map +1 -0
  12. package/dist/components.d.ts +2 -2
  13. package/dist/components.js +3 -3
  14. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  15. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-PRTSLxKx.d.ts} +3 -0
  16. package/dist/hooks.d.ts +49 -5
  17. package/dist/hooks.js +6 -4
  18. package/dist/hooks.js.map +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +9 -8
  21. package/dist/index.js.map +1 -1
  22. package/dist/rbac/index.d.ts +1 -1
  23. package/dist/rbac/index.js +2 -2
  24. package/dist/types.d.ts +2 -2
  25. package/dist/types.js +1 -1
  26. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +2 -2
  27. package/dist/utils.d.ts +1 -1
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/Logger.md +1 -1
  32. package/docs/api/classes/MissingUserContextError.md +1 -1
  33. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  34. package/docs/api/classes/PermissionDeniedError.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +2 -2
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +2 -2
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/enums/LogLevel.md +1 -1
  44. package/docs/api/enums/RBACErrorCode.md +1 -1
  45. package/docs/api/enums/RPCFunction.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/BadgeProps.md +1 -1
  48. package/docs/api/interfaces/ButtonProps.md +1 -1
  49. package/docs/api/interfaces/CalendarProps.md +1 -1
  50. package/docs/api/interfaces/CardProps.md +1 -1
  51. package/docs/api/interfaces/ColorPalette.md +1 -1
  52. package/docs/api/interfaces/ColorShade.md +1 -1
  53. package/docs/api/interfaces/ComplianceResult.md +1 -1
  54. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  55. package/docs/api/interfaces/DataRecord.md +1 -1
  56. package/docs/api/interfaces/DataTableAction.md +1 -1
  57. package/docs/api/interfaces/DataTableColumn.md +1 -1
  58. package/docs/api/interfaces/DataTableProps.md +1 -1
  59. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  60. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  61. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  62. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  63. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  64. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  65. package/docs/api/interfaces/ExportColumn.md +1 -1
  66. package/docs/api/interfaces/ExportOptions.md +1 -1
  67. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  68. package/docs/api/interfaces/FileMetadata.md +1 -1
  69. package/docs/api/interfaces/FileReference.md +1 -1
  70. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  71. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  72. package/docs/api/interfaces/FileUploadProps.md +36 -14
  73. package/docs/api/interfaces/FooterProps.md +1 -1
  74. package/docs/api/interfaces/FormFieldProps.md +1 -1
  75. package/docs/api/interfaces/FormProps.md +1 -1
  76. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoggerConfig.md +1 -1
  81. package/docs/api/interfaces/LoginFormProps.md +1 -1
  82. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  83. package/docs/api/interfaces/NavigationContextType.md +1 -1
  84. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  85. package/docs/api/interfaces/NavigationItem.md +1 -1
  86. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  87. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  88. package/docs/api/interfaces/Organisation.md +1 -1
  89. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  90. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  91. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  92. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  93. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  94. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  95. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  96. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  97. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  98. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  99. package/docs/api/interfaces/PaletteData.md +1 -1
  100. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  101. package/docs/api/interfaces/ProgressProps.md +1 -1
  102. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/QuickFix.md +1 -1
  107. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  108. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  109. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  110. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  111. package/docs/api/interfaces/RBACConfig.md +2 -2
  112. package/docs/api/interfaces/RBACContext.md +1 -1
  113. package/docs/api/interfaces/RBACLogger.md +1 -1
  114. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  115. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  116. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  117. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  118. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  119. package/docs/api/interfaces/RBACResult.md +1 -1
  120. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  121. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  122. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  123. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  124. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  125. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  126. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  127. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  128. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  129. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  130. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  131. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  132. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  134. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  135. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  136. package/docs/api/interfaces/RouteConfig.md +1 -1
  137. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  138. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  139. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  140. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  141. package/docs/api/interfaces/SetupIssue.md +1 -1
  142. package/docs/api/interfaces/StorageConfig.md +1 -1
  143. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  144. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  145. package/docs/api/interfaces/StorageListOptions.md +1 -1
  146. package/docs/api/interfaces/StorageListResult.md +1 -1
  147. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  148. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  149. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  150. package/docs/api/interfaces/StyleImport.md +1 -1
  151. package/docs/api/interfaces/SwitchProps.md +1 -1
  152. package/docs/api/interfaces/TabsContentProps.md +1 -1
  153. package/docs/api/interfaces/TabsListProps.md +1 -1
  154. package/docs/api/interfaces/TabsProps.md +1 -1
  155. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  156. package/docs/api/interfaces/TextareaProps.md +1 -1
  157. package/docs/api/interfaces/ToastActionElement.md +1 -1
  158. package/docs/api/interfaces/ToastProps.md +1 -1
  159. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  160. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  161. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  162. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  163. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  164. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  165. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  166. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  167. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  168. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  169. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  170. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  171. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  172. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  173. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  174. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  175. package/docs/api/interfaces/UserEventAccess.md +1 -1
  176. package/docs/api/interfaces/UserMenuProps.md +1 -1
  177. package/docs/api/interfaces/UserProfile.md +1 -1
  178. package/docs/api/modules.md +17 -17
  179. package/docs/api-reference/components.md +26 -12
  180. package/docs/implementation-guides/file-reference-system.md +24 -2
  181. package/docs/implementation-guides/file-upload-storage.md +9 -1
  182. package/package.json +1 -1
  183. package/scripts/check-pace-core-compliance.js +512 -0
  184. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  185. package/src/components/FileUpload/FileUpload.tsx +7 -1
  186. package/src/components/Header/Header.tsx +2 -5
  187. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  188. package/src/hooks/index.ts +3 -0
  189. package/src/hooks/useFileReference.test.ts +1 -0
  190. package/src/hooks/usePreventTabReload.ts +106 -0
  191. package/src/hooks/useSecureDataAccess.ts +2 -2
  192. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  193. package/src/styles/core.css +5 -5
  194. package/src/types/database.generated.ts +63 -9
  195. package/src/types/file-reference.ts +3 -0
  196. package/src/utils/file-reference/__tests__/file-reference.test.ts +58 -4
  197. package/src/utils/file-reference/index.ts +12 -2
  198. package/src/utils/security/secureDataAccess.ts +1 -1
  199. package/src/utils/storage/helpers.ts +68 -0
  200. package/dist/chunk-HC67NW5K.js.map +0 -1
  201. package/dist/chunk-IXSNYUCT.js.map +0 -1
  202. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  203. package/dist/chunk-STTZQK2I.js.map +0 -1
  204. /package/dist/{chunk-AISXLWGZ.js.map → chunk-GRIQLQ52.js.map} +0 -0
  205. /package/dist/{chunk-OKI34GZD.js.map → chunk-OALXJH4Y.js.map} +0 -0
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Static Analysis Script for pace-core Compliance
5
+ * @package @jmruthers/pace-core
6
+ * @module Scripts/check-pace-core-compliance
7
+ *
8
+ * Scans a consuming app's codebase to check compliance with pace-core usage.
9
+ * Generates a report of violations and suggestions.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // ANSI color codes for terminal output
16
+ const colors = {
17
+ reset: '\x1b[0m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ blue: '\x1b[34m',
22
+ cyan: '\x1b[36m',
23
+ bold: '\x1b[1m'
24
+ };
25
+
26
+ // Load manifest
27
+ function loadManifest() {
28
+ const manifestPath = path.join(__dirname, '../core-usage-manifest.json');
29
+ if (!fs.existsSync(manifestPath)) {
30
+ console.error(`${colors.red}Error: core-usage-manifest.json not found at ${manifestPath}${colors.reset}`);
31
+ process.exit(1);
32
+ }
33
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
34
+ }
35
+
36
+ // Find project root (look for package.json, going up from current dir or script location)
37
+ function findProjectRoot(startDir = process.cwd()) {
38
+ let current = path.resolve(startDir);
39
+ while (current !== path.dirname(current)) {
40
+ if (fs.existsSync(path.join(current, 'package.json'))) {
41
+ return current;
42
+ }
43
+ current = path.dirname(current);
44
+ }
45
+ return startDir;
46
+ }
47
+
48
+ // Scan file for violations
49
+ function scanFile(filePath, manifest) {
50
+ const violations = {
51
+ restrictedImports: [],
52
+ duplicateComponents: [],
53
+ duplicateHooks: [],
54
+ duplicateUtils: [],
55
+ suggestions: [],
56
+ customAuthCode: [],
57
+ duplicateConfig: [],
58
+ unprotectedPages: [],
59
+ directSupabaseAuth: []
60
+ };
61
+
62
+ const content = fs.readFileSync(filePath, 'utf8');
63
+ const relativePath = path.relative(process.cwd(), filePath);
64
+
65
+ // Check for restricted imports
66
+ manifest.restrictedImports.forEach(({ module, reason }) => {
67
+ const importPattern = new RegExp(`from\\s+['"]${module.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
68
+ if (importPattern.test(content)) {
69
+ violations.restrictedImports.push({
70
+ module,
71
+ reason,
72
+ file: relativePath,
73
+ line: getLineNumber(content, content.match(importPattern)[0])
74
+ });
75
+ }
76
+
77
+ // Also check for @radix-ui/* pattern
78
+ if (module.startsWith('@radix-ui/')) {
79
+ const radixPattern = /from\s+['"]@radix-ui\/[^'"]+['"]/g;
80
+ const matches = content.match(radixPattern);
81
+ if (matches) {
82
+ matches.forEach(match => {
83
+ const matchedModule = match.match(/['"]([^'"]+)['"]/)[1];
84
+ if (!manifest.restrictedImports.find(ri => ri.module === matchedModule)) {
85
+ violations.restrictedImports.push({
86
+ module: matchedModule,
87
+ reason: 'Use pace-core component instead of direct Radix UI import',
88
+ file: relativePath,
89
+ line: getLineNumber(content, match)
90
+ });
91
+ }
92
+ });
93
+ }
94
+ }
95
+ });
96
+
97
+ // Check for duplicate component names
98
+ const filename = path.basename(filePath);
99
+ const componentName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
100
+
101
+ if (manifest.components.includes(componentName)) {
102
+ // Check if this file exports a component
103
+ if (content.match(/export\s+(default\s+)?(function|const|class)\s+(\w+)?/)) {
104
+ violations.duplicateComponents.push({
105
+ component: componentName,
106
+ file: relativePath
107
+ });
108
+ }
109
+ }
110
+
111
+ // Check for duplicate hook names
112
+ if (filename.startsWith('use') && filename.endsWith('.ts') || filename.endsWith('.tsx')) {
113
+ const hookName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
114
+ if (manifest.hooks.includes(hookName)) {
115
+ if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
116
+ violations.duplicateHooks.push({
117
+ hook: hookName,
118
+ file: relativePath
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ // Check for duplicate util names
125
+ const utilName = filename.replace(/\.(ts|js)$/, '').replace(/\.(test|spec)$/, '');
126
+ if (manifest.utils.includes(utilName)) {
127
+ if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
128
+ violations.duplicateUtils.push({
129
+ util: utilName,
130
+ file: relativePath
131
+ });
132
+ }
133
+ }
134
+
135
+ // Check for native HTML elements that should use pace-core components
136
+ const nativeElementPatterns = {
137
+ '<button': { suggestion: 'Use Button from @jmruthers/pace-core' },
138
+ '<input': { suggestion: 'Use Input from @jmruthers/pace-core' },
139
+ '<textarea': { suggestion: 'Use Textarea from @jmruthers/pace-core' },
140
+ '<label': { suggestion: 'Use Label from @jmruthers/pace-core' }
141
+ };
142
+
143
+ Object.entries(nativeElementPatterns).forEach(([pattern, { suggestion }]) => {
144
+ if (content.includes(pattern) && !content.includes('from \'@jmruthers/pace-core\'')) {
145
+ violations.suggestions.push({
146
+ type: 'native-element',
147
+ suggestion,
148
+ file: relativePath,
149
+ pattern
150
+ });
151
+ }
152
+ });
153
+
154
+ // ============================================
155
+ // RBAC/Auth Compliance Checks
156
+ // ============================================
157
+
158
+ // Check for custom auth/rbac/permission code that doesn't import from pace-core
159
+ const authRbacPatterns = [
160
+ // Custom auth hooks
161
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAuth\s*[=\(]/g, name: 'useAuth', type: 'hook' },
162
+ { pattern: /export\s+(default\s+)?(function|const)\s+useLogin\s*[=\(]/g, name: 'useLogin', type: 'hook' },
163
+ { pattern: /export\s+(default\s+)?(function|const)\s+useLogout\s*[=\(]/g, name: 'useLogout', type: 'hook' },
164
+ { pattern: /export\s+(default\s+)?(function|const)\s+useSession\s*[=\(]/g, name: 'useSession', type: 'hook' },
165
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUser\s*[=\(]/g, name: 'useUser', type: 'hook' },
166
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAuthentication\s*[=\(]/g, name: 'useAuthentication', type: 'hook' },
167
+ // Custom RBAC hooks
168
+ { pattern: /export\s+(default\s+)?(function|const)\s+usePermissions\s*[=\(]/g, name: 'usePermissions', type: 'hook' },
169
+ { pattern: /export\s+(default\s+)?(function|const)\s+useCan\s*[=\(]/g, name: 'useCan', type: 'hook' },
170
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAccessLevel\s*[=\(]/g, name: 'useAccessLevel', type: 'hook' },
171
+ { pattern: /export\s+(default\s+)?(function|const)\s+useRole\s*[=\(]/g, name: 'useRole', type: 'hook' },
172
+ // Custom RBAC components
173
+ { pattern: /export\s+(default\s+)?(function|const)\s+PermissionGuard\s*[=\(]/g, name: 'PermissionGuard', type: 'component' },
174
+ { pattern: /export\s+(default\s+)?(function|const)\s+AuthGuard\s*[=\(]/g, name: 'AuthGuard', type: 'component' },
175
+ { pattern: /export\s+(default\s+)?(function|const)\s+RoleGuard\s*[=\(]/g, name: 'RoleGuard', type: 'component' },
176
+ { pattern: /export\s+(default\s+)?(function|const)\s+AccessGuard\s*[=\(]/g, name: 'AccessGuard', type: 'component' },
177
+ // Custom permission utilities
178
+ { pattern: /export\s+(default\s+)?(function|const)\s+checkPermission\s*[=\(]/g, name: 'checkPermission', type: 'util' },
179
+ { pattern: /export\s+(default\s+)?(function|const)\s+hasPermission\s*[=\(]/g, name: 'hasPermission', type: 'util' },
180
+ { pattern: /export\s+(default\s+)?(function|const)\s+hasAccess\s*[=\(]/g, name: 'hasAccess', type: 'util' },
181
+ { pattern: /export\s+(default\s+)?(function|const)\s+canAccess\s*[=\(]/g, name: 'canAccess', type: 'util' },
182
+ { pattern: /export\s+(default\s+)?(function|const)\s+isPermitted\s*[=\(]/g, name: 'isPermitted', type: 'util' }
183
+ ];
184
+
185
+ // Check if file imports from pace-core for auth/rbac
186
+ const hasPaceCoreImport = /from\s+['"]@jmruthers\/pace-core/.test(content) ||
187
+ /from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
188
+ /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
189
+
190
+ authRbacPatterns.forEach(({ pattern, name, type }) => {
191
+ if (pattern.test(content) && !hasPaceCoreImport) {
192
+ violations.customAuthCode.push({
193
+ name,
194
+ type,
195
+ file: relativePath,
196
+ line: getLineNumber(content, content.match(pattern)[0]),
197
+ reason: `Custom ${type} '${name}' detected. Use pace-core's ${name} instead.`
198
+ });
199
+ }
200
+ });
201
+
202
+ // Check for duplicate Supabase client configurations
203
+ const supabaseCreateClientPattern = /createClient\s*\(/g;
204
+ const supabaseCreateClientMatches = content.match(supabaseCreateClientPattern);
205
+ if (supabaseCreateClientMatches && supabaseCreateClientMatches.length > 1) {
206
+ violations.duplicateConfig.push({
207
+ type: 'supabase-client',
208
+ file: relativePath,
209
+ count: supabaseCreateClientMatches.length,
210
+ reason: `Multiple Supabase client instantiations found (${supabaseCreateClientMatches.length}). Consolidate to a single client configuration.`
211
+ });
212
+ }
213
+
214
+ // Check for Supabase URL/key configuration in multiple places
215
+ const supabaseUrlPattern = /(SUPABASE_URL|VITE_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_URL|REACT_APP_SUPABASE_URL)/g;
216
+ const supabaseKeyPattern = /(SUPABASE_ANON_KEY|VITE_SUPABASE_ANON_KEY|NEXT_PUBLIC_SUPABASE_ANON_KEY|REACT_APP_SUPABASE_ANON_KEY)/g;
217
+ const urlMatches = content.match(supabaseUrlPattern);
218
+ const keyMatches = content.match(supabaseKeyPattern);
219
+
220
+ if ((urlMatches && urlMatches.length > 2) || (keyMatches && keyMatches.length > 2)) {
221
+ violations.duplicateConfig.push({
222
+ type: 'supabase-env',
223
+ file: relativePath,
224
+ reason: 'Supabase environment variables referenced multiple times. Consider centralizing configuration.'
225
+ });
226
+ }
227
+
228
+ // Check for unprotected pages/routes
229
+ // Look for route definitions without PagePermissionGuard
230
+ const routePatterns = [
231
+ /<Route\s+path=["'][^"']+["']/g,
232
+ /<Route\s+element\s*=/g,
233
+ /createBrowserRouter\s*\(/g,
234
+ /createRoutesFromElements/g
235
+ ];
236
+
237
+ const isRouteFile = routePatterns.some(pattern => pattern.test(content));
238
+ const hasPagePermissionGuard = /PagePermissionGuard/.test(content) ||
239
+ /from\s+['"]@jmruthers\/pace-core\/rbac['"]/.test(content);
240
+
241
+ if (isRouteFile && !hasPagePermissionGuard && !relativePath.includes('test') && !relativePath.includes('spec')) {
242
+ violations.unprotectedPages.push({
243
+ file: relativePath,
244
+ reason: 'Route file found without PagePermissionGuard. All routes should be protected with PagePermissionGuard from pace-core.'
245
+ });
246
+ }
247
+
248
+ // Check for direct Supabase auth usage (should use UnifiedAuthProvider)
249
+ const directSupabaseAuthPatterns = [
250
+ /\.auth\.signIn/g,
251
+ /\.auth\.signUp/g,
252
+ /\.auth\.signOut/g,
253
+ /\.auth\.getSession/g,
254
+ /\.auth\.getUser/g,
255
+ /supabase\.auth\./g
256
+ ];
257
+
258
+ const hasUnifiedAuthImport = /UnifiedAuthProvider/.test(content) ||
259
+ /useUnifiedAuth/.test(content) ||
260
+ /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
261
+
262
+ directSupabaseAuthPatterns.forEach(pattern => {
263
+ if (pattern.test(content) && !hasUnifiedAuthImport && !hasPaceCoreImport) {
264
+ violations.directSupabaseAuth.push({
265
+ file: relativePath,
266
+ line: getLineNumber(content, content.match(pattern)[0]),
267
+ reason: 'Direct Supabase auth usage detected. Use UnifiedAuthProvider and useUnifiedAuth from pace-core instead.'
268
+ });
269
+ }
270
+ });
271
+
272
+ return violations;
273
+ }
274
+
275
+ // Get line number for a match
276
+ function getLineNumber(content, match) {
277
+ const lines = content.substring(0, content.indexOf(match)).split('\n');
278
+ return lines.length;
279
+ }
280
+
281
+ // Generate report
282
+ function generateReport(allViolations, manifest) {
283
+ const totalRestricted = allViolations.restrictedImports.length;
284
+ const totalDuplicates =
285
+ allViolations.duplicateComponents.length +
286
+ allViolations.duplicateHooks.length +
287
+ allViolations.duplicateUtils.length;
288
+ const totalSuggestions = allViolations.suggestions.length;
289
+ const totalRbacAuth =
290
+ allViolations.customAuthCode.length +
291
+ allViolations.duplicateConfig.length +
292
+ allViolations.unprotectedPages.length +
293
+ allViolations.directSupabaseAuth.length;
294
+ const totalIssues = totalRestricted + totalDuplicates + totalSuggestions + totalRbacAuth;
295
+
296
+ console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
297
+ console.log(`${colors.bold}${colors.cyan} pace-core Compliance Report${colors.reset}`);
298
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
299
+
300
+ // Restricted Imports
301
+ if (totalRestricted > 0) {
302
+ console.log(`${colors.red}${colors.bold}❌ Restricted Imports Found: ${totalRestricted}${colors.reset}\n`);
303
+ allViolations.restrictedImports.forEach(({ module, reason, file, line }) => {
304
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
305
+ console.log(` Import: ${colors.cyan}${module}${colors.reset}`);
306
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
307
+ });
308
+ } else {
309
+ console.log(`${colors.green}✅ No restricted imports found${colors.reset}\n`);
310
+ }
311
+
312
+ // Duplicate Components
313
+ if (allViolations.duplicateComponents.length > 0) {
314
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Components Found: ${allViolations.duplicateComponents.length}${colors.reset}\n`);
315
+ allViolations.duplicateComponents.forEach(({ component, file }) => {
316
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
317
+ console.log(` Component '${colors.cyan}${component}${colors.reset}' conflicts with pace-core component`);
318
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${component}' from '@jmruthers/pace-core' instead\n`);
319
+ });
320
+ }
321
+
322
+ // Duplicate Hooks
323
+ if (allViolations.duplicateHooks.length > 0) {
324
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Hooks Found: ${allViolations.duplicateHooks.length}${colors.reset}\n`);
325
+ allViolations.duplicateHooks.forEach(({ hook, file }) => {
326
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
327
+ console.log(` Hook '${colors.cyan}${hook}${colors.reset}' conflicts with pace-core hook`);
328
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${hook}' from '@jmruthers/pace-core' instead\n`);
329
+ });
330
+ }
331
+
332
+ // Duplicate Utils
333
+ if (allViolations.duplicateUtils.length > 0) {
334
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Utils Found: ${allViolations.duplicateUtils.length}${colors.reset}\n`);
335
+ allViolations.duplicateUtils.forEach(({ util, file }) => {
336
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
337
+ console.log(` Util '${colors.cyan}${util}${colors.reset}' conflicts with pace-core util`);
338
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${util}' from '@jmruthers/pace-core' instead\n`);
339
+ });
340
+ }
341
+
342
+ // Suggestions
343
+ if (totalSuggestions > 0) {
344
+ console.log(`${colors.yellow}${colors.bold}💡 Suggestions: ${totalSuggestions}${colors.reset}\n`);
345
+ const grouped = {};
346
+ allViolations.suggestions.forEach(s => {
347
+ if (!grouped[s.file]) grouped[s.file] = [];
348
+ grouped[s.file].push(s);
349
+ });
350
+ Object.entries(grouped).forEach(([file, suggestions]) => {
351
+ console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
352
+ suggestions.forEach(s => {
353
+ console.log(` ${s.suggestion}\n`);
354
+ });
355
+ });
356
+ }
357
+
358
+ // RBAC/Auth Compliance Section
359
+ if (totalRbacAuth > 0) {
360
+ console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
361
+ console.log(`${colors.bold}${colors.cyan} RBAC/Auth Compliance${colors.reset}`);
362
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
363
+
364
+ // Custom Auth/RBAC Code
365
+ if (allViolations.customAuthCode.length > 0) {
366
+ console.log(`${colors.red}${colors.bold}❌ Custom Auth/RBAC Code Found: ${allViolations.customAuthCode.length}${colors.reset}\n`);
367
+ allViolations.customAuthCode.forEach(({ name, type, file, line, reason }) => {
368
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
369
+ console.log(` ${type}: ${colors.cyan}${name}${colors.reset}`);
370
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
371
+ });
372
+ }
373
+
374
+ // Duplicate Configurations
375
+ if (allViolations.duplicateConfig.length > 0) {
376
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Configurations Found: ${allViolations.duplicateConfig.length}${colors.reset}\n`);
377
+ allViolations.duplicateConfig.forEach(({ type, file, count, reason }) => {
378
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
379
+ console.log(` Type: ${colors.cyan}${type}${colors.reset}${count ? ` (${count} instances)` : ''}`);
380
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
381
+ });
382
+ }
383
+
384
+ // Unprotected Pages
385
+ if (allViolations.unprotectedPages.length > 0) {
386
+ console.log(`${colors.red}${colors.bold}❌ Unprotected Pages Found: ${allViolations.unprotectedPages.length}${colors.reset}\n`);
387
+ allViolations.unprotectedPages.forEach(({ file, reason }) => {
388
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
389
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
390
+ });
391
+ }
392
+
393
+ // Direct Supabase Auth Usage
394
+ if (allViolations.directSupabaseAuth.length > 0) {
395
+ console.log(`${colors.red}${colors.bold}❌ Direct Supabase Auth Usage Found: ${allViolations.directSupabaseAuth.length}${colors.reset}\n`);
396
+ allViolations.directSupabaseAuth.forEach(({ file, line, reason }) => {
397
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
398
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
399
+ });
400
+ }
401
+ } else {
402
+ console.log(`\n${colors.green}✅ RBAC/Auth compliance: All checks passed${colors.reset}\n`);
403
+ }
404
+
405
+ // Summary
406
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
407
+ console.log(`${colors.bold}Summary:${colors.reset}`);
408
+ console.log(` Total Issues: ${totalIssues > 0 ? colors.red : colors.green}${totalIssues}${colors.reset}`);
409
+ console.log(` - Restricted Imports: ${totalRestricted > 0 ? colors.red : colors.green}${totalRestricted}${colors.reset}`);
410
+ console.log(` - Duplicate Components/Hooks/Utils: ${totalDuplicates > 0 ? colors.red : colors.green}${totalDuplicates}${colors.reset}`);
411
+ console.log(` - Suggestions: ${colors.yellow}${totalSuggestions}${colors.reset}`);
412
+ console.log(` - RBAC/Auth Issues: ${totalRbacAuth > 0 ? colors.red : colors.green}${totalRbacAuth}${colors.reset}`);
413
+
414
+ if (totalIssues === 0) {
415
+ console.log(`\n${colors.green}${colors.bold}✅ Excellent! Your codebase is fully compliant with pace-core standards.${colors.reset}\n`);
416
+ return 0;
417
+ } else {
418
+ console.log(`\n${colors.yellow}${colors.bold}⚠️ Please review the issues above and migrate to pace-core components/hooks/utils.${colors.reset}\n`);
419
+ return 1;
420
+ }
421
+ }
422
+
423
+ // Recursively find source files
424
+ function findSourceFiles(dir, fileList = []) {
425
+ const ignoreDirs = ['node_modules', 'dist', 'build', '.next', 'coverage', '__tests__', '__mocks__'];
426
+ const ignoreFiles = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
427
+ const sourceExtensions = /\.(ts|tsx|js|jsx)$/;
428
+
429
+ try {
430
+ const items = fs.readdirSync(dir);
431
+
432
+ items.forEach(item => {
433
+ const fullPath = path.join(dir, item);
434
+ const stat = fs.statSync(fullPath);
435
+
436
+ if (stat.isDirectory()) {
437
+ if (!ignoreDirs.includes(item) && !item.startsWith('.')) {
438
+ findSourceFiles(fullPath, fileList);
439
+ }
440
+ } else if (stat.isFile()) {
441
+ if (sourceExtensions.test(item) && !ignoreFiles.test(item)) {
442
+ fileList.push(fullPath);
443
+ }
444
+ }
445
+ });
446
+ } catch (error) {
447
+ // Skip directories we can't read
448
+ }
449
+
450
+ return fileList;
451
+ }
452
+
453
+ // Main function
454
+ function main() {
455
+ const manifest = loadManifest();
456
+ const projectRoot = findProjectRoot();
457
+
458
+ console.log(`${colors.cyan}Scanning project at: ${projectRoot}${colors.reset}`);
459
+
460
+ // Find all TypeScript/JavaScript files (excluding node_modules, dist, etc.)
461
+ const files = findSourceFiles(projectRoot);
462
+
463
+ console.log(`Found ${files.length} files to scan...\n`);
464
+
465
+ // Scan all files
466
+ const allViolations = {
467
+ restrictedImports: [],
468
+ duplicateComponents: [],
469
+ duplicateHooks: [],
470
+ duplicateUtils: [],
471
+ suggestions: [],
472
+ customAuthCode: [],
473
+ duplicateConfig: [],
474
+ unprotectedPages: [],
475
+ directSupabaseAuth: []
476
+ };
477
+
478
+ files.forEach(file => {
479
+ try {
480
+ const violations = scanFile(file, manifest);
481
+ allViolations.restrictedImports.push(...violations.restrictedImports);
482
+ allViolations.duplicateComponents.push(...violations.duplicateComponents);
483
+ allViolations.duplicateHooks.push(...violations.duplicateHooks);
484
+ allViolations.duplicateUtils.push(...violations.duplicateUtils);
485
+ allViolations.suggestions.push(...violations.suggestions);
486
+ allViolations.customAuthCode.push(...violations.customAuthCode);
487
+ allViolations.duplicateConfig.push(...violations.duplicateConfig);
488
+ allViolations.unprotectedPages.push(...violations.unprotectedPages);
489
+ allViolations.directSupabaseAuth.push(...violations.directSupabaseAuth);
490
+ } catch (error) {
491
+ console.error(`${colors.red}Error scanning ${file}: ${error.message}${colors.reset}`);
492
+ }
493
+ });
494
+
495
+ // Generate and display report
496
+ const exitCode = generateReport(allViolations, manifest);
497
+ process.exit(exitCode);
498
+ }
499
+
500
+ // Run if called directly
501
+ if (require.main === module) {
502
+ try {
503
+ main();
504
+ } catch (error) {
505
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
506
+ console.error(error.stack);
507
+ process.exit(1);
508
+ }
509
+ }
510
+
511
+ module.exports = { main, scanFile, generateReport };
512
+
@@ -71,6 +71,8 @@ describe('[component] FileUpload', () => {
71
71
  organisation_id: 'org-1',
72
72
  app_id: 'app-123',
73
73
  category: FileCategory.PROFILE_PHOTOS,
74
+ folder: 'profile_photos',
75
+ pageContext: 'test',
74
76
  accept: 'image/*',
75
77
  } as const;
76
78
 
@@ -22,7 +22,9 @@ export interface FileUploadProps {
22
22
  organisation_id: string;
23
23
  app_id?: string; // Optional - will be resolved from app name if not provided
24
24
  category: FileCategory;
25
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
25
26
  pageContext: string; // The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
27
+ event_id?: string; // Optional event ID for event-scoped permission checks (required for event-based apps)
26
28
  accept?: string;
27
29
  maxSize?: number;
28
30
  multiple?: boolean;
@@ -51,7 +53,9 @@ export function FileUpload({
51
53
  organisation_id,
52
54
  app_id,
53
55
  category,
56
+ folder,
54
57
  pageContext,
58
+ event_id,
55
59
  accept = '*/*',
56
60
  maxSize = 10 * 1024 * 1024, // 10MB default
57
61
  multiple = false,
@@ -286,7 +290,9 @@ export function FileUpload({
286
290
  organisation_id,
287
291
  app_id: resolvedAppId ? assertAppId(resolvedAppId) : assertAppId(''),
288
292
  category,
293
+ folder,
289
294
  pageContext,
295
+ event_id,
290
296
  is_public: isPublic
291
297
  }, file);
292
298
 
@@ -384,7 +390,7 @@ export function FileUpload({
384
390
  onUploadError?.(errorMessage, file);
385
391
  }
386
392
  }
387
- }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
393
+ }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, folder, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
388
394
 
389
395
  const handleDragOver = useCallback((e: React.DragEvent) => {
390
396
  e.preventDefault();
@@ -260,7 +260,7 @@ export function Header({
260
260
  "w-full border-b border-main-200 h-16 shadow-sm bg-main-100 ",
261
261
  className
262
262
  )} role="banner">
263
- <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto grid grid-cols-[auto_auto_1fr_auto] gap-4 h-full items-center">
263
+ <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto flex items-center gap-4 h-full">
264
264
  {/* Logo */}
265
265
  {logo ? (
266
266
  logoHref ? (
@@ -318,7 +318,7 @@ export function Header({
318
318
 
319
319
 
320
320
  {/* Right side: Organisation Selector, Event Selector, Actions, and User Menu */}
321
- <div className="flex items-center gap-4 justify-end">
321
+ <div className="flex items-center gap-4 ml-auto">
322
322
  {/* Organisation Selector */}
323
323
  {showOrgSelector ? (
324
324
  <OrganisationSelector
@@ -337,10 +337,7 @@ export function Header({
337
337
  data-testid="event-selector"
338
338
  />
339
339
  ) : null}
340
- </div>
341
340
 
342
- {/* Custom Actions and User Menu */}
343
- <div className="flex items-center gap-4">
344
341
  {/* Custom Actions */}
345
342
  {actions}
346
343