@jmruthers/pace-core 0.6.4 → 0.6.6

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 (387) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/core-usage-manifest.json +93 -0
  4. package/cursor-rules/00-pace-core-compliance.mdc +128 -26
  5. package/cursor-rules/01-standards-compliance.mdc +49 -8
  6. package/cursor-rules/02-project-structure.mdc +6 -0
  7. package/cursor-rules/03-solid-principles.mdc +2 -0
  8. package/cursor-rules/04-testing-standards.mdc +2 -0
  9. package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
  10. package/cursor-rules/06-code-quality.mdc +2 -0
  11. package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
  12. package/cursor-rules/08-markup-quality.mdc +52 -27
  13. package/cursor-rules/09-rbac-compliance.mdc +462 -0
  14. package/cursor-rules/10-error-handling-patterns.mdc +179 -0
  15. package/cursor-rules/11-performance-optimization.mdc +169 -0
  16. package/cursor-rules/12-ci-cd-integration.mdc +150 -0
  17. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  18. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
  19. package/dist/DataTable-LRJL4IRV.js +15 -0
  20. package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
  21. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  22. package/dist/api-Y4MQWOFW.js +4 -0
  23. package/dist/audit-MYQXYZFU.js +3 -0
  24. package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
  25. package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
  26. package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
  27. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  28. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  29. package/dist/chunk-4T7OBVTU.js +62 -0
  30. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  31. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  32. package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
  33. package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
  34. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  35. package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
  36. package/dist/chunk-C7NSAPTL.js +1 -0
  37. package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
  38. package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
  39. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  40. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  41. package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
  42. package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
  43. package/dist/chunk-MBADTM7L.js +64 -0
  44. package/dist/chunk-OHIK3MIO.js +994 -0
  45. package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
  46. package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
  47. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  48. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  49. package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
  50. package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
  51. package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
  52. package/dist/components.d.ts +6 -6
  53. package/dist/components.js +57 -267
  54. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  55. package/dist/eslint-rules/index.cjs +22 -0
  56. package/dist/eslint-rules/rules/compliance.cjs +348 -0
  57. package/dist/eslint-rules/rules/components.cjs +113 -0
  58. package/dist/eslint-rules/rules/imports.cjs +102 -0
  59. package/dist/eslint-rules/rules/rbac.cjs +790 -0
  60. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  61. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  62. package/dist/hooks.d.ts +5 -5
  63. package/dist/hooks.js +62 -270
  64. package/dist/icons/index.d.ts +1 -0
  65. package/dist/icons/index.js +1 -0
  66. package/dist/index.d.ts +36 -26
  67. package/dist/index.js +87 -690
  68. package/dist/providers.d.ts +2 -2
  69. package/dist/providers.js +8 -35
  70. package/dist/rbac/eslint-rules.d.ts +46 -44
  71. package/dist/rbac/eslint-rules.js +7 -4
  72. package/dist/rbac/index.d.ts +124 -594
  73. package/dist/rbac/index.js +14 -207
  74. package/dist/styles/index.js +2 -12
  75. package/dist/theming/runtime.js +3 -19
  76. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  77. package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
  78. package/dist/types-t9H8qKRw.d.ts +55 -0
  79. package/dist/types.d.ts +1 -1
  80. package/dist/types.js +7 -94
  81. package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
  82. package/dist/utils.d.ts +24 -117
  83. package/dist/utils.js +54 -392
  84. package/docs/README.md +16 -6
  85. package/docs/api/README.md +4 -402
  86. package/docs/api/modules.md +454 -930
  87. package/docs/api-reference/components.md +3 -1
  88. package/docs/api-reference/deprecated.md +31 -6
  89. package/docs/api-reference/rpc-functions.md +78 -3
  90. package/docs/best-practices/accessibility.md +6 -3
  91. package/docs/getting-started/cursor-rules.md +3 -23
  92. package/docs/getting-started/dependencies.md +650 -0
  93. package/docs/getting-started/installation-guide.md +20 -7
  94. package/docs/getting-started/quick-start.md +23 -12
  95. package/docs/implementation-guides/permission-enforcement.md +4 -0
  96. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  97. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  98. package/docs/rbac/README.md +12 -3
  99. package/docs/rbac/edge-functions-guide.md +376 -0
  100. package/docs/rbac/secure-client-protection.md +0 -34
  101. package/docs/standards/00-pace-core-compliance.md +967 -0
  102. package/docs/standards/01-standards-compliance.md +188 -0
  103. package/docs/standards/02-project-structure.md +985 -0
  104. package/docs/standards/03-solid-principles.md +39 -0
  105. package/docs/standards/04-testing-standards.md +36 -0
  106. package/docs/standards/05-bug-reports-and-features.md +27 -0
  107. package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
  108. package/docs/standards/07-tech-stack-compliance.md +30 -0
  109. package/docs/standards/08-markup-quality.md +345 -0
  110. package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
  111. package/docs/standards/10-error-handling-patterns.md +401 -0
  112. package/docs/standards/11-performance-optimization.md +348 -0
  113. package/docs/standards/12-ci-cd-integration.md +370 -0
  114. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
  115. package/docs/standards/README.md +62 -33
  116. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  117. package/eslint-config-pace-core.cjs +20 -4
  118. package/package.json +31 -21
  119. package/scripts/audit/audit-compliance.cjs +1295 -0
  120. package/scripts/audit/audit-components.cjs +260 -0
  121. package/scripts/audit/audit-dependencies.cjs +395 -0
  122. package/scripts/audit/audit-rbac.cjs +954 -0
  123. package/scripts/audit/audit-standards.cjs +1268 -0
  124. package/scripts/audit/index.cjs +1898 -194
  125. package/scripts/install-cursor-rules.cjs +259 -8
  126. package/scripts/validate-master.js +1 -1
  127. package/src/__tests__/fixtures/supabase.ts +1 -1
  128. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
  129. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  130. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  131. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
  132. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  133. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  134. package/src/__tests__/public-recipe-view.test.ts +38 -9
  135. package/src/components/Button/Button.tsx +5 -1
  136. package/src/components/ContextSelector/ContextSelector.tsx +42 -39
  137. package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
  138. package/src/components/DataTable/components/DataTableBody.tsx +55 -31
  139. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  140. package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
  141. package/src/components/DataTable/components/EditFields.tsx +23 -3
  142. package/src/components/DataTable/components/EditableRow.tsx +7 -2
  143. package/src/components/DataTable/components/ImportModal.tsx +4 -6
  144. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  145. package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
  146. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
  147. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
  148. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  149. package/src/components/DataTable/core/DataTableContext.tsx +1 -1
  150. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  151. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  152. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  153. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  154. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  155. package/src/components/DataTable/types.ts +5 -0
  156. package/src/components/DateTimeField/DateTimeField.tsx +20 -20
  157. package/src/components/DateTimeField/README.md +5 -2
  158. package/src/components/Dialog/Dialog.test.tsx +361 -318
  159. package/src/components/Dialog/Dialog.tsx +1154 -323
  160. package/src/components/Dialog/index.ts +3 -3
  161. package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
  162. package/src/components/FileDisplay/FileDisplay.tsx +28 -22
  163. package/src/components/Form/Form.test.tsx +9 -10
  164. package/src/components/Form/Form.tsx +369 -9
  165. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
  166. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
  167. package/src/components/LoginForm/LoginForm.tsx +2 -2
  168. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  169. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  170. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  171. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  172. package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
  173. package/src/components/PaceAppLayout/README.md +10 -9
  174. package/src/components/PaceAppLayout/test-setup.tsx +40 -31
  175. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  176. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  177. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
  178. package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
  179. package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
  180. package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
  181. package/src/components/Select/Select.tsx +23 -21
  182. package/src/components/Select/types.ts +1 -1
  183. package/src/components/UserMenu/UserMenu.test.tsx +38 -6
  184. package/src/components/UserMenu/UserMenu.tsx +39 -34
  185. package/src/components/index.ts +3 -4
  186. package/src/eslint-rules/index.cjs +22 -0
  187. package/src/eslint-rules/rules/compliance.cjs +348 -0
  188. package/src/eslint-rules/rules/components.cjs +113 -0
  189. package/src/eslint-rules/rules/imports.cjs +102 -0
  190. package/src/eslint-rules/rules/rbac.cjs +790 -0
  191. package/src/eslint-rules/utils/helpers.cjs +42 -0
  192. package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
  193. package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
  194. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  195. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  196. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  197. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  198. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  199. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  200. package/src/hooks/public/usePublicEvent.ts +62 -190
  201. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  202. package/src/hooks/public/usePublicEventLogo.ts +19 -9
  203. package/src/hooks/useAppConfig.ts +26 -24
  204. package/src/hooks/useEventTheme.test.ts +211 -233
  205. package/src/hooks/useEventTheme.ts +19 -28
  206. package/src/hooks/useEvents.ts +11 -7
  207. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  208. package/src/hooks/useOrganisationPermissions.ts +9 -11
  209. package/src/hooks/useOrganisations.ts +13 -7
  210. package/src/hooks/useQueryCache.ts +0 -1
  211. package/src/hooks/useSessionDraft.ts +380 -0
  212. package/src/hooks/useSessionRestoration.ts +3 -1
  213. package/src/icons/index.ts +27 -0
  214. package/src/index.ts +16 -1
  215. package/src/providers/OrganisationProvider.tsx +23 -14
  216. package/src/providers/services/EventServiceProvider.tsx +1 -24
  217. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  218. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
  219. package/src/rbac/README.md +20 -20
  220. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  221. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  222. package/src/rbac/adapters.tsx +7 -295
  223. package/src/rbac/api.test.ts +44 -56
  224. package/src/rbac/api.ts +10 -17
  225. package/src/rbac/cache-invalidation.ts +0 -1
  226. package/src/rbac/compliance/index.ts +10 -0
  227. package/src/rbac/compliance/pattern-detector.ts +553 -0
  228. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  229. package/src/rbac/components/AccessDenied.tsx +150 -0
  230. package/src/rbac/components/NavigationGuard.tsx +12 -20
  231. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  232. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  233. package/src/rbac/components/index.ts +3 -41
  234. package/src/rbac/eslint-rules.js +1 -1
  235. package/src/rbac/hooks/index.ts +0 -3
  236. package/src/rbac/hooks/permissions/index.ts +0 -3
  237. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  238. package/src/rbac/hooks/usePermissions.ts +0 -3
  239. package/src/rbac/hooks/useRBAC.test.ts +21 -3
  240. package/src/rbac/hooks/useRBAC.ts +4 -3
  241. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  242. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  243. package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
  244. package/src/rbac/hooks/useResourcePermissions.ts +182 -63
  245. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  246. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  247. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  248. package/src/rbac/index.ts +7 -9
  249. package/src/rbac/permissions.ts +17 -17
  250. package/src/rbac/utils/contextValidator.ts +45 -7
  251. package/src/services/AuthService.ts +132 -23
  252. package/src/services/EventService.ts +4 -97
  253. package/src/services/InactivityService.ts +155 -58
  254. package/src/services/OrganisationService.ts +7 -44
  255. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  256. package/src/services/base/BaseService.ts +0 -3
  257. package/src/styles/core.css +4 -0
  258. package/src/types/database.generated.ts +4733 -3809
  259. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  260. package/src/utils/context/organisationContext.test.ts +13 -28
  261. package/src/utils/context/organisationContext.ts +21 -52
  262. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  263. package/src/utils/file-reference/index.ts +39 -15
  264. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  265. package/src/utils/formatting/formatTime.test.ts +3 -2
  266. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  267. package/src/utils/index.ts +4 -1
  268. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  269. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  270. package/src/utils/persistence/keyDerivation.ts +304 -0
  271. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  272. package/src/utils/security/secureStorage.ts +5 -5
  273. package/src/utils/storage/helpers.ts +3 -3
  274. package/src/utils/supabase/createBaseClient.ts +147 -0
  275. package/src/utils/timezone/timezone.test.ts +1 -2
  276. package/src/utils/timezone/timezone.ts +1 -1
  277. package/src/utils/validation/csrf.ts +4 -4
  278. package/cursor-rules/CHANGELOG.md +0 -119
  279. package/cursor-rules/README.md +0 -192
  280. package/dist/DataTable-E7YQZD7D.js +0 -175
  281. package/dist/DataTable-E7YQZD7D.js.map +0 -1
  282. package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
  283. package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
  284. package/dist/api-6LVZTHDS.js +0 -52
  285. package/dist/api-6LVZTHDS.js.map +0 -1
  286. package/dist/audit-V53FV5AG.js +0 -17
  287. package/dist/audit-V53FV5AG.js.map +0 -1
  288. package/dist/chunk-36LVWXB2.js +0 -227
  289. package/dist/chunk-36LVWXB2.js.map +0 -1
  290. package/dist/chunk-3LPHPB62.js.map +0 -1
  291. package/dist/chunk-5DRSZLL2.js.map +0 -1
  292. package/dist/chunk-5EC5MEWX.js.map +0 -1
  293. package/dist/chunk-63FOKYGO.js.map +0 -1
  294. package/dist/chunk-6SOIHG6Z.js.map +0 -1
  295. package/dist/chunk-7JPAB3T5.js.map +0 -1
  296. package/dist/chunk-ATKZM7RX.js +0 -2053
  297. package/dist/chunk-ATKZM7RX.js.map +0 -1
  298. package/dist/chunk-AVMLPIM7.js.map +0 -1
  299. package/dist/chunk-DGUM43GV.js.map +0 -1
  300. package/dist/chunk-E66EQZE6.js.map +0 -1
  301. package/dist/chunk-FFQEQTNW.js.map +0 -1
  302. package/dist/chunk-FMUCXFII.js.map +0 -1
  303. package/dist/chunk-G37KK66H.js.map +0 -1
  304. package/dist/chunk-I6DAQMWX.js.map +0 -1
  305. package/dist/chunk-J36DSWQK.js.map +0 -1
  306. package/dist/chunk-KQCRWDSA.js +0 -1
  307. package/dist/chunk-KQCRWDSA.js.map +0 -1
  308. package/dist/chunk-L4OXEN46.js.map +0 -1
  309. package/dist/chunk-LMC26NLJ.js +0 -84
  310. package/dist/chunk-LMC26NLJ.js.map +0 -1
  311. package/dist/chunk-M43Y4SSO.js.map +0 -1
  312. package/dist/chunk-M7MPQISP.js.map +0 -1
  313. package/dist/chunk-NN6WWZ5U.js.map +0 -1
  314. package/dist/chunk-OEWDTMG7.js.map +0 -1
  315. package/dist/chunk-PWLANIRT.js.map +0 -1
  316. package/dist/chunk-QXHPKYJV.js.map +0 -1
  317. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  318. package/dist/chunk-YKRAFF5K.js.map +0 -1
  319. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  320. package/dist/components.js.map +0 -1
  321. package/dist/contextValidator-OOPCLPZW.js +0 -9
  322. package/dist/contextValidator-OOPCLPZW.js.map +0 -1
  323. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  324. package/dist/hooks.js.map +0 -1
  325. package/dist/index.js.map +0 -1
  326. package/dist/providers.js.map +0 -1
  327. package/dist/rbac/eslint-rules.js.map +0 -1
  328. package/dist/rbac/index.js.map +0 -1
  329. package/dist/styles/index.js.map +0 -1
  330. package/dist/theming/runtime.js.map +0 -1
  331. package/dist/types.js.map +0 -1
  332. package/dist/utils.js.map +0 -1
  333. package/docs/standards/01-architecture-standard.md +0 -44
  334. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  335. package/docs/standards/03-component-standard.md +0 -32
  336. package/docs/standards/05-security-standard.md +0 -44
  337. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  338. package/docs/standards/pace-core-compliance.md +0 -432
  339. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  340. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  341. package/scripts/audit/core/checks/bundle.cjs +0 -142
  342. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  343. package/scripts/audit/core/checks/config.cjs +0 -54
  344. package/scripts/audit/core/checks/coverage.cjs +0 -84
  345. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  346. package/scripts/audit/core/checks/documentation.cjs +0 -268
  347. package/scripts/audit/core/checks/environment.cjs +0 -116
  348. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  349. package/scripts/audit/core/checks/forms.cjs +0 -172
  350. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  351. package/scripts/audit/core/checks/hooks.cjs +0 -334
  352. package/scripts/audit/core/checks/imports.cjs +0 -244
  353. package/scripts/audit/core/checks/performance.cjs +0 -325
  354. package/scripts/audit/core/checks/routes.cjs +0 -117
  355. package/scripts/audit/core/checks/state.cjs +0 -130
  356. package/scripts/audit/core/checks/structure.cjs +0 -65
  357. package/scripts/audit/core/checks/style.cjs +0 -584
  358. package/scripts/audit/core/checks/testing.cjs +0 -122
  359. package/scripts/audit/core/checks/typescript.cjs +0 -61
  360. package/scripts/audit/core/scanner.cjs +0 -199
  361. package/scripts/audit/core/utils.cjs +0 -137
  362. package/scripts/audit/reporters/console.cjs +0 -151
  363. package/scripts/audit/reporters/json.cjs +0 -54
  364. package/scripts/audit/reporters/markdown.cjs +0 -124
  365. package/scripts/audit-consuming-app.cjs +0 -86
  366. package/src/eslint-rules/pace-core-compliance.cjs +0 -510
  367. package/src/eslint-rules/pace-core-compliance.js +0 -638
  368. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  369. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  370. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  371. package/src/rbac/components/NavigationProvider.tsx +0 -345
  372. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  373. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  374. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  375. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  376. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  377. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  378. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  379. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  380. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  381. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  382. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  383. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  384. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  385. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  386. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  387. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
@@ -24,16 +24,16 @@ describe('organisationContext', () => {
24
24
  });
25
25
 
26
26
  describe('setOrganisationContext', () => {
27
- it('should set organisation context successfully', async () => {
27
+ it('should set organisation context successfully (no-op)', async () => {
28
28
  const organisationId = 'org-123';
29
29
  const mockRpc = vi.fn().mockResolvedValue({ error: null });
30
30
  mockSupabase.rpc = mockRpc;
31
31
 
32
+ // setOrganisationContext is now a no-op (deprecated)
32
33
  await setOrganisationContext(mockSupabase, organisationId);
33
34
 
34
- expect(mockRpc).toHaveBeenCalledWith('set_organisation_context', {
35
- org_id: organisationId
36
- });
35
+ // Should not call rpc since it's a no-op
36
+ expect(mockRpc).not.toHaveBeenCalled();
37
37
  });
38
38
 
39
39
  it('should handle missing supabase client gracefully', async () => {
@@ -153,14 +153,13 @@ describe('organisationContext', () => {
153
153
  });
154
154
 
155
155
  describe('isOrganisationContextAvailable', () => {
156
- it('should return true when functions are available', async () => {
157
- const mockRpc = vi.fn().mockResolvedValue({ error: null });
158
- mockSupabase.rpc = mockRpc;
159
-
156
+ it('should return false (deprecated function)', async () => {
157
+ // isOrganisationContextAvailable is deprecated and always returns false
160
158
  const result = await isOrganisationContextAvailable(mockSupabase);
161
159
 
162
- expect(mockRpc).toHaveBeenCalledWith('get_organisation_context');
163
- expect(result).toBe(true);
160
+ // Should not call RPC since function is deprecated
161
+ expect(mockSupabase.rpc).not.toHaveBeenCalled();
162
+ expect(result).toBe(false);
164
163
  });
165
164
 
166
165
  it('should return false when supabase client is missing', async () => {
@@ -29,17 +29,13 @@ describe('Organisation Context', () => {
29
29
  });
30
30
 
31
31
  describe('setOrganisationContext', () => {
32
- it('sets organisation context successfully', async () => {
33
- mockSupabase.rpc.mockResolvedValue({
34
- data: null,
35
- error: null
36
- });
37
-
32
+ it('sets organisation context successfully (no-op)', async () => {
33
+ // setOrganisationContext is now a no-op (deprecated)
34
+ // It should complete without calling rpc
38
35
  await setOrganisationContext(mockSupabase, 'org-123');
39
36
 
40
- expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', {
41
- org_id: 'org-123'
42
- });
37
+ // Should not call rpc since it's a no-op
38
+ expect(mockSupabase.rpc).not.toHaveBeenCalled();
43
39
  });
44
40
 
45
41
  it('handles missing supabase client gracefully', async () => {
@@ -180,20 +176,14 @@ describe('Organisation Context', () => {
180
176
  });
181
177
 
182
178
  it('handles complete workflow', async () => {
183
- // Set context
184
- mockSupabase.rpc.mockResolvedValueOnce({
185
- data: null,
186
- error: null
187
- });
188
-
179
+ // setOrganisationContext is now a no-op, so it won't call rpc
189
180
  await setOrganisationContext(mockSupabase, 'org-123');
190
181
 
191
- // Get context
182
+ // getOrganisationContext always returns null (deprecated)
192
183
  const result = await getOrganisationContext(mockSupabase);
193
-
194
184
  expect(result).toBeNull();
195
185
 
196
- // Clear context
186
+ // clearOrganisationContext still calls rpc
197
187
  mockSupabase.rpc.mockResolvedValueOnce({
198
188
  data: null,
199
189
  error: null
@@ -201,25 +191,20 @@ describe('Organisation Context', () => {
201
191
 
202
192
  await clearOrganisationContext(mockSupabase);
203
193
 
204
- expect(mockSupabase.rpc).toHaveBeenCalledTimes(2);
194
+ // Only clearOrganisationContext should call rpc (once)
195
+ expect(mockSupabase.rpc).toHaveBeenCalledTimes(1);
205
196
  });
206
197
 
207
198
  it('handles multiple organisation switches', async () => {
208
199
  const organisations = ['org-123', 'org-456', 'org-789'];
209
200
 
201
+ // setOrganisationContext is now a no-op, so it won't call rpc
210
202
  for (const orgId of organisations) {
211
- mockSupabase.rpc.mockResolvedValue({
212
- data: null,
213
- error: null
214
- });
215
-
216
203
  await setOrganisationContext(mockSupabase, orgId);
217
204
  }
218
205
 
219
- expect(mockSupabase.rpc).toHaveBeenCalledTimes(3);
220
- expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', { org_id: 'org-123' });
221
- expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', { org_id: 'org-456' });
222
- expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', { org_id: 'org-789' });
206
+ // setOrganisationContext is deprecated and doesn't call rpc anymore
207
+ expect(mockSupabase.rpc).not.toHaveBeenCalled();
223
208
  });
224
209
  });
225
210
 
@@ -16,47 +16,25 @@ const log = createLogger('organisationContext');
16
16
  /**
17
17
  * Set organisation context in the database session
18
18
  *
19
- * This function attempts to set the organisation context using a database function.
20
- * If the function is not available, it falls back gracefully without throwing errors.
19
+ * @deprecated This function is a no-op. Organisation context is now handled via:
20
+ * - Secure Supabase client headers (useSecureSupabase hook)
21
+ * - Explicit p_organisation_id parameters in RPC calls
22
+ * - RLS policies that use auth.uid() and organisation_id columns
21
23
  *
22
- * @param supabase - Supabase client instance
23
- * @param organisationId - The organisation ID to set as context
24
- * @returns Promise that resolves when context is set (or falls back gracefully)
24
+ * This function is kept for backward compatibility but does nothing.
25
+ *
26
+ * @param supabase - Supabase client instance (unused)
27
+ * @param organisationId - The organisation ID (unused)
28
+ * @returns Promise that resolves immediately
25
29
  */
26
30
  export async function setOrganisationContext(
27
31
  supabase: SupabaseClient,
28
32
  organisationId: string
29
33
  ): Promise<void> {
30
- if (!supabase || !organisationId) {
31
- // TODO: Replace with proper logging service integration
32
- return;
33
- }
34
-
35
- try {
36
- // Add timeout to prevent hanging RPC calls
37
- const timeoutPromise = new Promise((_, reject) => {
38
- setTimeout(() => reject(new Error('RPC timeout after 3 seconds')), 3000);
39
- });
40
-
41
- // Call the database function to set organisation context
42
- const rpcPromise = supabase.rpc('set_organisation_context', {
43
- org_id: organisationId
44
- });
45
-
46
- const { error } = await Promise.race([rpcPromise, timeoutPromise]) as any;
47
-
48
- if (error) {
49
- // Function might not exist yet - this is expected during migration
50
- // Silent fail - will fall back to client-side filtering
51
- log.debug('RPC function not available or failed, continuing without database context');
52
- } else {
53
- log.debug('Organisation context set in database successfully');
54
- }
55
- } catch (error) {
56
- // Handle any other errors gracefully
57
- // Silent fail - will fall back to client-side filtering
58
- log.debug('Failed to set database context, continuing without it:', error);
59
- }
34
+ // No-op: Organisation context is now handled via secure client and explicit parameters
35
+ // This function is kept for backward compatibility
36
+ log.debug('setOrganisationContext called but is a no-op - context handled via secure client');
37
+ return Promise.resolve();
60
38
  }
61
39
 
62
40
  /**
@@ -132,25 +110,16 @@ export async function getOrganisationContext(
132
110
  /**
133
111
  * Check if organisation context functions are available in the database
134
112
  *
135
- * @param supabase - Supabase client instance
136
- * @returns Promise that resolves to true if functions are available
113
+ * @deprecated This function always returns false. Organisation context functions have been removed.
114
+ * Organisation context is now handled via secure client and explicit parameters.
115
+ *
116
+ * @param supabase - Supabase client instance (unused)
117
+ * @returns Promise that resolves to false
137
118
  */
138
119
  export async function isOrganisationContextAvailable(
139
120
  supabase: SupabaseClient
140
121
  ): Promise<boolean> {
141
- if (!supabase) {
142
- return false;
143
- }
144
-
145
- try {
146
- const { error } = await supabase.rpc('get_organisation_context');
147
-
148
- if (error) {
149
- return false;
150
- }
151
-
152
- return true;
153
- } catch (error) {
154
- return false;
155
- }
122
+ // Always return false - organisation context functions have been removed
123
+ // Context is now handled via secure client and explicit parameters
124
+ return false;
156
125
  }
@@ -73,7 +73,7 @@ export const loadFormUtils = async (): Promise<{
73
73
 
74
74
  // Dynamic CSV utilities
75
75
  export const loadCSVUtils = async (): Promise<unknown> => {
76
- // @ts-ignore - papaparse is an optional dependency provided by consuming apps
76
+ // @ts-ignore - papaparse is a dependency of pace-core, should be available via node_modules
77
77
  const papaparse = await import('papaparse');
78
78
  return papaparse.default;
79
79
  };
@@ -87,6 +87,26 @@ export class FileReferenceServiceImpl implements FileReferenceService {
87
87
  log.debug('Using authenticated user ID for user-scoped file upload', { userId: authenticatedUserId });
88
88
  }
89
89
 
90
+ // CRITICAL: Check super admin status in application layer (consistent with pace-core pattern)
91
+ // Super admins bypass all permission checks - this is handled in the application layer,
92
+ // not in RLS policies. The RPC function still validates input and handles the insert,
93
+ // but permission checks are bypassed for super admins.
94
+ let isSuperAdminUser = false;
95
+ const userIdForCheck = authenticatedUserId || options.userId;
96
+ if (userIdForCheck) {
97
+ try {
98
+ // Import isSuperAdmin from rbac/api - this is the standard way to check super admin
99
+ const { isSuperAdmin } = await import('../../rbac/api');
100
+ isSuperAdminUser = await isSuperAdmin(userIdForCheck);
101
+ if (isSuperAdminUser) {
102
+ log.debug('Super admin detected - bypassing permission checks', { userId: userIdForCheck });
103
+ }
104
+ } catch (superAdminCheckError) {
105
+ // If super admin check fails, continue with normal permission flow
106
+ log.warn('Failed to check super-admin status, proceeding with normal permission checks', superAdminCheckError);
107
+ }
108
+ }
109
+
90
110
  // Step 1: Upload file to storage bucket first
91
111
  // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename} or users/{auth.uid()}/{folder}/{timestamp-uuid-filename}
92
112
  // Bucket is automatically selected based on is_public flag
@@ -123,6 +143,22 @@ export class FileReferenceServiceImpl implements FileReferenceService {
123
143
 
124
144
  // Step 4: Create file reference in database using RPC function
125
145
  // This links the storage path to the record in core_file_references table
146
+ // CRITICAL: Always pass the authenticated user ID to SECURITY DEFINER functions
147
+ // In SECURITY DEFINER functions, auth.uid() returns the function owner's ID,
148
+ // not the caller's ID, so we must explicitly pass the user ID
149
+ let rpcUserId: string | null = null;
150
+ if (authenticatedUserId) {
151
+ rpcUserId = authenticatedUserId;
152
+ } else if (options.userId) {
153
+ rpcUserId = options.userId;
154
+ } else {
155
+ // Get authenticated user ID from session as fallback
156
+ const { data: { user: authUser } } = await this.supabase.auth.getUser();
157
+ if (authUser) {
158
+ rpcUserId = authUser.id;
159
+ }
160
+ }
161
+
126
162
  const { data, error } = await this.supabase
127
163
  .rpc('data_file_reference_create', {
128
164
  p_table_name: options.table_name,
@@ -141,7 +177,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
141
177
  ...options.custom_metadata
142
178
  },
143
179
  p_is_public: options.is_public || false,
144
- p_user_id: authenticatedUserId || options.userId || null // Pass authenticated user ID for user-scoped files
180
+ p_user_id: rpcUserId // Always pass authenticated user ID for SECURITY DEFINER functions
145
181
  });
146
182
 
147
183
  // Step 5: Rollback - if database insert fails, clean up uploaded file
@@ -152,19 +188,6 @@ export class FileReferenceServiceImpl implements FileReferenceService {
152
188
 
153
189
  // Check if RPC returned null (permission denied or other failure)
154
190
  if (!data || data === null) {
155
- // Before throwing permission error, check if user is super-admin
156
- // If super-admin, the RPC should have allowed the upload, so this is likely a different issue
157
- let isSuperAdminUser = false;
158
- try {
159
- const { data: { user: authUser } } = await this.supabase.auth.getUser();
160
- if (authUser) {
161
- isSuperAdminUser = await isSuperAdmin(authUser.id);
162
- }
163
- } catch (superAdminCheckError) {
164
- // If super-admin check fails, continue with permission error
165
- log.warn('Failed to check super-admin status', superAdminCheckError);
166
- }
167
-
168
191
  // Clean up the uploaded file since DB insert failed
169
192
  await deleteFile(this.supabase, filePath, options.is_public || false);
170
193
 
@@ -173,7 +196,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
173
196
  const pageContextDisplay = options.pageContext || 'undefined';
174
197
 
175
198
  if (isSuperAdminUser) {
176
- // Super-admin should have been allowed - this suggests a different issue
199
+ // Super-admin should have been allowed - this suggests a database or RPC function issue
200
+ // Since we already checked super admin in the application layer, this is unexpected
177
201
  throw new Error(
178
202
  `File upload failed for super-admin user. This may indicate a database issue. ` +
179
203
  `Page context: '${pageContextDisplay}', App: '${appName}'. ` +
@@ -161,8 +161,9 @@ describe('formatDateTime Utility', () => {
161
161
  }
162
162
  const end = performance.now();
163
163
 
164
- // Should complete in reasonable time (less than 100ms for 1000 calls)
165
- expect(end - start).toBeLessThan(100);
164
+ // Should complete in reasonable time (less than 200ms for 1000 calls in test environment)
165
+ // Increased threshold to account for test environment overhead
166
+ expect(end - start).toBeLessThan(200);
166
167
  });
167
168
  });
168
169
 
@@ -161,8 +161,9 @@ describe('formatTime Utility', () => {
161
161
  }
162
162
  const end = performance.now();
163
163
 
164
- // Should complete in reasonable time (less than 100ms for 1000 calls)
165
- expect(end - start).toBeLessThan(100);
164
+ // Should complete in reasonable time (less than 200ms for 1000 calls)
165
+ // Increased threshold to account for test environment variability
166
+ expect(end - start).toBeLessThan(200);
166
167
  });
167
168
  });
168
169
 
@@ -127,13 +127,19 @@ export function loadGoogleMapsScript(
127
127
  return;
128
128
  }
129
129
 
130
- // Check if script is already being loaded
130
+ // Check if script is already being loaded or exists
131
131
  const existingScript = document.querySelector(
132
132
  `script[src*="maps.googleapis.com/maps/api/js"]`
133
133
  );
134
134
  if (existingScript) {
135
+ // If Google Maps is already loaded, resolve immediately
136
+ if (window.google?.maps?.places) {
137
+ resolve();
138
+ return;
139
+ }
140
+
135
141
  // Wait for existing script to load
136
- existingScript.addEventListener('load', () => {
142
+ const handleLoad = () => {
137
143
  // Wait for the library to initialize with multiple retries
138
144
  let attempts = 0;
139
145
  const maxAttempts = 20; // 2 seconds total
@@ -150,10 +156,26 @@ export function loadGoogleMapsScript(
150
156
  };
151
157
 
152
158
  checkPlaces();
153
- });
159
+ };
160
+
161
+ // Check if script already loaded
162
+ const scriptElement = existingScript as HTMLScriptElement;
163
+ if (scriptElement.getAttribute('data-loaded') === 'true') {
164
+ handleLoad();
165
+ return;
166
+ }
167
+
168
+ // Check if script is already complete (using type assertion for readyState which exists at runtime)
169
+ const scriptReadyState = (scriptElement as any).readyState;
170
+ if (scriptReadyState === 'complete' || scriptReadyState === 'loaded') {
171
+ handleLoad();
172
+ return;
173
+ }
174
+
175
+ existingScript.addEventListener('load', handleLoad, { once: true });
154
176
  existingScript.addEventListener('error', () => {
155
177
  reject(new Error('Failed to load Google Maps script'));
156
- });
178
+ }, { once: true });
157
179
  return;
158
180
  }
159
181
 
@@ -164,6 +186,9 @@ export function loadGoogleMapsScript(
164
186
  script.defer = true;
165
187
 
166
188
  script.onload = () => {
189
+ // Mark script as loaded
190
+ script.setAttribute('data-loaded', 'true');
191
+
167
192
  // Wait for the library to initialize with multiple retries
168
193
  let attempts = 0;
169
194
  const maxAttempts = 20; // 2 seconds total (20 * 100ms)
@@ -21,7 +21,7 @@ export * from './validation';
21
21
  // Explicitly re-export commonly used validation utilities to ensure they're available
22
22
  // Import from source modules to avoid re-export issues
23
23
  export { validateUserInput, usernameSchema } from './validation/validationUtils';
24
- export { sanitizeUserInput, sanitizeFormData } from './validation/sanitization';
24
+ export { sanitizeUserInput, sanitizeFormData, sanitizeHtml } from './validation/sanitization';
25
25
  export { emailSchema, nameSchema, phoneSchema, urlSchema } from './validation/common';
26
26
  export { passwordSchema } from './validation/passwordSchema';
27
27
  export { pickSchema, combineSchemas } from './validation/schema';
@@ -178,3 +178,6 @@ export {
178
178
  getInFlightRequestStats,
179
179
  deduplicatedQuery
180
180
  } from './request-deduplication';
181
+
182
+ // Supabase client creation (restricted wrapper)
183
+ export { createBaseClient } from './supabase/createBaseClient';
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @file Key Derivation Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Persistence/__tests__
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ deriveDataTableKey,
10
+ deriveDialogKey,
11
+ deriveFormKey,
12
+ hashStableFingerprint,
13
+ } from '../keyDerivation';
14
+
15
+ describe('keyDerivation', () => {
16
+ describe('deriveDataTableKey', () => {
17
+ it('should use rbacPageId as primary key', () => {
18
+ const key = deriveDataTableKey({
19
+ rbacPageId: 'user-management',
20
+ });
21
+ expect(key).toBe('datatable:user-management');
22
+ });
23
+
24
+ it('should fall back to title when rbacPageId not available', () => {
25
+ const key = deriveDataTableKey({
26
+ title: 'Users Table',
27
+ });
28
+ expect(key).toBe('datatable:users-table');
29
+ });
30
+
31
+ it('should use route pathname as fallback', () => {
32
+ const key = deriveDataTableKey(
33
+ {},
34
+ { pathname: '/users' }
35
+ );
36
+ expect(key).toBe('datatable:/users');
37
+ });
38
+
39
+ it('should use column IDs hash as last resort', () => {
40
+ const key = deriveDataTableKey({
41
+ columnIds: ['id', 'name', 'email'],
42
+ });
43
+ expect(key).toMatch(/^datatable:[a-f0-9]+$/);
44
+ });
45
+
46
+ it('should return null if no stable key can be determined', () => {
47
+ const key = deriveDataTableKey({});
48
+ expect(key).toBeNull();
49
+ });
50
+ });
51
+
52
+ describe('deriveDialogKey', () => {
53
+ it('should use title as primary key', () => {
54
+ const key = deriveDialogKey({
55
+ title: 'Edit User',
56
+ });
57
+ expect(key).toBe('dialog:edit-user');
58
+ });
59
+
60
+ it('should use route pathname as fallback', () => {
61
+ const key = deriveDialogKey(
62
+ {},
63
+ { pathname: '/users/edit' }
64
+ );
65
+ expect(key).toBe('dialog:/users/edit');
66
+ });
67
+
68
+ it('should return null if no stable key can be determined', () => {
69
+ const key = deriveDialogKey({});
70
+ expect(key).toBeNull();
71
+ });
72
+ });
73
+
74
+ describe('deriveFormKey', () => {
75
+ it('should use parent dialog title when available', () => {
76
+ const key = deriveFormKey(
77
+ {},
78
+ { dialogTitle: 'Edit User' }
79
+ );
80
+ expect(key).toBe('form:edit-user');
81
+ });
82
+
83
+ it('should use route + field names hash as fallback', () => {
84
+ const key = deriveFormKey(
85
+ {
86
+ fieldNames: ['name', 'email'],
87
+ },
88
+ null,
89
+ { pathname: '/users' }
90
+ );
91
+ expect(key).toMatch(/^form:\/users:[a-f0-9]+$/);
92
+ });
93
+
94
+ it('should use route pathname as last resort', () => {
95
+ const key = deriveFormKey(
96
+ {},
97
+ null,
98
+ { pathname: '/users' }
99
+ );
100
+ expect(key).toBe('form:/users');
101
+ });
102
+
103
+ it('should return null if no stable key can be determined', () => {
104
+ const key = deriveFormKey({});
105
+ expect(key).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe('hashStableFingerprint', () => {
110
+ it('should create stable hash from array of strings', () => {
111
+ const hash1 = hashStableFingerprint(['a', 'b', 'c']);
112
+ const hash2 = hashStableFingerprint(['a', 'b', 'c']);
113
+ expect(hash1).toBe(hash2);
114
+ });
115
+
116
+ it('should be order-independent', () => {
117
+ const hash1 = hashStableFingerprint(['a', 'b', 'c']);
118
+ const hash2 = hashStableFingerprint(['c', 'b', 'a']);
119
+ expect(hash1).toBe(hash2);
120
+ });
121
+
122
+ it('should normalize strings (lowercase, trim)', () => {
123
+ const hash1 = hashStableFingerprint(['A', ' B ', 'C']);
124
+ const hash2 = hashStableFingerprint(['a', 'b', 'c']);
125
+ expect(hash1).toBe(hash2);
126
+ });
127
+
128
+ it('should filter out empty values', () => {
129
+ const hash1 = hashStableFingerprint(['a', '', 'b', 'c']);
130
+ const hash2 = hashStableFingerprint(['a', 'b', 'c']);
131
+ expect(hash1).toBe(hash2);
132
+ });
133
+ });
134
+ });
135
+
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @file Sensitive Field Detection Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Persistence/__tests__
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ isSensitiveField,
10
+ filterSensitiveFields,
11
+ getSensitiveFieldNames,
12
+ } from '../sensitiveFieldDetection';
13
+
14
+ describe('sensitiveFieldDetection', () => {
15
+ describe('isSensitiveField', () => {
16
+ it('should detect password input type', () => {
17
+ expect(isSensitiveField('password', 'password')).toBe(true);
18
+ expect(isSensitiveField('userPassword', 'password')).toBe(true);
19
+ });
20
+
21
+ it('should detect hidden input type', () => {
22
+ expect(isSensitiveField('token', 'hidden')).toBe(true);
23
+ });
24
+
25
+ it('should detect sensitive field names by pattern', () => {
26
+ expect(isSensitiveField('password', 'text')).toBe(true);
27
+ expect(isSensitiveField('user_password', 'text')).toBe(true);
28
+ expect(isSensitiveField('api_key', 'text')).toBe(true);
29
+ expect(isSensitiveField('secretToken', 'text')).toBe(true);
30
+ expect(isSensitiveField('credit_card', 'text')).toBe(true);
31
+ expect(isSensitiveField('ssn', 'text')).toBe(true);
32
+ });
33
+
34
+ it('should not detect non-sensitive fields', () => {
35
+ expect(isSensitiveField('name', 'text')).toBe(false);
36
+ expect(isSensitiveField('email', 'email')).toBe(false);
37
+ expect(isSensitiveField('age', 'number')).toBe(false);
38
+ });
39
+
40
+ it('should be case-insensitive', () => {
41
+ expect(isSensitiveField('PASSWORD', 'text')).toBe(true);
42
+ expect(isSensitiveField('Api_Key', 'text')).toBe(true);
43
+ });
44
+ });
45
+
46
+ describe('filterSensitiveFields', () => {
47
+ it('should filter out sensitive fields', () => {
48
+ const data = {
49
+ name: 'John',
50
+ email: 'john@example.com',
51
+ password: 'secret123',
52
+ api_key: 'key123',
53
+ };
54
+
55
+ const filtered = filterSensitiveFields(data, ['name', 'email', 'password', 'api_key']);
56
+
57
+ expect(filtered).toEqual({
58
+ name: 'John',
59
+ email: 'john@example.com',
60
+ });
61
+ expect(filtered).not.toHaveProperty('password');
62
+ expect(filtered).not.toHaveProperty('api_key');
63
+ });
64
+
65
+ it('should filter by input type', () => {
66
+ const data = {
67
+ name: 'John',
68
+ password: 'secret123',
69
+ };
70
+
71
+ const filtered = filterSensitiveFields(data, ['name', 'password'], {
72
+ password: 'password',
73
+ });
74
+
75
+ expect(filtered).toEqual({
76
+ name: 'John',
77
+ });
78
+ expect(filtered).not.toHaveProperty('password');
79
+ });
80
+
81
+ it('should return empty object if all fields are sensitive', () => {
82
+ const data = {
83
+ password: 'secret123',
84
+ api_key: 'key123',
85
+ };
86
+
87
+ const filtered = filterSensitiveFields(data, ['password', 'api_key']);
88
+
89
+ expect(filtered).toEqual({});
90
+ });
91
+
92
+ it('should return all fields if none are sensitive', () => {
93
+ const data = {
94
+ name: 'John',
95
+ email: 'john@example.com',
96
+ };
97
+
98
+ const filtered = filterSensitiveFields(data, ['name', 'email']);
99
+
100
+ expect(filtered).toEqual(data);
101
+ });
102
+ });
103
+
104
+ describe('getSensitiveFieldNames', () => {
105
+ it('should return list of sensitive field names', () => {
106
+ const sensitive = getSensitiveFieldNames(
107
+ ['name', 'email', 'password', 'api_key'],
108
+ {
109
+ password: 'password',
110
+ }
111
+ );
112
+
113
+ expect(sensitive).toEqual(['password', 'api_key']);
114
+ });
115
+
116
+ it('should return empty array if no sensitive fields', () => {
117
+ const sensitive = getSensitiveFieldNames(['name', 'email']);
118
+
119
+ expect(sensitive).toEqual([]);
120
+ });
121
+ });
122
+ });
123
+