@jmruthers/pace-core 0.6.7 → 0.6.8

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 (100) hide show
  1. package/audit-tool/00-dependencies.cjs +215 -9
  2. package/audit-tool/audits/02-project-structure.cjs +3 -18
  3. package/audit-tool/audits/03-architecture.cjs +34 -6
  4. package/audit-tool/audits/06-security-rbac.cjs +10 -0
  5. package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
  6. package/audit-tool/index.cjs +23 -19
  7. package/audit-tool/utils/report-utils.cjs +141 -2
  8. package/dist/{DataTable-7PMH7XN7.js → DataTable-6RMSCQJ6.js} +5 -5
  9. package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
  10. package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
  11. package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
  12. package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
  13. package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
  14. package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
  15. package/dist/{chunk-JGWDVX64.js → chunk-EURB7QFZ.js} +123 -53
  16. package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
  17. package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
  18. package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
  19. package/dist/{chunk-Q7Q7V5NV.js → chunk-NKHKXPI4.js} +7 -7
  20. package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
  21. package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
  22. package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
  23. package/dist/components.d.ts +1 -1
  24. package/dist/components.js +12 -12
  25. package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
  26. package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
  27. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
  28. package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +15 -15
  32. package/dist/providers.js +2 -2
  33. package/dist/rbac/index.d.ts +1 -1
  34. package/dist/rbac/index.js +6 -6
  35. package/dist/theming/runtime.d.ts +48 -1
  36. package/dist/theming/runtime.js +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/utils.js +1 -1
  39. package/docs/api/modules.md +63 -14
  40. package/docs/getting-started/dependencies.md +23 -0
  41. package/docs/implementation-guides/app-layout.md +1 -1
  42. package/docs/implementation-guides/data-tables.md +1 -1
  43. package/docs/standards/1-pace-core-compliance-standards.md +38 -1
  44. package/eslint-config-pace-core.cjs +30 -11
  45. package/package.json +45 -15
  46. package/scripts/eslint-audit.cjs +123 -0
  47. package/scripts/install-eslint-config.cjs +67 -2
  48. package/scripts/validate-dependencies.cjs +248 -0
  49. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
  50. package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
  51. package/src/components/AddressField/AddressField.tsx +26 -1
  52. package/src/components/Alert/Alert.test.tsx +86 -22
  53. package/src/components/Alert/Alert.tsx +19 -11
  54. package/src/components/Badge/Badge.tsx +1 -1
  55. package/src/components/Checkbox/Checkbox.test.tsx +2 -1
  56. package/src/components/ContextSelector/ContextSelector.tsx +39 -41
  57. package/src/components/DataTable/DataTable.tsx +1 -19
  58. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
  59. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
  60. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
  61. package/src/components/DataTable/components/EmptyState.tsx +1 -1
  62. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
  63. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
  64. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
  65. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
  66. package/src/components/FileUpload/FileUpload.test.tsx +22 -31
  67. package/src/components/FileUpload/FileUpload.tsx +29 -0
  68. package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
  69. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
  70. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
  71. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
  72. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
  73. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
  74. package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
  75. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
  76. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
  77. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
  78. package/src/hooks/public/usePublicRouteParams.ts +8 -4
  79. package/src/hooks/useAddressAutocomplete.test.ts +18 -18
  80. package/src/hooks/useEventTheme.ts +5 -1
  81. package/src/hooks/useFileUrl.ts +52 -8
  82. package/src/hooks/useOrganisationSecurity.test.ts +2 -1
  83. package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
  84. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
  85. package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
  86. package/src/rbac/api.test.ts +104 -0
  87. package/src/rbac/engine.ts +1 -1
  88. package/src/rbac/hooks/useCan.test.ts +2 -2
  89. package/src/rbac/secureClient.ts +1 -1
  90. package/src/rbac/types/functions.ts +1 -1
  91. package/src/theming/__tests__/parseEventColours.test.ts +117 -8
  92. package/src/theming/parseEventColours.ts +56 -2
  93. package/src/types/supabase.ts +2 -3
  94. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
  95. package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
  96. package/src/utils/formatting/formatDate.test.ts +3 -2
  97. package/src/utils/formatting/formatDateTime.test.ts +2 -2
  98. package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
  99. package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
  100. package/src/utils/storage/helpers.test.ts +69 -3
@@ -57,12 +57,21 @@ export function useFileUrl(
57
57
  const [isLoading, setIsLoading] = useState<boolean>(false);
58
58
  const [error, setError] = useState<Error | null>(null);
59
59
  const fileReferenceIdRef = useRef<string | null>(null);
60
+ const isLoadingRef = useRef<boolean>(false);
61
+ const urlRef = useRef<string | null>(null);
62
+
63
+ // Keep refs in sync with state
64
+ useEffect(() => {
65
+ isLoadingRef.current = isLoading;
66
+ urlRef.current = url;
67
+ }, [isLoading, url]);
60
68
 
61
69
  const loadUrl = useCallback(async () => {
62
70
  if (!fileReference) {
63
71
  setUrl(null);
64
72
  setIsLoading(false);
65
73
  setError(null);
74
+ fileReferenceIdRef.current = null;
66
75
  return;
67
76
  }
68
77
 
@@ -70,11 +79,12 @@ export function useFileUrl(
70
79
  setUrl(null);
71
80
  setIsLoading(false);
72
81
  setError(new Error('Supabase client is required for URL generation'));
82
+ fileReferenceIdRef.current = null;
73
83
  return;
74
84
  }
75
85
 
76
86
  // Skip if already loading or URL already exists for this file
77
- if (isLoading || (url && fileReferenceIdRef.current === fileReference.id)) {
87
+ if (isLoadingRef.current || (urlRef.current && fileReferenceIdRef.current === fileReference.id)) {
78
88
  return;
79
89
  }
80
90
 
@@ -108,29 +118,63 @@ export function useFileUrl(
108
118
  } finally {
109
119
  setIsLoading(false);
110
120
  }
111
- }, [fileReference, supabase, organisation_id, isLoading, url]);
121
+ }, [fileReference, supabase, organisation_id]);
112
122
 
113
123
  const clear = useCallback(() => {
114
124
  setUrl(null);
115
125
  setError(null);
116
126
  setIsLoading(false);
117
127
  fileReferenceIdRef.current = null;
128
+ urlRef.current = null;
118
129
  }, []);
119
130
 
120
131
  // Auto-load URL when fileReference changes
121
132
  useEffect(() => {
122
- if (autoLoad) {
123
- // Reset URL when file reference changes
124
- if (fileReferenceIdRef.current !== fileReference?.id) {
133
+ if (!autoLoad) {
134
+ return;
135
+ }
136
+
137
+ const currentFileId = fileReference?.id ?? null;
138
+ const previousFileId = fileReferenceIdRef.current;
139
+
140
+ // Handle null file reference
141
+ if (!fileReference) {
142
+ if (previousFileId !== null) {
125
143
  setUrl(null);
126
144
  setError(null);
145
+ fileReferenceIdRef.current = null;
146
+ urlRef.current = null;
127
147
  }
128
-
129
- if (fileReference && !url && !isLoading) {
148
+ return;
149
+ }
150
+
151
+ // Reset URL when file reference ID changes (but not on initial mount)
152
+ if (previousFileId !== null && previousFileId !== currentFileId) {
153
+ setUrl(null);
154
+ setError(null);
155
+ fileReferenceIdRef.current = null;
156
+ urlRef.current = null; // Also reset the ref immediately
157
+ // Immediately try to load - loadUrl will check if already loading
158
+ loadUrl();
159
+ return;
160
+ }
161
+
162
+ // Initial load or reload after clear: Load URL if we have a file reference and no URL is set
163
+ if (previousFileId === null && currentFileId !== null) {
164
+ // First time loading this file or reloading after clear - update ref and load
165
+ fileReferenceIdRef.current = currentFileId;
166
+ if (!isLoading && !url) {
130
167
  loadUrl();
131
168
  }
169
+ } else if (previousFileId === currentFileId && !url && !isLoading) {
170
+ // Same file but URL not loaded yet (e.g., after clear or initial mount edge case)
171
+ // Ensure ref is set correctly
172
+ if (fileReferenceIdRef.current !== currentFileId) {
173
+ fileReferenceIdRef.current = currentFileId;
174
+ }
175
+ loadUrl();
132
176
  }
133
- }, [fileReference?.id, autoLoad, loadUrl, url, isLoading]);
177
+ }, [fileReference, autoLoad, loadUrl, url, isLoading]);
134
178
 
135
179
  return {
136
180
  url,
@@ -625,7 +625,8 @@ describe('useOrganisationSecurity', () => {
625
625
 
626
626
  it('handles permission retrieval errors gracefully', async () => {
627
627
  mockIsSuperAdmin.mockResolvedValue(false);
628
- mockIsPermittedCached.mockRejectedValue(new Error('Permission retrieval failed'));
628
+ // getUserPermissions uses getPermissionMap, not isPermittedCached
629
+ mockGetPermissionMap.mockRejectedValue(new Error('Permission retrieval failed'));
629
630
 
630
631
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
631
632
 
@@ -82,7 +82,7 @@ describe('Provider Lifecycle Tests', () => {
82
82
 
83
83
  rerender(<TestComponent isVisible={false} />);
84
84
 
85
- expect(screen.queryByTestId('cleanup-test')).not.toBeInTheDocument();
85
+ expect(screen.queryByTestId('cleanup-test')).toBeNull();
86
86
  });
87
87
 
88
88
  it('should cleanup subscription listeners', () => {
@@ -109,9 +109,12 @@ describe('Auth/RBAC E2E Flows', () => {
109
109
  }
110
110
  return Promise.resolve({ data: false, error: null });
111
111
  },
112
- util_app_resolve: (params?: any) => {
112
+ data_app_resolve: (params?: any) => {
113
113
  // Return app ID for TEST_APP
114
- return Promise.resolve({ data: { id: 'app-123', name: 'TEST_APP' }, error: null });
114
+ return Promise.resolve({
115
+ data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
116
+ error: null
117
+ });
115
118
  },
116
119
  data_user_organisation_roles_get: (params?: any) => {
117
120
  // Return organisation roles/memberships
@@ -241,8 +244,11 @@ describe('Auth/RBAC E2E Flows', () => {
241
244
  return Promise.resolve({ data: true, error: null });
242
245
  }
243
246
  // Handle other RPCs
244
- if (functionName === 'util_app_resolve') {
245
- return Promise.resolve({ data: { id: 'app-123', name: 'TEST_APP' }, error: null });
247
+ if (functionName === 'data_app_resolve') {
248
+ return Promise.resolve({
249
+ data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
250
+ error: null
251
+ });
246
252
  }
247
253
  if (functionName === 'data_user_organisation_roles_get') {
248
254
  return Promise.resolve({ data: [], error: null });
@@ -419,8 +425,11 @@ describe('Auth/RBAC E2E Flows', () => {
419
425
  return Promise.resolve({ data: false, error: null });
420
426
  }
421
427
  // Handle other RPCs
422
- if (functionName === 'util_app_resolve') {
423
- return Promise.resolve({ data: { id: 'app-123', name: 'TEST_APP' }, error: null });
428
+ if (functionName === 'data_app_resolve') {
429
+ return Promise.resolve({
430
+ data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
431
+ error: null
432
+ });
424
433
  }
425
434
  if (functionName === 'data_user_organisation_roles_get') {
426
435
  return Promise.resolve({
@@ -430,7 +430,7 @@ describe('RBAC Functions', () => {
430
430
  });
431
431
  });
432
432
 
433
- describe('rbac_roles_list', () => {
433
+ describe('data_rbac_roles_list', () => {
434
434
  it('should list all user roles', async () => {
435
435
  mockSupabase.rpc.mockResolvedValue({
436
436
  data: [
@@ -454,7 +454,7 @@ describe('RBAC Functions', () => {
454
454
  error: null
455
455
  });
456
456
 
457
- const result = await mockSupabase.rpc('rbac_roles_list', {
457
+ const result = await mockSupabase.rpc('data_rbac_roles_list', {
458
458
  p_user_id: mockUser.id
459
459
  });
460
460
 
@@ -476,7 +476,7 @@ describe('RBAC Functions', () => {
476
476
  error: null
477
477
  });
478
478
 
479
- const result = await mockSupabase.rpc('rbac_roles_list', {
479
+ const result = await mockSupabase.rpc('data_rbac_roles_list', {
480
480
  p_user_id: mockUser.id,
481
481
  p_organisation_id: mockOrganisation.id
482
482
  });
@@ -191,6 +191,14 @@ describe('RBAC API', () => {
191
191
 
192
192
  it('handles engine creation errors gracefully', () => {
193
193
  const error = new Error('Engine creation error');
194
+ // Ensure createRBACConfig doesn't throw so we can test engine creation error
195
+ mockCreateRBACConfig.mockImplementation((config) => config);
196
+ mockGetRBACLogger.mockReturnValue({
197
+ info: vi.fn(),
198
+ warn: vi.fn(),
199
+ error: vi.fn(),
200
+ debug: vi.fn()
201
+ });
194
202
  mockCreateRBACEngine.mockImplementation(() => {
195
203
  throw error;
196
204
  });
@@ -202,6 +210,16 @@ describe('RBAC API', () => {
202
210
 
203
211
  it('handles audit manager creation errors gracefully', () => {
204
212
  const error = new Error('Audit manager creation error');
213
+ // Ensure createRBACConfig doesn't throw so we can test audit manager creation error
214
+ mockCreateRBACConfig.mockImplementation((config) => config);
215
+ mockGetRBACLogger.mockReturnValue({
216
+ info: vi.fn(),
217
+ warn: vi.fn(),
218
+ error: vi.fn(),
219
+ debug: vi.fn()
220
+ });
221
+ const mockEngine = { id: 'test-engine' };
222
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
205
223
  mockCreateAuditManager.mockImplementation(() => {
206
224
  throw error;
207
225
  });
@@ -309,6 +327,21 @@ describe('RBAC API', () => {
309
327
 
310
328
  describe('Error Handling', () => {
311
329
  it('handles missing supabase client', () => {
330
+ const mockEngine = { id: 'test-engine' };
331
+ const mockAuditManager = { id: 'test-audit' };
332
+ const mockLogger = {
333
+ info: vi.fn(),
334
+ warn: vi.fn(),
335
+ error: vi.fn(),
336
+ debug: vi.fn()
337
+ };
338
+
339
+ // Ensure mocks don't throw
340
+ mockCreateRBACConfig.mockImplementation((config) => config);
341
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
342
+ mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
343
+ mockGetRBACLogger.mockReturnValue(mockLogger);
344
+
312
345
  // The function doesn't throw, it just creates config with null
313
346
  expect(() => {
314
347
  setupRBAC(null as any);
@@ -317,6 +350,20 @@ describe('RBAC API', () => {
317
350
 
318
351
  it('handles invalid supabase client', () => {
319
352
  const invalidSupabase = { invalid: 'client' };
353
+ const mockEngine = { id: 'test-engine' };
354
+ const mockAuditManager = { id: 'test-audit' };
355
+ const mockLogger = {
356
+ info: vi.fn(),
357
+ warn: vi.fn(),
358
+ error: vi.fn(),
359
+ debug: vi.fn()
360
+ };
361
+
362
+ // Ensure mocks don't throw
363
+ mockCreateRBACConfig.mockImplementation((config) => config);
364
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
365
+ mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
366
+ mockGetRBACLogger.mockReturnValue(mockLogger);
320
367
 
321
368
  // The function doesn't throw, it just creates config with invalid client
322
369
  expect(() => {
@@ -354,6 +401,21 @@ describe('RBAC API', () => {
354
401
 
355
402
  describe('Configuration Validation', () => {
356
403
  it('validates required supabase client', () => {
404
+ const mockEngine = { id: 'test-engine' };
405
+ const mockAuditManager = { id: 'test-audit' };
406
+ const mockLogger = {
407
+ info: vi.fn(),
408
+ warn: vi.fn(),
409
+ error: vi.fn(),
410
+ debug: vi.fn()
411
+ };
412
+
413
+ // Ensure mocks don't throw
414
+ mockCreateRBACConfig.mockImplementation((config) => config);
415
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
416
+ mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
417
+ mockGetRBACLogger.mockReturnValue(mockLogger);
418
+
357
419
  // The function doesn't throw, it just creates config with undefined
358
420
  expect(() => {
359
421
  setupRBAC(undefined as any);
@@ -365,6 +427,20 @@ describe('RBAC API', () => {
365
427
  from: 'not-a-function',
366
428
  auth: 'not-an-object'
367
429
  };
430
+ const mockEngine = { id: 'test-engine' };
431
+ const mockAuditManager = { id: 'test-audit' };
432
+ const mockLogger = {
433
+ info: vi.fn(),
434
+ warn: vi.fn(),
435
+ error: vi.fn(),
436
+ debug: vi.fn()
437
+ };
438
+
439
+ // Ensure mocks don't throw
440
+ mockCreateRBACConfig.mockImplementation((config) => config);
441
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
442
+ mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
443
+ mockGetRBACLogger.mockReturnValue(mockLogger);
368
444
 
369
445
  // The function doesn't throw, it just creates config with invalid client
370
446
  expect(() => {
@@ -415,6 +491,32 @@ describe('RBAC API', () => {
415
491
  });
416
492
  });
417
493
 
494
+ describe('Type Safety', () => {
495
+ it('accepts valid Supabase client types', () => {
496
+ const validSupabase = {
497
+ from: vi.fn(),
498
+ auth: { getUser: vi.fn() },
499
+ rpc: vi.fn()
500
+ };
501
+ const mockEngine = { id: 'test-engine' };
502
+ const mockAuditManager = { id: 'test-audit' };
503
+ const mockLogger = {
504
+ info: vi.fn(),
505
+ warn: vi.fn(),
506
+ error: vi.fn(),
507
+ debug: vi.fn()
508
+ };
509
+
510
+ mockCreateRBACEngine.mockReturnValue(mockEngine as any);
511
+ mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
512
+ mockGetRBACLogger.mockReturnValue(mockLogger);
513
+
514
+ expect(() => {
515
+ setupRBAC(validSupabase as any);
516
+ }).not.toThrow();
517
+ });
518
+ });
519
+
418
520
  describe('Cache Integration', () => {
419
521
  it('initializes cache correctly', () => {
420
522
  const mockEngine = { id: 'test-engine' };
@@ -456,6 +558,8 @@ describe('RBAC API', () => {
456
558
  debug: vi.fn()
457
559
  };
458
560
 
561
+ // Ensure mocks don't throw
562
+ mockCreateRBACConfig.mockImplementation((config) => config);
459
563
  mockCreateRBACEngine.mockReturnValue(mockEngine as any);
460
564
  mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
461
565
  mockGetRBACLogger.mockReturnValue(mockLogger);
@@ -440,7 +440,7 @@ export class RBACEngine {
440
440
  async resolveAppContext(input: { userId: UUID; appName: string }): Promise<RBACAppContext | null> {
441
441
  try {
442
442
  const { userId, appName } = input;
443
- const { data, error } = await (this.supabase as any).rpc('util_app_resolve', {
443
+ const { data, error } = await (this.supabase as any).rpc('data_app_resolve', {
444
444
  p_user_id: userId,
445
445
  p_app_name: appName,
446
446
  });
@@ -647,7 +647,7 @@ describe('useCan Hook', () => {
647
647
  }, { timeout: 20000 });
648
648
  });
649
649
 
650
- it('times out after 3 seconds when organisationId is missing', async () => {
650
+ it('times out after 3 seconds when organisationId is missing', { timeout: 10000 }, async () => {
651
651
  vi.useFakeTimers({ shouldAdvanceTime: true });
652
652
 
653
653
  const emptyScope = {} as any;
@@ -672,7 +672,7 @@ describe('useCan Hook', () => {
672
672
  expect(result.current.error).not.toBeNull();
673
673
  expect(result.current.error?.message).toBe('Organisation context is required for permission checks');
674
674
  }, { timeout: 2000 });
675
- }, { timeout: 10000 });
675
+ });
676
676
 
677
677
  it('handles null userId', async () => {
678
678
  const { result } = renderHook(() =>
@@ -536,7 +536,7 @@ export class SecureSupabaseClient {
536
536
  // 2. Add it here with the parameters it accepts
537
537
  const rpcContextWhitelist: Record<string, Set<string>> = {
538
538
  // RPCs that accept all three context parameters
539
- 'rbac_roles_list': new Set(['p_organisation_id', 'p_event_id', 'p_app_id']),
539
+ 'data_rbac_roles_list': new Set(['p_organisation_id', 'p_event_id', 'p_app_id']),
540
540
 
541
541
  // RPCs that accept only p_organisation_id (not p_app_id or p_event_id)
542
542
  'data_file_reference_by_category_list': new Set(['p_organisation_id']),
@@ -185,7 +185,7 @@ export enum RPCFunction {
185
185
  // Role Management Functions
186
186
  RBAC_ROLE_GRANT = 'rbac_role_grant',
187
187
  RBAC_ROLE_REVOKE = 'rbac_role_revoke',
188
- RBAC_ROLES_LIST = 'rbac_roles_list',
188
+ RBAC_ROLES_LIST = 'data_rbac_roles_list',
189
189
  RBAC_ROLE_VALIDATE = 'rbac_role_validate',
190
190
 
191
191
  // Session & Audit Functions
@@ -88,7 +88,7 @@ describe('parseAndNormalizeEventColours', () => {
88
88
  });
89
89
 
90
90
  describe('Standard format only', () => {
91
- it('normalizes legacy format with ev-main, ev-sec, ev-acc keys to standard format', () => {
91
+ it('supports ev-main, ev-sec, ev-acc keys and normalizes them', () => {
92
92
  const input = {
93
93
  'ev-main': { 500: { L: 0.5, C: 0.2, H: 0 } },
94
94
  'ev-sec': { 500: { L: 0.5, C: 0.2, H: 120 } },
@@ -96,14 +96,11 @@ describe('parseAndNormalizeEventColours', () => {
96
96
  };
97
97
 
98
98
  const result = parseAndNormalizeEventColours(input);
99
- // Legacy format is normalized to standard format (ev-main -> main, etc.)
99
+ // ev-main, ev-sec, ev-acc keys are supported and normalized to main, sec, acc
100
100
  expect(result).not.toBeNull();
101
- expect(result).toHaveProperty('main');
102
- expect(result).toHaveProperty('sec');
103
- expect(result).toHaveProperty('acc');
104
- expect(result?.main).toHaveProperty('500');
105
- expect(result?.sec).toHaveProperty('500');
106
- expect(result?.acc).toHaveProperty('500');
101
+ expect(result?.main[500]).toEqual({ L: 0.5, C: 0.2, H: 0 });
102
+ expect(result?.sec[500]).toEqual({ L: 0.5, C: 0.2, H: 120 });
103
+ expect(result?.acc[500]).toEqual({ L: 0.5, C: 0.2, H: 240 });
107
104
  });
108
105
 
109
106
  it('uses standard keys when provided', () => {
@@ -198,6 +195,118 @@ describe('parseAndNormalizeEventColours', () => {
198
195
  });
199
196
  });
200
197
 
198
+ describe('ev- prefix handling for shade names', () => {
199
+ it('strips ev- prefix from shade names', () => {
200
+ const input = {
201
+ main: {
202
+ 'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
203
+ 'ev-main-100': { L: 0.9, C: 0.1, H: 0 }
204
+ },
205
+ acc: {
206
+ 'ev-acc-500': { L: 0.5, C: 0.2, H: 240 }
207
+ }
208
+ };
209
+
210
+ const result = parseAndNormalizeEventColours(input);
211
+ expect(result).not.toBeNull();
212
+
213
+ // ev- prefix should be stripped from shade names
214
+ expect(result?.main['main-500']).toEqual({ L: 0.5, C: 0.2, H: 0 });
215
+ expect(result?.main['main-100']).toEqual({ L: 0.9, C: 0.1, H: 0 });
216
+ expect(result?.acc['acc-500']).toEqual({ L: 0.5, C: 0.2, H: 240 });
217
+
218
+ // Original keys with ev- prefix should not exist
219
+ expect(result?.main['ev-main-500']).toBeUndefined();
220
+ expect(result?.main['ev-main-100']).toBeUndefined();
221
+ expect(result?.acc['ev-acc-500']).toBeUndefined();
222
+ });
223
+
224
+ it('handles mixed ev- prefixed and non-prefixed shade names', () => {
225
+ const input = {
226
+ main: {
227
+ 'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
228
+ 100: { L: 0.9, C: 0.1, H: 0 },
229
+ raw: { L: 0.55, C: 0.25, H: 5 }
230
+ }
231
+ };
232
+
233
+ const result = parseAndNormalizeEventColours(input);
234
+ expect(result).not.toBeNull();
235
+
236
+ // ev- prefixed shade should be normalized
237
+ expect(result?.main['main-500']).toEqual({ L: 0.5, C: 0.2, H: 0 });
238
+
239
+ // Non-prefixed shades should remain unchanged
240
+ expect(result?.main[100]).toEqual({ L: 0.9, C: 0.1, H: 0 });
241
+ expect(result?.main.raw).toEqual({ L: 0.55, C: 0.25, H: 5 });
242
+ });
243
+
244
+ it('handles ev- prefixed shade names in sec palette', () => {
245
+ const input = {
246
+ sec: {
247
+ 'ev-sec-500': { L: 0.5, C: 0.2, H: 120 },
248
+ 'ev-sec-200': { L: 0.8, C: 0.15, H: 120 }
249
+ }
250
+ };
251
+
252
+ const result = parseAndNormalizeEventColours(input);
253
+ expect(result).not.toBeNull();
254
+
255
+ expect(result?.sec['sec-500']).toEqual({ L: 0.5, C: 0.2, H: 120 });
256
+ expect(result?.sec['sec-200']).toEqual({ L: 0.8, C: 0.15, H: 120 });
257
+ });
258
+
259
+ it('preserves shade names that do not start with ev-', () => {
260
+ const input = {
261
+ main: {
262
+ 500: { L: 0.5, C: 0.2, H: 0 },
263
+ 'main-500': { L: 0.6, C: 0.3, H: 0 },
264
+ raw: { L: 0.55, C: 0.25, H: 5 }
265
+ }
266
+ };
267
+
268
+ const result = parseAndNormalizeEventColours(input);
269
+ expect(result).not.toBeNull();
270
+
271
+ // All shades should be preserved as-is (no ev- prefix to strip)
272
+ expect(result?.main[500]).toEqual({ L: 0.5, C: 0.2, H: 0 });
273
+ expect(result?.main['main-500']).toEqual({ L: 0.6, C: 0.3, H: 0 });
274
+ expect(result?.main.raw).toEqual({ L: 0.55, C: 0.25, H: 5 });
275
+ });
276
+
277
+ it('normalizes both palette keys and shade names with ev- prefix together', () => {
278
+ const input = {
279
+ 'ev-main': {
280
+ 'ev-main-raw': { L: 0.55, C: 0.25, H: 5 },
281
+ 'ev-main-200': { L: 0.8, C: 0.15, H: 0 }
282
+ },
283
+ 'ev-sec': {
284
+ 'ev-sec-200': { L: 0.8, C: 0.15, H: 120 },
285
+ 'ev-sec-500': { L: 0.5, C: 0.2, H: 120 }
286
+ },
287
+ 'ev-acc': {
288
+ 'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
289
+ }
290
+ };
291
+
292
+ const result = parseAndNormalizeEventColours(input);
293
+ expect(result).not.toBeNull();
294
+
295
+ // Palette keys should be normalized: ev-main -> main, ev-sec -> sec, ev-acc -> acc
296
+ // Shade names should be normalized: ev-main-raw -> main-raw, ev-sec-200 -> sec-200, etc.
297
+ expect(result?.main['main-raw']).toEqual({ L: 0.55, C: 0.25, H: 5 });
298
+ expect(result?.main['main-200']).toEqual({ L: 0.8, C: 0.15, H: 0 });
299
+ expect(result?.sec['sec-200']).toEqual({ L: 0.8, C: 0.15, H: 120 });
300
+ expect(result?.sec['sec-500']).toEqual({ L: 0.5, C: 0.2, H: 120 });
301
+ expect(result?.acc['acc-800']).toEqual({ L: 0.3, C: 0.2, H: 240 });
302
+
303
+ // Original keys should not exist
304
+ expect(result?.main['ev-main-raw']).toBeUndefined();
305
+ expect(result?.sec['ev-sec-200']).toBeUndefined();
306
+ expect(result?.acc['ev-acc-800']).toBeUndefined();
307
+ });
308
+ });
309
+
201
310
  describe('Error handling', () => {
202
311
  it('handles malformed color objects gracefully', () => {
203
312
  const input = {
@@ -15,12 +15,21 @@
15
15
  * Supports input formats:
16
16
  * - Object with 'main', 'sec', 'acc' keys (standard format)
17
17
  * - Object with 'ev-main', 'ev-sec', 'ev-acc' keys (database format with prefix)
18
+ * - Shade names with 'ev-' prefix (e.g., 'ev-acc-500' -> 'acc-500', 'ev-main-raw' -> 'main-raw')
18
19
  * - JSON string that will be parsed
19
20
  *
20
21
  * Only includes explicitly defined color values. Does not fill
21
22
  * missing shades - only shades that are present in the input will
22
23
  * be included in the output.
23
24
  *
25
+ * The parser automatically strips 'ev-' prefixes from BOTH palette keys and shade names
26
+ * for future-proofing, allowing distinction between event colors and organization colors
27
+ * (org colors not yet implemented).
28
+ *
29
+ * Normalization rules:
30
+ * - Palette keys: 'ev-main' -> 'main', 'ev-sec' -> 'sec', 'ev-acc' -> 'acc'
31
+ * - Shade names: 'ev-main-raw' -> 'main-raw', 'ev-sec-200' -> 'sec-200', 'ev-acc-800' -> 'acc-800'
32
+ *
24
33
  * @param input - Event colours from database (JSONB field)
25
34
  * @returns Normalized palette data with main, sec, acc palettes, or null if invalid
26
35
  *
@@ -38,7 +47,7 @@
38
47
  *
39
48
  * @example
40
49
  * ```ts
41
- * // Database format with ev- prefix
50
+ * // Database format with ev- prefix on palette keys
42
51
  * const colours = {
43
52
  * 'ev-main': { 500: { L: 0.5, C: 0.2, H: 0 } },
44
53
  * 'ev-sec': { 500: { L: 0.5, C: 0.2, H: 120 } },
@@ -48,6 +57,44 @@
48
57
  * // Returns: { main: { 500: {...} }, sec: { 500: {...} }, acc: { 500: {...} } }
49
58
  * ```
50
59
  *
60
+ * @example
61
+ * ```ts
62
+ * // Future-proofing: ev- prefix on shade names
63
+ * const colours = {
64
+ * main: {
65
+ * 'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
66
+ * 'ev-main-raw': { L: 0.55, C: 0.25, H: 5 }
67
+ * },
68
+ * sec: {
69
+ * 'ev-sec-200': { L: 0.8, C: 0.15, H: 120 }
70
+ * },
71
+ * acc: {
72
+ * 'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
73
+ * }
74
+ * };
75
+ * const palette = parseAndNormalizeEventColours(colours);
76
+ * // Returns: { main: { 'main-500': {...}, 'main-raw': {...} }, sec: { 'sec-200': {...} }, acc: { 'acc-800': {...} } }
77
+ * ```
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Both palette keys and shade names with ev- prefix
82
+ * const colours = {
83
+ * 'ev-main': {
84
+ * 'ev-main-raw': { L: 0.55, C: 0.25, H: 5 },
85
+ * 'ev-main-200': { L: 0.8, C: 0.15, H: 0 }
86
+ * },
87
+ * 'ev-sec': {
88
+ * 'ev-sec-500': { L: 0.5, C: 0.2, H: 120 }
89
+ * },
90
+ * 'ev-acc': {
91
+ * 'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
92
+ * }
93
+ * };
94
+ * const palette = parseAndNormalizeEventColours(colours);
95
+ * // Returns: { main: { 'main-raw': {...}, 'main-200': {...} }, sec: { 'sec-500': {...} }, acc: { 'acc-800': {...} } }
96
+ * ```
97
+ *
51
98
  */
52
99
  export function parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
53
100
  try {
@@ -78,6 +125,8 @@ export function parseAndNormalizeEventColours(input: unknown): { main: any; sec:
78
125
 
79
126
  // Helper: only include explicitly defined color values
80
127
  // This ensures we don't include undefined shades in the palette
128
+ // Also strips 'ev-' prefix from shade names for future-proofing (to distinguish event colors from org colors)
129
+ // Palette keys are normalized above (ev-main -> main), shade names are normalized here (ev-main-raw -> main-raw)
81
130
  const fill = (p: any) => {
82
131
  if (!p || typeof p !== 'object') return {} as any;
83
132
  const out: any = {};
@@ -88,7 +137,12 @@ export function parseAndNormalizeEventColours(input: unknown): { main: any; sec:
88
137
  // Only include the key if it has a defined value (not null, undefined, or empty)
89
138
  const value = p[key];
90
139
  if (value !== null && value !== undefined && value !== '') {
91
- out[key] = value;
140
+ // Strip 'ev-' prefix from shade names for future-proofing
141
+ // e.g., 'ev-acc-500' -> 'acc-500', 'ev-main-raw' -> 'main-raw', 'ev-sec-200' -> 'sec-200'
142
+ const normalizedKey = typeof key === 'string' && key.startsWith('ev-')
143
+ ? key.substring(3)
144
+ : key;
145
+ out[normalizedKey] = value;
92
146
  }
93
147
  }
94
148
 
@@ -128,7 +128,7 @@ export type RPCFunction =
128
128
  | 'rbac_page_access_check'
129
129
  | 'rbac_role_grant'
130
130
  | 'rbac_role_revoke'
131
- | 'rbac_roles_list'
131
+ | 'data_rbac_roles_list'
132
132
  | 'rbac_role_validate'
133
133
  | 'rbac_session_track'
134
134
  | 'rbac_audit_log'
@@ -137,8 +137,7 @@ export type RPCFunction =
137
137
  | 'data_user_organisation_roles_get'
138
138
  | 'data_user_organisations_get'
139
139
  | 'data_cake_meals_get'
140
- // Utility Functions
141
- | 'util_app_resolve'
140
+ | 'data_app_resolve'
142
141
  | 'debug_auth_context'
143
142
  | 'handle_new_user'
144
143
  // Other functions