@jmruthers/pace-core 0.5.186 → 0.5.188

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 (290) hide show
  1. package/dist/{DataTable-IX2NBUTP.js → DataTable-GUFUNZ3N.js} +7 -7
  2. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  3. package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-HGPQUCBC.js → chunk-2UUZZJFT.js} +3 -3
  8. package/dist/{chunk-445GEP27.js → chunk-3GOZZZYH.js} +33 -8
  9. package/dist/chunk-3GOZZZYH.js.map +1 -0
  10. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  11. package/dist/chunk-63FOKYGO.js.map +1 -0
  12. package/dist/{chunk-DAGICKHT.js → chunk-DDM4CCYT.js} +3 -3
  13. package/dist/{chunk-XAUHJD3L.js → chunk-E7UAOUMY.js} +2 -2
  14. package/dist/{chunk-HDCUMOOI.js → chunk-EFCLXK7F.js} +792 -559
  15. package/dist/chunk-EFCLXK7F.js.map +1 -0
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-GRIQLQ52.js → chunk-IM4QE42D.js} +27 -23
  19. package/dist/chunk-IM4QE42D.js.map +1 -0
  20. package/dist/{chunk-OALXJH4Y.js → chunk-IPCH26AG.js} +8 -8
  21. package/dist/chunk-IPCH26AG.js.map +1 -0
  22. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  23. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  24. package/dist/{chunk-TC7D3CR3.js → chunk-UNOTYLQF.js} +556 -101
  25. package/dist/chunk-UNOTYLQF.js.map +1 -0
  26. package/dist/{chunk-FXFJRTKI.js → chunk-VGZZXKBR.js} +5 -5
  27. package/dist/chunk-VGZZXKBR.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/getting-started/examples/README.md +2 -2
  208. package/docs/implementation-guides/file-upload-storage.md +29 -0
  209. package/docs/implementation-guides/public-pages.md +140 -1230
  210. package/docs/rbac/README.md +2 -1
  211. package/docs/rbac/api-reference.md +11 -0
  212. package/docs/rbac/performance.md +320 -0
  213. package/docs/standards/01-architecture-standard.md +5 -0
  214. package/docs/standards/05-security-standard.md +14 -0
  215. package/docs/standards/07-rbac-and-rls-standard.md +356 -0
  216. package/package.json +1 -1
  217. package/src/__tests__/public-recipe-view.test.ts +199 -0
  218. package/src/__tests__/rls-policies.test.ts +333 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/index.ts +2 -0
  227. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  228. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  229. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  230. package/src/hooks/index.ts +6 -0
  231. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  232. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  233. package/src/hooks/useAddressAutocomplete.ts +268 -0
  234. package/src/hooks/useFileDisplay.ts +3 -15
  235. package/src/hooks/useFileReference.test.ts +20 -3
  236. package/src/hooks/useFileReference.ts +3 -24
  237. package/src/hooks/useFileUrlCache.ts +246 -0
  238. package/src/hooks/useInactivityTracker.ts +31 -20
  239. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  240. package/src/hooks/useOrganisationSecurity.ts +3 -3
  241. package/src/hooks/useQueryCache.ts +315 -0
  242. package/src/index.ts +2 -0
  243. package/src/providers/services/EventServiceProvider.tsx +4 -1
  244. package/src/rbac/api.test.ts +21 -6
  245. package/src/rbac/api.ts +32 -11
  246. package/src/rbac/audit-batched.ts +223 -0
  247. package/src/rbac/audit-enhanced.ts +2 -2
  248. package/src/rbac/audit.test.ts +6 -5
  249. package/src/rbac/audit.ts +34 -6
  250. package/src/rbac/cache-invalidation.ts +63 -12
  251. package/src/rbac/cache.test.ts +2 -2
  252. package/src/rbac/cache.ts +61 -14
  253. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  254. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  255. package/src/rbac/config.ts +9 -0
  256. package/src/rbac/engine.ts +2 -21
  257. package/src/rbac/hooks/usePermissions.ts +21 -5
  258. package/src/rbac/index.ts +19 -0
  259. package/src/rbac/performance.ts +210 -0
  260. package/src/rbac/request-deduplication.ts +87 -0
  261. package/src/rbac/utils/deep-equal.ts +93 -0
  262. package/src/services/OrganisationService.ts +5 -4
  263. package/src/types/file-reference.ts +0 -1
  264. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  265. package/src/utils/file-reference/index.ts +44 -15
  266. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  267. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  268. package/src/utils/google-places/index.ts +26 -0
  269. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  270. package/src/utils/google-places/types.ts +94 -0
  271. package/src/utils/index.ts +23 -0
  272. package/src/utils/request-deduplication.ts +165 -0
  273. package/src/utils/storage/helpers.ts +143 -4
  274. package/dist/chunk-445GEP27.js.map +0 -1
  275. package/dist/chunk-FMUCXFII.js +0 -76
  276. package/dist/chunk-FMUCXFII.js.map +0 -1
  277. package/dist/chunk-FSFQFJCU.js.map +0 -1
  278. package/dist/chunk-FXFJRTKI.js.map +0 -1
  279. package/dist/chunk-GRIQLQ52.js.map +0 -1
  280. package/dist/chunk-HDCUMOOI.js.map +0 -1
  281. package/dist/chunk-OALXJH4Y.js.map +0 -1
  282. package/dist/chunk-TC7D3CR3.js.map +0 -1
  283. package/dist/chunk-U6WNSFX5.js.map +0 -1
  284. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
  285. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
  286. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  287. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  288. /package/dist/{chunk-HGPQUCBC.js.map → chunk-2UUZZJFT.js.map} +0 -0
  289. /package/dist/{chunk-DAGICKHT.js.map → chunk-DDM4CCYT.js.map} +0 -0
  290. /package/dist/{chunk-XAUHJD3L.js.map → chunk-E7UAOUMY.js.map} +0 -0
@@ -0,0 +1,356 @@
1
+ ---
2
+ lastUpdated: 2025-01-28T00:00:00+11:00
3
+ version: 0.5.181
4
+ reviewedBy: database-performance-audit
5
+ ---
6
+
7
+ # RBAC and RLS Standard
8
+
9
+ ## Purpose
10
+
11
+ Define standards for Row-Level Security (RLS) policies and Role-Based Access Control (RBAC) integration to ensure security, performance, and maintainability.
12
+
13
+ ## Principles
14
+
15
+ - **Performance First**: All RLS policies must use optimized helper functions
16
+ - **Security by Default**: Deny access unless explicitly allowed
17
+ - **Consistent Patterns**: Use standardized helper functions across all policies
18
+ - **Performance Monitoring**: Regular validation of query performance
19
+
20
+ ## RLS Policy Performance Requirements
21
+
22
+ ### Helper Function Requirements
23
+
24
+ All helper functions used in RLS policies **MUST** have these attributes:
25
+
26
+ | Requirement | Why | Example |
27
+ |------------|-----|---------|
28
+ | `STABLE` | Prevents re-evaluation for each row | `STABLE` |
29
+ | `SECURITY DEFINER` | Bypass RLS to avoid recursion | `SECURITY DEFINER` |
30
+ | `SET search_path TO 'public'` | Prevent search path injection | `SET search_path TO 'public'` |
31
+ | No inline `auth.uid()` | Causes InitPlan nodes, severe performance degradation | Use helper function instead |
32
+
33
+ ### Helper Function Template
34
+
35
+ ```sql
36
+ CREATE OR REPLACE FUNCTION function_name(parameters)
37
+ RETURNS return_type
38
+ LANGUAGE plpgsql
39
+ STABLE -- ✅ Required
40
+ SECURITY DEFINER -- ✅ Required
41
+ SET search_path TO public -- ✅ Required
42
+ AS $$
43
+ DECLARE
44
+ -- Declarations
45
+ BEGIN
46
+ -- Function body
47
+ RETURN result;
48
+ END;
49
+ $$;
50
+ ```
51
+
52
+ ### Forbidden Patterns
53
+
54
+ **❌ NEVER use these patterns in RLS policies:**
55
+
56
+ ```sql
57
+ -- ❌ BAD: Inline auth.uid() call
58
+ CREATE POLICY "bad_policy" ON table_name
59
+ FOR SELECT USING (
60
+ user_id = auth.uid() -- Called for every row!
61
+ );
62
+
63
+ -- ❌ BAD: Subquery in policy
64
+ CREATE POLICY "bad_policy" ON table_name
65
+ FOR SELECT USING (
66
+ organisation_id IN (
67
+ SELECT organisation_id FROM rbac_organisation_roles
68
+ WHERE user_id = auth.uid() -- Executes for every row!
69
+ )
70
+ );
71
+
72
+ -- ❌ BAD: current_setting in policy
73
+ CREATE POLICY "bad_policy" ON table_name
74
+ FOR SELECT USING (
75
+ organisation_id = current_setting('app.organisation_id')::UUID -- Called for every row!
76
+ );
77
+ ```
78
+
79
+ ### Required Patterns
80
+
81
+ **✅ ALWAYS use these patterns:**
82
+
83
+ ```sql
84
+ -- ✅ GOOD: Use helper function
85
+ CREATE POLICY "good_policy" ON table_name
86
+ FOR SELECT USING (
87
+ check_user_organisation_access(organisation_id) -- Single function call per row
88
+ );
89
+
90
+ -- ✅ GOOD: Use helper function for app ID
91
+ CREATE POLICY "good_policy" ON table_name
92
+ FOR SELECT USING (
93
+ rbac_check_permission_simplified(
94
+ auth.uid(),
95
+ 'read:page.table_name',
96
+ organisation_id,
97
+ event_id,
98
+ get_app_id('APP_NAME'), -- ✅ Helper function
99
+ 'table_name'
100
+ )
101
+ );
102
+ ```
103
+
104
+ ## Standard Helper Functions
105
+
106
+ ### Core Helper Functions
107
+
108
+ These functions are available for use in RLS policies:
109
+
110
+ #### `is_super_admin()`
111
+ - **Returns**: `boolean`
112
+ - **Purpose**: Checks if current user is a super admin
113
+ - **Usage**: `is_super_admin()`
114
+
115
+ #### `check_user_organisation_access(p_organisation_id UUID)`
116
+ - **Returns**: `boolean`
117
+ - **Purpose**: Checks if current user has access to the specified organisation
118
+ - **Usage**: `check_user_organisation_access(organisation_id)`
119
+
120
+ #### `check_user_event_access(p_event_id TEXT)`
121
+ - **Returns**: `boolean`
122
+ - **Purpose**: Checks if current user has access to the specified event
123
+ - **Usage**: `check_user_event_access(event_id)`
124
+
125
+ #### `check_public_event_access(p_event_id TEXT)`
126
+ - **Returns**: `boolean`
127
+ - **Purpose**: Checks if the specified event is publicly accessible
128
+ - **Usage**: `check_public_event_access(event_id)`
129
+
130
+ #### `get_app_id(p_app_name TEXT)`
131
+ - **Returns**: `UUID`
132
+ - **Purpose**: Returns the UUID for any registered app
133
+ - **Usage**: `get_app_id('BASE')`, `get_app_id('PACE')`, `get_app_id('CAKE')`
134
+ - **Note**: Replaces `get_base_app_id()`, `get_pace_app_id()`, `util_get_cake_app_id()`
135
+
136
+ #### `get_organisation_context()`
137
+ - **Returns**: `UUID`
138
+ - **Purpose**: Returns the current organisation context from the session
139
+ - **Usage**: `get_organisation_context()`
140
+
141
+ ### Event Helper Functions
142
+
143
+ #### `get_unit_event_id(p_unit_id TEXT)`
144
+ - **Returns**: `TEXT`
145
+ - **Purpose**: Returns the event_id for a given unit_id
146
+ - **Usage**: `get_unit_event_id(unit_id)`
147
+
148
+ #### `get_form_event_id(p_form_id UUID)`
149
+ - **Returns**: `TEXT`
150
+ - **Purpose**: Returns the event_id for a given form_id
151
+ - **Usage**: `get_form_event_id(form_id)`
152
+
153
+ #### `get_form_response_event_id(p_response_id UUID)`
154
+ - **Returns**: `TEXT`
155
+ - **Purpose**: Returns the event_id for a given form_response_id
156
+ - **Usage**: `get_form_response_event_id(response_id)`
157
+
158
+ #### `get_event_organisation_id(p_event_id TEXT)`
159
+ - **Returns**: `UUID`
160
+ - **Purpose**: Returns the organisation_id for a given event_id
161
+ - **Usage**: `get_event_organisation_id(event_id)`
162
+
163
+ ### RBAC Permission Helper Functions
164
+
165
+ #### `check_rbac_permission_with_context(p_permission TEXT, p_page_name TEXT, p_organisation_id UUID, p_event_id TEXT, p_app_id UUID)`
166
+ - **Returns**: `boolean`
167
+ - **Purpose**: STABLE SECURITY DEFINER wrapper for `rbac_check_permission_simplified()`. Use this in RLS policies instead of calling `rbac_check_permission_simplified()` with `auth.uid()` directly.
168
+ - **Usage**: `check_rbac_permission_with_context('read:page.table_name', 'table_name', organisation_id, NULL, get_app_id('APP_NAME'))`
169
+ - **Note**: This wrapper ensures `auth.uid()` is called once per function invocation (not per row) and the function is STABLE for RLS policy performance.
170
+
171
+ ## RLS Policy Patterns
172
+
173
+ ### Standard Organisation-Scoped Policy
174
+
175
+ ```sql
176
+ CREATE POLICY "rbac_select_table_name" ON table_name
177
+ FOR SELECT TO authenticated
178
+ USING (
179
+ organisation_id IS NOT NULL
180
+ AND (
181
+ is_super_admin()
182
+ OR check_user_organisation_access(organisation_id)
183
+ )
184
+ );
185
+ ```
186
+
187
+ ### Standard Event-Scoped Policy
188
+
189
+ ```sql
190
+ CREATE POLICY "rbac_select_table_name" ON table_name
191
+ FOR SELECT TO authenticated
192
+ USING (
193
+ organisation_id IS NOT NULL
194
+ AND (
195
+ is_super_admin()
196
+ OR check_user_event_access(event_id)
197
+ )
198
+ );
199
+ ```
200
+
201
+ ### RBAC Permission-Based Policy
202
+
203
+ ```sql
204
+ CREATE POLICY "rbac_select_table_name" ON table_name
205
+ FOR SELECT TO authenticated
206
+ USING (
207
+ organisation_id IS NOT NULL
208
+ AND (
209
+ is_super_admin()
210
+ OR check_rbac_permission_with_context(
211
+ 'read:page.table_name',
212
+ 'table_name',
213
+ organisation_id,
214
+ event_id,
215
+ get_app_id('APP_NAME')
216
+ )
217
+ )
218
+ );
219
+ ```
220
+
221
+ **Note**: Always use `check_rbac_permission_with_context()` instead of calling `rbac_check_permission_simplified()` with `auth.uid()` directly. The wrapper function is STABLE SECURITY DEFINER and ensures optimal performance.
222
+
223
+ ### Public Access Policy
224
+
225
+ ```sql
226
+ CREATE POLICY "public_select_table_name" ON table_name
227
+ FOR SELECT TO anon
228
+ USING (
229
+ check_public_event_access(event_id)
230
+ );
231
+ ```
232
+
233
+ ## App Ownership
234
+
235
+ Tables are assigned to specific apps for RBAC permission checking:
236
+
237
+ | App | Tables |
238
+ |-----|--------|
239
+ | **BASE** | `base_application`, `base_questions` |
240
+ | **PACE** | `pace_*` tables, `form_*` tables |
241
+ | **CAKE** | `cake_*` tables |
242
+ | **TRAC** | `trac_*` tables |
243
+ | **MEDI** | `medi_*` tables |
244
+ | **MINT** | `mint_*` tables (handled separately) |
245
+ | **PORTAL** | `form_context_types` (shared with PACE) |
246
+
247
+ ## Testing Requirements
248
+
249
+ ### Before Merging RLS Changes
250
+
251
+ 1. **Run RLS Compliance Audit**:
252
+ ```bash
253
+ npm run audit:rls
254
+ ```
255
+ This checks for violations and should pass with zero violations.
256
+
257
+ 2. **Run Supabase Advisors**:
258
+ ```bash
259
+ supabase advisors performance
260
+ supabase advisors security
261
+ ```
262
+
263
+ 3. **Run Database Tests**:
264
+ ```bash
265
+ timeout 120 npm run test:db
266
+ ```
267
+
268
+ 4. **Run Application Tests**:
269
+ ```bash
270
+ timeout 60 npm run test
271
+ ```
272
+
273
+ 5. **Verify Performance**:
274
+ - Use EXPLAIN ANALYZE to verify no InitPlan nodes
275
+ - Verify queries complete in < 1 second
276
+ - Check Supabase Advisors show zero `auth_rls_initplan` warnings
277
+
278
+ ### Test Coverage Requirements
279
+
280
+ - [ ] Policy coverage: All tables (except mint_*) have RLS enabled and policies
281
+ - [ ] Performance: Queries complete in < 1 second
282
+ - [ ] Security: Cross-organisation access is blocked
283
+ - [ ] Helper functions: All are STABLE SECURITY DEFINER
284
+
285
+ ## Maintenance
286
+
287
+ ### Ongoing RLS Compliance Monitoring
288
+
289
+ **Run RLS audit regularly** to catch any rogue policies that violate standards:
290
+
291
+ ```bash
292
+ # Quick audit (recommended before merging PRs)
293
+ npm run audit:rls
294
+
295
+ # Or use test database
296
+ npm run audit:rls:test
297
+
298
+ # For CI/CD (fails on violations)
299
+ npm run audit:rls:ci
300
+ ```
301
+
302
+ The audit checks for:
303
+ - Tables with RLS enabled but no policies
304
+ - Policies using inline `auth.uid()` calls
305
+ - Policies using `current_setting()` directly
306
+ - Helper functions missing STABLE/SECURITY DEFINER
307
+
308
+ ### Weekly Health Checks
309
+
310
+ Run weekly:
311
+ ```bash
312
+ # RLS compliance audit
313
+ npm run audit:rls
314
+
315
+ # Performance advisors
316
+ supabase advisors performance
317
+
318
+ # Security advisors
319
+ supabase advisors security
320
+
321
+ # Review slow queries
322
+ # (configure log_min_duration_statement = 1000)
323
+ ```
324
+
325
+ ### Monitoring
326
+
327
+ Monitor:
328
+ - Query duration > 5 seconds
329
+ - Connection pool exhaustion (>80% utilization)
330
+ - CPU usage > 80%
331
+ - Memory usage > 80%
332
+ - `auth_rls_initplan` advisor warnings > 0
333
+
334
+ ## Migration Guidelines
335
+
336
+ ### Creating New RLS Policies
337
+
338
+ 1. **Use helper functions** - Never inline `auth.uid()` or `current_setting()`
339
+ 2. **Test with EXPLAIN ANALYZE** - Verify no InitPlan nodes
340
+ 3. **Load test** - Test with realistic data volumes
341
+ 4. **Run Supabase Advisors** - Verify no new warnings
342
+
343
+ ### Updating Existing Policies
344
+
345
+ 1. **Backup existing policies** before changes
346
+ 2. **Test in development** first
347
+ 3. **Monitor performance** after deployment
348
+ 4. **Rollback plan** ready if issues occur
349
+
350
+ ## Related Documentation
351
+
352
+ - [Security Standard](./05-security-standard.md)
353
+ - [RLS Policy Remediation Plan](../troubleshooting/rls-policy-remediation-plan-combined.md)
354
+ - [Database Unhealthiness Diagnosis](../troubleshooting/database-unhealthiness-diagnosis.md)
355
+ - [RBAC-RLS Integration Guide](../rbac/rbac-rls-integration.md)
356
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.186",
3
+ "version": "0.5.188",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @file Public Recipe View Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RLS/Tests/PublicViews
5
+ * @since 0.5.181
6
+ *
7
+ * Regression tests for the public_recipe_details view to verify:
8
+ * - Anonymous access works only when event.public_readable = true
9
+ * - View exposes only expected columns
10
+ * - Respects diet/meal filters
11
+ * - Performance is acceptable (< 500ms)
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
15
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
16
+ import type { Database } from '../../types/database';
17
+
18
+ const TEST_TIMEOUT = 5000;
19
+ const PERFORMANCE_THRESHOLD = 500; // 500ms for public view queries
20
+
21
+ // Check if we're using real test-db (via environment variables)
22
+ const USE_REAL_DB = !!(process.env.SUPABASE_URL && process.env.VITE_SUPABASE_ANON_KEY);
23
+ const TEST_SUPABASE_URL = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL;
24
+ const TEST_SUPABASE_ANON_KEY = process.env.TEST_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
25
+ const TEST_SUPABASE_SERVICE_ROLE_KEY = process.env.TEST_SUPABASE_SERVICE_ROLE_KEY;
26
+
27
+ let anonClient: SupabaseClient<Database>;
28
+ let authenticatedClient: SupabaseClient<Database>;
29
+
30
+ const publicEvent = {
31
+ event_id: 'public-event-1',
32
+ event_name: 'Public Event',
33
+ public_readable: true,
34
+ is_visible: true,
35
+ organisation_id: 'org-1' as any
36
+ };
37
+
38
+ const privateEvent = {
39
+ event_id: 'private-event-1',
40
+ event_name: 'Private Event',
41
+ public_readable: false,
42
+ is_visible: true,
43
+ organisation_id: 'org-1' as any
44
+ };
45
+
46
+ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_ANON_KEY)('Public Recipe View - Anonymous Access', () => {
47
+ beforeEach(() => {
48
+ if (USE_REAL_DB && TEST_SUPABASE_URL && TEST_SUPABASE_ANON_KEY) {
49
+ anonClient = createClient<Database>(TEST_SUPABASE_URL, TEST_SUPABASE_ANON_KEY);
50
+ authenticatedClient = createClient<Database>(TEST_SUPABASE_URL, TEST_SUPABASE_SERVICE_ROLE_KEY || TEST_SUPABASE_ANON_KEY);
51
+ } else {
52
+ // This should not happen due to skipIf, but provide fallback
53
+ throw new Error('Test database credentials not available. Set SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables.');
54
+ }
55
+ });
56
+
57
+ describe('Public Event Access', () => {
58
+ it('should allow anonymous access when event.public_readable = true', async () => {
59
+ const start = Date.now();
60
+ const { data, error } = await anonClient
61
+ .from('public_recipe_details')
62
+ .select('*')
63
+ .eq('event_id', publicEvent.event_id);
64
+ const duration = Date.now() - start;
65
+
66
+ expect(error).toBeNull();
67
+ expect(data).toBeDefined();
68
+ expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
69
+ }, TEST_TIMEOUT);
70
+
71
+ it('should block anonymous access when event.public_readable = false', async () => {
72
+ const { data, error } = await anonClient
73
+ .from('public_recipe_details')
74
+ .select('*')
75
+ .eq('event_id', privateEvent.event_id);
76
+
77
+ // Should return empty (view filters by public_readable = true)
78
+ expect(data).toEqual([]);
79
+ }, TEST_TIMEOUT);
80
+
81
+ it('should block anonymous access when event.is_visible = false', async () => {
82
+ const { data, error } = await anonClient
83
+ .from('public_recipe_details')
84
+ .select('*')
85
+ .eq('event_id', 'hidden-event-1');
86
+
87
+ // Should return empty (view filters by is_visible = true)
88
+ expect(data).toEqual([]);
89
+ }, TEST_TIMEOUT);
90
+ });
91
+
92
+ describe('View Column Exposure', () => {
93
+ it('should expose only expected columns', async () => {
94
+ const { data, error } = await anonClient
95
+ .from('public_recipe_details')
96
+ .select('*')
97
+ .eq('event_id', publicEvent.event_id)
98
+ .limit(1)
99
+ .single();
100
+
101
+ expect(error).toBeNull();
102
+ if (data) {
103
+ // Verify expected columns exist
104
+ expect(data).toHaveProperty('dish_id');
105
+ expect(data).toHaveProperty('dish_name');
106
+ expect(data).toHaveProperty('dish_code');
107
+ expect(data).toHaveProperty('event_id');
108
+ expect(data).toHaveProperty('organisation_id');
109
+
110
+ // Verify sensitive columns are NOT exposed
111
+ // (adjust based on actual view definition)
112
+ // expect(data).not.toHaveProperty('created_by');
113
+ // expect(data).not.toHaveProperty('updated_by');
114
+ }
115
+ }, TEST_TIMEOUT);
116
+ });
117
+
118
+ describe('Diet/Meal Filters', () => {
119
+ it('should filter by meal type', async () => {
120
+ const start = Date.now();
121
+ const { data, error } = await anonClient
122
+ .from('public_recipe_details')
123
+ .select('*')
124
+ .eq('event_id', publicEvent.event_id)
125
+ .eq('mealtype_name', 'Breakfast');
126
+ const duration = Date.now() - start;
127
+
128
+ expect(error).toBeNull();
129
+ expect(data).toBeDefined();
130
+ expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
131
+ }, TEST_TIMEOUT);
132
+
133
+ it('should filter by diet requirements', async () => {
134
+ // This test would verify that diet filters work correctly
135
+ // Adjust based on actual view structure
136
+ const { data, error } = await anonClient
137
+ .from('public_recipe_details')
138
+ .select('*')
139
+ .eq('event_id', publicEvent.event_id)
140
+ .eq('dish_dietnote', 'Vegetarian');
141
+
142
+ expect(error).toBeNull();
143
+ expect(data).toBeDefined();
144
+ }, TEST_TIMEOUT);
145
+ });
146
+
147
+ describe('Performance', () => {
148
+ it('should complete view queries in < 500ms', async () => {
149
+ const start = Date.now();
150
+ await anonClient
151
+ .from('public_recipe_details')
152
+ .select('*')
153
+ .eq('event_id', publicEvent.event_id)
154
+ .limit(100);
155
+ const duration = Date.now() - start;
156
+
157
+ expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
158
+ }, TEST_TIMEOUT);
159
+
160
+ it('should handle filtered queries efficiently', async () => {
161
+ const start = Date.now();
162
+ await anonClient
163
+ .from('public_recipe_details')
164
+ .select('*')
165
+ .eq('event_id', publicEvent.event_id)
166
+ .eq('mealtype_name', 'Breakfast')
167
+ .limit(50);
168
+ const duration = Date.now() - start;
169
+
170
+ expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
171
+ }, TEST_TIMEOUT);
172
+ });
173
+ });
174
+
175
+ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_ANON_KEY)('Public Recipe View - Authenticated Access', () => {
176
+ it('should allow authenticated users to view public recipes', async () => {
177
+ const { data, error } = await authenticatedClient
178
+ .from('public_recipe_details')
179
+ .select('*')
180
+ .eq('event_id', publicEvent.event_id);
181
+
182
+ expect(error).toBeNull();
183
+ expect(data).toBeDefined();
184
+ }, TEST_TIMEOUT);
185
+
186
+ it('should allow authenticated users to view private events they have access to', async () => {
187
+ // Authenticated users with organisation access should be able to view
188
+ // recipes from events in their organisation, even if not public_readable
189
+ // (This depends on the view definition and RLS policies)
190
+ const { data, error } = await authenticatedClient
191
+ .from('public_recipe_details')
192
+ .select('*')
193
+ .eq('event_id', privateEvent.event_id);
194
+
195
+ // Result depends on RLS policies and view definition
196
+ expect(error).toBeNull();
197
+ }, TEST_TIMEOUT);
198
+ });
199
+