@jmruthers/pace-core 0.5.136 → 0.5.139

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 (292) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
  8. package/dist/chunk-5JMOHWDI.js.map +1 -0
  9. package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
  10. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  11. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  12. package/dist/chunk-7QCC6MCP.js.map +1 -0
  13. package/dist/chunk-BJPBT3CU.js +21 -0
  14. package/dist/chunk-BJPBT3CU.js.map +1 -0
  15. package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
  16. package/dist/chunk-BOOI7GK2.js.map +1 -0
  17. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  18. package/dist/chunk-JISYG63F.js +70 -0
  19. package/dist/chunk-JISYG63F.js.map +1 -0
  20. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  21. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  22. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  23. package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
  24. package/dist/chunk-TLT2ZR3L.js.map +1 -0
  25. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  26. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  27. package/dist/components.d.ts +1 -1
  28. package/dist/components.js +12 -11
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.js +8 -9
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.js +3 -4
  36. package/dist/rbac/index.js +8 -9
  37. package/dist/schema-DTDZQe2u.d.ts +28 -0
  38. package/dist/types.d.ts +152 -3
  39. package/dist/types.js +51 -16
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils.d.ts +89 -4
  42. package/dist/utils.js +214 -96
  43. package/dist/utils.js.map +1 -1
  44. package/dist/validation.d.ts +1 -343
  45. package/dist/validation.js +3 -100
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +1 -1
  60. package/docs/api/enums/FileCategory.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/BadgeProps.md +27 -0
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/EventLogoProps.md +1 -1
  77. package/docs/api/interfaces/ExportColumn.md +1 -1
  78. package/docs/api/interfaces/ExportOptions.md +1 -1
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  128. package/docs/api/interfaces/StorageConfig.md +1 -1
  129. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  130. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  131. package/docs/api/interfaces/StorageListOptions.md +1 -1
  132. package/docs/api/interfaces/StorageListResult.md +1 -1
  133. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  134. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  135. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  136. package/docs/api/interfaces/StyleImport.md +1 -1
  137. package/docs/api/interfaces/SwitchProps.md +1 -1
  138. package/docs/api/interfaces/ToastActionElement.md +1 -1
  139. package/docs/api/interfaces/ToastProps.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  141. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  143. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  145. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  147. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  148. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  150. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  151. package/docs/api/interfaces/UserEventAccess.md +1 -1
  152. package/docs/api/interfaces/UserMenuProps.md +1 -1
  153. package/docs/api/interfaces/UserProfile.md +1 -1
  154. package/docs/api/modules.md +84 -15
  155. package/docs/architecture/README.md +0 -1
  156. package/docs/styles/README.md +0 -2
  157. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  158. package/examples/RBAC/EventBasedApp.tsx +239 -0
  159. package/examples/RBAC/PermissionExample.tsx +151 -0
  160. package/examples/RBAC/index.ts +13 -0
  161. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  162. package/examples/public-pages/PublicEventPage.tsx +274 -0
  163. package/examples/public-pages/PublicPageApp.tsx +308 -0
  164. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  165. package/examples/public-pages/index.ts +14 -0
  166. package/package.json +1 -10
  167. package/src/__tests__/TEST_STANDARD.md +92 -0
  168. package/src/components/Badge/Badge.test.tsx +314 -0
  169. package/src/components/Badge/Badge.tsx +304 -0
  170. package/src/components/Badge/index.ts +3 -0
  171. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  172. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  173. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  174. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  175. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  176. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  177. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  178. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  179. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  180. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  181. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  182. package/src/components/DataTable/core/ActionManager.ts +235 -0
  183. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  184. package/src/components/DataTable/core/DataManager.ts +188 -0
  185. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  186. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  187. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  188. package/src/components/DataTable/core/StateManager.ts +311 -0
  189. package/src/components/DataTable/core/interfaces.ts +338 -0
  190. package/src/components/DataTable/styles.ts +27 -6
  191. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  192. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  193. package/src/components/DataTable/utils/debugTools.ts +609 -0
  194. package/src/components/DataTable/utils/index.ts +1 -0
  195. package/src/components/Dialog/README.md +804 -0
  196. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  197. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  198. package/src/components/Footer/Footer.test.tsx +1 -1
  199. package/src/components/Form/Form.test.tsx +1 -1
  200. package/src/components/Form/FormErrorSummary.tsx +113 -0
  201. package/src/components/Form/FormFieldset.tsx +127 -0
  202. package/src/components/Form/FormLiveRegion.tsx +198 -0
  203. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  204. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  205. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  206. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  207. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  208. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  209. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  210. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  211. package/src/components/Select/Select.test.tsx +1 -1
  212. package/src/components/Select/Select.tsx +20 -8
  213. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  214. package/src/components/index.ts +3 -0
  215. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  216. package/src/index.ts +4 -0
  217. package/src/rbac/hooks/useCan.test.ts +24 -0
  218. package/src/rbac/hooks/usePermissions.ts +49 -12
  219. package/src/styles/core.css +3 -0
  220. package/src/utils/appConfig.ts +47 -0
  221. package/src/utils/appIdResolver.test.ts +499 -0
  222. package/src/utils/appIdResolver.ts +130 -0
  223. package/src/utils/appNameResolver.simple.test.ts +212 -0
  224. package/src/utils/appNameResolver.test.ts +121 -0
  225. package/src/utils/appNameResolver.ts +191 -0
  226. package/src/utils/audit.ts +127 -0
  227. package/src/utils/auth-utils.ts +96 -0
  228. package/src/utils/bundleAnalysis.ts +129 -0
  229. package/src/utils/cn.ts +7 -0
  230. package/src/utils/debugLogger.ts +67 -0
  231. package/src/utils/deviceFingerprint.ts +215 -0
  232. package/src/utils/dynamicUtils.ts +105 -0
  233. package/src/utils/file-reference.test.ts +788 -0
  234. package/src/utils/file-reference.ts +519 -0
  235. package/src/utils/formatDate.test.ts +237 -0
  236. package/src/utils/formatting.ts +133 -0
  237. package/src/utils/index.ts +7 -0
  238. package/src/utils/lazyLoad.tsx +44 -0
  239. package/src/utils/logger.ts +179 -0
  240. package/src/utils/organisationContext.test.ts +322 -0
  241. package/src/utils/organisationContext.ts +153 -0
  242. package/src/utils/performanceBenchmark.ts +64 -0
  243. package/src/utils/performanceBudgets.ts +110 -0
  244. package/src/utils/permissionTypes.ts +37 -0
  245. package/src/utils/permissionUtils.test.ts +393 -0
  246. package/src/utils/permissionUtils.ts +34 -0
  247. package/src/utils/sanitization.ts +264 -0
  248. package/src/utils/schemaUtils.ts +37 -0
  249. package/src/utils/secureDataAccess.test.ts +711 -0
  250. package/src/utils/secureDataAccess.ts +377 -0
  251. package/src/utils/secureErrors.ts +79 -0
  252. package/src/utils/secureStorage.ts +244 -0
  253. package/src/utils/security.ts +156 -0
  254. package/src/utils/securityMonitor.ts +45 -0
  255. package/src/utils/sessionTracking.ts +126 -0
  256. package/src/utils/validation.ts +111 -0
  257. package/src/utils/validationUtils.ts +120 -0
  258. package/src/validation/index.ts +2 -2
  259. package/dist/chunk-444EZN6N.js.map +0 -1
  260. package/dist/chunk-APIBCTL2.js +0 -670
  261. package/dist/chunk-APIBCTL2.js.map +0 -1
  262. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  263. package/dist/chunk-K2WWTH7O.js +0 -94
  264. package/dist/chunk-K2WWTH7O.js.map +0 -1
  265. package/dist/chunk-L6PGMCMD.js.map +0 -1
  266. package/dist/chunk-LMC26NLJ.js +0 -84
  267. package/dist/chunk-LMC26NLJ.js.map +0 -1
  268. package/dist/chunk-NOHEVYVX.js.map +0 -1
  269. package/dist/chunk-TVYPTYOY.js.map +0 -1
  270. package/dist/validation-8npbysjg.d.ts +0 -177
  271. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
  272. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  273. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  274. /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
  275. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  276. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  277. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  278. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  279. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  280. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  282. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  283. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  284. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  285. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  289. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  290. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  291. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  292. /package/examples/{components → components 2}/index.ts +0 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * @file Permission Utils Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/PermissionUtils
5
+ * @since 1.0.0
6
+ *
7
+ * Comprehensive tests for permission utility functions covering all critical functionality.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import {
12
+ transformPermissionMapToBoolean,
13
+ hasPermission,
14
+ hasAnyPermission,
15
+ hasAllPermissions
16
+ } from './permissionUtils';
17
+
18
+ describe('Permission Utils', () => {
19
+ describe('transformPermissionMapToBoolean', () => {
20
+ it('transforms string values correctly', () => {
21
+ const permissions = {
22
+ 'read:users': 'true',
23
+ 'write:users': 'false',
24
+ 'delete:users': '',
25
+ 'manage:users': 'yes',
26
+ 'view:users': 'no'
27
+ };
28
+
29
+ const result = transformPermissionMapToBoolean(permissions);
30
+
31
+ expect(result).toEqual({
32
+ 'read:users': true,
33
+ 'write:users': false,
34
+ 'delete:users': false,
35
+ 'manage:users': true,
36
+ 'view:users': true
37
+ });
38
+ });
39
+
40
+ it('transforms boolean values correctly', () => {
41
+ const permissions = {
42
+ 'read:users': true,
43
+ 'write:users': false,
44
+ 'delete:users': true,
45
+ 'manage:users': false
46
+ };
47
+
48
+ const result = transformPermissionMapToBoolean(permissions);
49
+
50
+ expect(result).toEqual({
51
+ 'read:users': true,
52
+ 'write:users': false,
53
+ 'delete:users': true,
54
+ 'manage:users': false
55
+ });
56
+ });
57
+
58
+ it('transforms mixed types correctly', () => {
59
+ const permissions = {
60
+ 'read:users': 'true',
61
+ 'write:users': false,
62
+ 'delete:users': 1,
63
+ 'manage:users': 0,
64
+ 'view:users': null,
65
+ 'edit:users': undefined
66
+ };
67
+
68
+ const result = transformPermissionMapToBoolean(permissions);
69
+
70
+ expect(result).toEqual({
71
+ 'read:users': true,
72
+ 'write:users': false,
73
+ 'delete:users': true,
74
+ 'manage:users': false,
75
+ 'view:users': false,
76
+ 'edit:users': false
77
+ });
78
+ });
79
+
80
+ it('handles empty object', () => {
81
+ const permissions = {};
82
+
83
+ const result = transformPermissionMapToBoolean(permissions);
84
+
85
+ expect(result).toEqual({});
86
+ });
87
+
88
+ it('handles null and undefined values', () => {
89
+ const permissions = {
90
+ 'read:users': null,
91
+ 'write:users': undefined,
92
+ 'delete:users': false,
93
+ 'manage:users': true
94
+ };
95
+
96
+ const result = transformPermissionMapToBoolean(permissions);
97
+
98
+ expect(result).toEqual({
99
+ 'read:users': false,
100
+ 'write:users': false,
101
+ 'delete:users': false,
102
+ 'manage:users': true
103
+ });
104
+ });
105
+
106
+ it('handles numeric values', () => {
107
+ const permissions = {
108
+ 'read:users': 1,
109
+ 'write:users': 0,
110
+ 'delete:users': -1,
111
+ 'manage:users': 42
112
+ };
113
+
114
+ const result = transformPermissionMapToBoolean(permissions);
115
+
116
+ expect(result).toEqual({
117
+ 'read:users': true,
118
+ 'write:users': false,
119
+ 'delete:users': true,
120
+ 'manage:users': true
121
+ });
122
+ });
123
+
124
+ it('handles case-insensitive string values', () => {
125
+ const permissions = {
126
+ 'read:users': 'TRUE',
127
+ 'write:users': 'FALSE',
128
+ 'delete:users': 'True',
129
+ 'manage:users': 'False'
130
+ };
131
+
132
+ const result = transformPermissionMapToBoolean(permissions);
133
+
134
+ expect(result).toEqual({
135
+ 'read:users': true,
136
+ 'write:users': false,
137
+ 'delete:users': true,
138
+ 'manage:users': false
139
+ });
140
+ });
141
+ });
142
+
143
+ describe('hasPermission', () => {
144
+ it('returns true for granted permission', () => {
145
+ const permissions = {
146
+ 'read:users': true,
147
+ 'write:users': false,
148
+ 'delete:users': true
149
+ };
150
+
151
+ expect(hasPermission(permissions, 'read:users')).toBe(true);
152
+ expect(hasPermission(permissions, 'delete:users')).toBe(true);
153
+ });
154
+
155
+ it('returns false for denied permission', () => {
156
+ const permissions = {
157
+ 'read:users': true,
158
+ 'write:users': false,
159
+ 'delete:users': true
160
+ };
161
+
162
+ expect(hasPermission(permissions, 'write:users')).toBe(false);
163
+ });
164
+
165
+ it('returns false for non-existent permission', () => {
166
+ const permissions = {
167
+ 'read:users': true,
168
+ 'write:users': false
169
+ };
170
+
171
+ expect(hasPermission(permissions, 'manage:users')).toBe(false);
172
+ });
173
+
174
+ it('handles empty permissions object', () => {
175
+ const permissions = {};
176
+
177
+ expect(hasPermission(permissions, 'read:users')).toBe(false);
178
+ });
179
+
180
+ it('handles null/undefined permission', () => {
181
+ const permissions = {
182
+ 'read:users': true,
183
+ 'write:users': false
184
+ };
185
+
186
+ expect(hasPermission(permissions, null as any)).toBe(false);
187
+ expect(hasPermission(permissions, undefined as any)).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe('hasAnyPermission', () => {
192
+ it('returns true when at least one permission is granted', () => {
193
+ const permissions = {
194
+ 'read:users': true,
195
+ 'write:users': false,
196
+ 'delete:users': false
197
+ };
198
+
199
+ expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(true);
200
+ expect(hasAnyPermission(permissions, ['write:users', 'delete:users', 'read:users'])).toBe(true);
201
+ });
202
+
203
+ it('returns false when no permissions are granted', () => {
204
+ const permissions = {
205
+ 'read:users': false,
206
+ 'write:users': false,
207
+ 'delete:users': false
208
+ };
209
+
210
+ expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(false);
211
+ });
212
+
213
+ it('returns false for empty permission list', () => {
214
+ const permissions = {
215
+ 'read:users': true,
216
+ 'write:users': false
217
+ };
218
+
219
+ expect(hasAnyPermission(permissions, [])).toBe(false);
220
+ });
221
+
222
+ it('returns false when permission list contains non-existent permissions', () => {
223
+ const permissions = {
224
+ 'read:users': true,
225
+ 'write:users': false
226
+ };
227
+
228
+ expect(hasAnyPermission(permissions, ['manage:users', 'admin:users'])).toBe(false);
229
+ });
230
+
231
+ it('handles mixed permission states', () => {
232
+ const permissions = {
233
+ 'read:users': true,
234
+ 'write:users': false,
235
+ 'delete:users': true,
236
+ 'manage:users': false
237
+ };
238
+
239
+ expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(true);
240
+ expect(hasAnyPermission(permissions, ['write:users', 'manage:users'])).toBe(false);
241
+ expect(hasAnyPermission(permissions, ['delete:users', 'manage:users'])).toBe(true);
242
+ });
243
+
244
+ it('handles null/undefined permission list', () => {
245
+ const permissions = {
246
+ 'read:users': true,
247
+ 'write:users': false
248
+ };
249
+
250
+ expect(hasAnyPermission(permissions, null as any)).toBe(false);
251
+ expect(hasAnyPermission(permissions, undefined as any)).toBe(false);
252
+ });
253
+ });
254
+
255
+ describe('hasAllPermissions', () => {
256
+ it('returns true when all permissions are granted', () => {
257
+ const permissions = {
258
+ 'read:users': true,
259
+ 'write:users': true,
260
+ 'delete:users': true
261
+ };
262
+
263
+ expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(true);
264
+ });
265
+
266
+ it('returns false when at least one permission is denied', () => {
267
+ const permissions = {
268
+ 'read:users': true,
269
+ 'write:users': false,
270
+ 'delete:users': true
271
+ };
272
+
273
+ expect(hasAllPermissions(permissions, ['read:users', 'write:users'])).toBe(false);
274
+ expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(false);
275
+ });
276
+
277
+ it('returns true for empty permission list', () => {
278
+ const permissions = {
279
+ 'read:users': true,
280
+ 'write:users': false
281
+ };
282
+
283
+ expect(hasAllPermissions(permissions, [])).toBe(true);
284
+ });
285
+
286
+ it('returns false when permission list contains non-existent permissions', () => {
287
+ const permissions = {
288
+ 'read:users': true,
289
+ 'write:users': true
290
+ };
291
+
292
+ expect(hasAllPermissions(permissions, ['read:users', 'manage:users'])).toBe(false);
293
+ });
294
+
295
+ it('handles mixed permission states', () => {
296
+ const permissions = {
297
+ 'read:users': true,
298
+ 'write:users': true,
299
+ 'delete:users': false,
300
+ 'manage:users': true
301
+ };
302
+
303
+ expect(hasAllPermissions(permissions, ['read:users', 'write:users'])).toBe(true);
304
+ expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(false);
305
+ expect(hasAllPermissions(permissions, ['read:users', 'manage:users'])).toBe(true);
306
+ });
307
+
308
+ it('handles null/undefined permission list', () => {
309
+ const permissions = {
310
+ 'read:users': true,
311
+ 'write:users': false
312
+ };
313
+
314
+ expect(hasAllPermissions(permissions, null as any)).toBe(false);
315
+ expect(hasAllPermissions(permissions, undefined as any)).toBe(false);
316
+ });
317
+ });
318
+
319
+ describe('Edge Cases', () => {
320
+ it('handles very large permission objects', () => {
321
+ const permissions: Record<string, boolean> = {};
322
+ for (let i = 0; i < 1000; i++) {
323
+ permissions[`permission:${i}`] = i % 2 === 0;
324
+ }
325
+
326
+ const result = transformPermissionMapToBoolean(permissions);
327
+ expect(Object.keys(result)).toHaveLength(1000);
328
+ expect(result['permission:0']).toBe(true);
329
+ expect(result['permission:1']).toBe(false);
330
+ });
331
+
332
+ it('handles special characters in permission names', () => {
333
+ const permissions = {
334
+ 'read:users@domain.com': true,
335
+ 'write:users#special': false,
336
+ 'delete:users$with$symbols': true,
337
+ 'manage:users.with.dots': false
338
+ };
339
+
340
+ const result = transformPermissionMapToBoolean(permissions);
341
+ expect(result['read:users@domain.com']).toBe(true);
342
+ expect(result['write:users#special']).toBe(false);
343
+ expect(result['delete:users$with$symbols']).toBe(true);
344
+ expect(result['manage:users.with.dots']).toBe(false);
345
+ });
346
+
347
+ it('handles unicode characters in permission names', () => {
348
+ const permissions = {
349
+ 'read:用户': true,
350
+ 'write:ユーザー': false,
351
+ 'delete:مستخدم': true
352
+ };
353
+
354
+ const result = transformPermissionMapToBoolean(permissions);
355
+ expect(result['read:用户']).toBe(true);
356
+ expect(result['write:ユーザー']).toBe(false);
357
+ expect(result['delete:مستخدم']).toBe(true);
358
+ });
359
+ });
360
+
361
+ describe('Type Safety', () => {
362
+ it('maintains type safety with TypeScript', () => {
363
+ const permissions: Record<string, boolean> = {
364
+ 'read:users': true,
365
+ 'write:users': false
366
+ };
367
+
368
+ // These should compile without errors
369
+ const hasRead = hasPermission(permissions, 'read:users');
370
+ const hasAny = hasAnyPermission(permissions, ['read:users', 'write:users']);
371
+ const hasAll = hasAllPermissions(permissions, ['read:users']);
372
+
373
+ expect(typeof hasRead).toBe('boolean');
374
+ expect(typeof hasAny).toBe('boolean');
375
+ expect(typeof hasAll).toBe('boolean');
376
+ });
377
+
378
+ it('handles mixed input types gracefully', () => {
379
+ const permissions = {
380
+ 'read:users': 'true',
381
+ 'write:users': 1,
382
+ 'delete:users': false,
383
+ 'manage:users': null
384
+ };
385
+
386
+ const result = transformPermissionMapToBoolean(permissions);
387
+ expect(typeof result['read:users']).toBe('boolean');
388
+ expect(typeof result['write:users']).toBe('boolean');
389
+ expect(typeof result['delete:users']).toBe('boolean');
390
+ expect(typeof result['manage:users']).toBe('boolean');
391
+ });
392
+ });
393
+ });
@@ -0,0 +1,34 @@
1
+
2
+ /**
3
+ * Permission utilities for transforming and managing permissions
4
+ */
5
+
6
+ export function transformPermissionMapToBoolean(permissions: Record<string, unknown>): Record<string, boolean> {
7
+ const result: Record<string, boolean> = {};
8
+
9
+ Object.entries(permissions).forEach(([key, value]) => {
10
+ if (typeof value === 'string') {
11
+ // Handle string values - 'false' should be false, empty strings are false, other non-empty strings are true
12
+ result[key] = value !== '' && value.toLowerCase() !== 'false';
13
+ } else {
14
+ result[key] = Boolean(value);
15
+ }
16
+ });
17
+
18
+ return result;
19
+ }
20
+
21
+ export function hasPermission(permissions: Record<string, boolean>, permission: string): boolean {
22
+ return Boolean(permissions[permission]);
23
+ }
24
+
25
+ export function hasAnyPermission(permissions: Record<string, boolean>, permissionList: string[] | null | undefined): boolean {
26
+ if (!permissionList || permissionList.length === 0) return false;
27
+ return permissionList.some(permission => hasPermission(permissions, permission));
28
+ }
29
+
30
+ export function hasAllPermissions(permissions: Record<string, boolean>, permissionList: string[] | null | undefined): boolean {
31
+ if (!permissionList) return false;
32
+ if (permissionList.length === 0) return true;
33
+ return permissionList.every(permission => hasPermission(permissions, permission));
34
+ }
@@ -0,0 +1,264 @@
1
+
2
+ /**
3
+ * @file Input Sanitization Layer
4
+ * @package @jmruthers/pace-core
5
+ * @module Security
6
+ * @since 0.1.0
7
+ *
8
+ * Comprehensive input sanitization utilities to prevent XSS, injection attacks,
9
+ * and other security vulnerabilities.
10
+ */
11
+
12
+ import { z } from 'zod';
13
+
14
+ /**
15
+ * Sanitization options for different contexts
16
+ */
17
+ export interface SanitizationOptions {
18
+ allowHtml?: boolean;
19
+ allowedTags?: string[];
20
+ maxLength?: number;
21
+ trim?: boolean;
22
+ removeScripts?: boolean;
23
+ removeEvents?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Default sanitization options
28
+ */
29
+ const DEFAULT_OPTIONS: SanitizationOptions = {
30
+ allowHtml: false,
31
+ allowedTags: [],
32
+ maxLength: 1000,
33
+ trim: true,
34
+ removeScripts: true,
35
+ removeEvents: true
36
+ };
37
+
38
+ /**
39
+ * Sanitizes user input by removing potentially dangerous characters and patterns
40
+ */
41
+ export function sanitizeUserInput(input: string, options: SanitizationOptions = {}): string {
42
+ if (typeof input !== 'string') {
43
+ return '';
44
+ }
45
+
46
+ const opts = { ...DEFAULT_OPTIONS, ...options };
47
+ let sanitized = input;
48
+
49
+ // Trim whitespace if requested
50
+ if (opts.trim) {
51
+ sanitized = sanitized.trim();
52
+ }
53
+
54
+ // Enforce maximum length
55
+ if (opts.maxLength && sanitized.length > opts.maxLength) {
56
+ sanitized = sanitized.substring(0, opts.maxLength);
57
+ }
58
+
59
+ // Remove or escape HTML if not allowed
60
+ if (!opts.allowHtml) {
61
+ sanitized = sanitized
62
+ .replace(/</g, '&lt;')
63
+ .replace(/>/g, '&gt;')
64
+ .replace(/"/g, '&quot;')
65
+ .replace(/'/g, '&#x27;')
66
+ .replace(/\//g, '&#x2F;');
67
+ } else if (opts.allowedTags && opts.allowedTags.length > 0) {
68
+ // If HTML is allowed, only permit specific tags
69
+ const allowedTagsRegex = new RegExp(`<(?!\/?(?:${opts.allowedTags.join('|')})\s*\/?>)[^>]+>`, 'gi');
70
+ sanitized = sanitized.replace(allowedTagsRegex, '');
71
+ }
72
+
73
+ // Remove script tags and javascript: protocols
74
+ if (opts.removeScripts) {
75
+ sanitized = sanitized
76
+ .replace(/<script[^>]*>.*?<\/script>/gi, '')
77
+ .replace(/javascript:/gi, '')
78
+ .replace(/vbscript:/gi, '')
79
+ .replace(/data:/gi, '');
80
+ }
81
+
82
+ // Remove event handlers
83
+ if (opts.removeEvents) {
84
+ sanitized = sanitized.replace(/on\w+\s*=/gi, '');
85
+ }
86
+
87
+ return sanitized;
88
+ }
89
+
90
+ /**
91
+ * Sanitizes email addresses
92
+ */
93
+ export function sanitizeEmail(email: string): string {
94
+ if (typeof email !== 'string') {
95
+ return '';
96
+ }
97
+
98
+ return email
99
+ .trim()
100
+ .toLowerCase()
101
+ .replace(/[^\w@.-]/g, ''); // Only allow word characters, @, ., and -
102
+ }
103
+
104
+ /**
105
+ * Sanitizes phone numbers
106
+ */
107
+ export function sanitizePhoneNumber(phone: string): string {
108
+ if (typeof phone !== 'string') {
109
+ return '';
110
+ }
111
+
112
+ return phone.replace(/[^\d+\-\s()]/g, '').trim();
113
+ }
114
+
115
+ /**
116
+ * Sanitizes URLs
117
+ */
118
+ export function sanitizeUrl(url: string): string {
119
+ if (typeof url !== 'string') {
120
+ return '';
121
+ }
122
+
123
+ const sanitized = url.trim();
124
+
125
+ // Only allow http(s) and ftp protocols
126
+ if (!/^https?:\/\/|^ftp:\/\//i.test(sanitized)) {
127
+ return '';
128
+ }
129
+
130
+ // Remove javascript: and other dangerous protocols
131
+ if (/javascript:|data:|vbscript:/i.test(sanitized)) {
132
+ return '';
133
+ }
134
+
135
+ return sanitized;
136
+ }
137
+
138
+ /**
139
+ * Sanitizes file names
140
+ */
141
+ export function sanitizeFileName(fileName: string): string {
142
+ if (typeof fileName !== 'string') {
143
+ return '';
144
+ }
145
+
146
+ return fileName
147
+ .trim()
148
+ .replace(/[<>:"/\\|?*]/g, '') // Remove invalid file name characters
149
+ .replace(/\.\./g, '') // Remove directory traversal attempts
150
+ .substring(0, 255); // Limit length
151
+ }
152
+
153
+ /**
154
+ * Sanitizes SQL input to prevent injection
155
+ */
156
+ export function sanitizeSqlInput(input: string): string {
157
+ if (typeof input !== 'string') {
158
+ return '';
159
+ }
160
+
161
+ return input
162
+ .replace(/['";\\]/g, '') // Remove SQL special characters
163
+ .replace(/--.*$/gm, '') // Remove SQL comments
164
+ .replace(/\/\*.*?\*\//g, '') // Remove SQL block comments
165
+ .trim();
166
+ }
167
+
168
+ /**
169
+ * Validates and sanitizes form data using Zod schemas
170
+ */
171
+ export function sanitizeFormData<T>(
172
+ data: unknown,
173
+ schema: z.ZodSchema<T>,
174
+ sanitizationRules?: Record<string, SanitizationOptions>
175
+ ): { success: boolean; data?: T; error?: string } {
176
+ try {
177
+ // First, sanitize string fields if rules are provided
178
+ if (sanitizationRules && typeof data === 'object' && data !== null) {
179
+ const sanitizedData = { ...data } as Record<string, unknown>;
180
+
181
+ Object.entries(sanitizationRules).forEach(([field, options]) => {
182
+ if (typeof sanitizedData[field] === 'string') {
183
+ sanitizedData[field] = sanitizeUserInput(sanitizedData[field] as string, options);
184
+ }
185
+ });
186
+
187
+ data = sanitizedData;
188
+ }
189
+
190
+ // Then validate with Zod schema
191
+ const result = schema.parse(data);
192
+ return { success: true, data: result };
193
+ } catch (error) {
194
+ if (error instanceof z.ZodError) {
195
+ return {
196
+ success: false,
197
+ error: error.errors.map(e => e.message).join(', ')
198
+ };
199
+ }
200
+ return {
201
+ success: false,
202
+ error: 'Validation failed'
203
+ };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Content Security Policy (CSP) utilities
209
+ */
210
+ export const CSP_DIRECTIVES = {
211
+ default: "default-src 'self'",
212
+ script: "script-src 'self' 'unsafe-inline'",
213
+ style: "style-src 'self' 'unsafe-inline'",
214
+ img: "img-src 'self' data: https:",
215
+ font: "font-src 'self'",
216
+ connect: "connect-src 'self'",
217
+ frame: "frame-src 'none'"
218
+ };
219
+
220
+ export function generateCSPHeader(customDirectives?: Partial<typeof CSP_DIRECTIVES>): string {
221
+ const directives = { ...CSP_DIRECTIVES, ...customDirectives };
222
+ return Object.values(directives).join('; ');
223
+ }
224
+
225
+ /**
226
+ * Rate limiting utilities
227
+ */
228
+ export class RateLimiter {
229
+ private attempts: Map<string, { count: number; resetTime: number }> = new Map();
230
+
231
+ constructor(
232
+ private maxAttempts: number = 5,
233
+ private windowMs: number = 15 * 60 * 1000 // 15 minutes
234
+ ) {}
235
+
236
+ isAllowed(identifier: string): boolean {
237
+ const now = Date.now();
238
+ const record = this.attempts.get(identifier);
239
+
240
+ if (!record || now > record.resetTime) {
241
+ this.attempts.set(identifier, { count: 1, resetTime: now + this.windowMs });
242
+ return true;
243
+ }
244
+
245
+ if (record.count >= this.maxAttempts) {
246
+ return false;
247
+ }
248
+
249
+ record.count++;
250
+ return true;
251
+ }
252
+
253
+ getRemainingAttempts(identifier: string): number {
254
+ const record = this.attempts.get(identifier);
255
+ if (!record || Date.now() > record.resetTime) {
256
+ return this.maxAttempts;
257
+ }
258
+ return Math.max(0, this.maxAttempts - record.count);
259
+ }
260
+
261
+ reset(identifier: string): void {
262
+ this.attempts.delete(identifier);
263
+ }
264
+ }