@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Role hierarchy configuration type
3
+ */
4
+ export type RoleHierarchyConfig = Record<string, string[]>
5
+
6
+ /**
7
+ * RoleHierarchy - Topological role resolution for Symfony-like permission system
8
+ *
9
+ * Roles form a Directed Acyclic Graph (DAG) where higher roles inherit
10
+ * permissions from lower roles. This class resolves the complete set of
11
+ * roles that a user effectively has based on their assigned roles.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const hierarchy = new RoleHierarchy({
16
+ * ROLE_ADMIN: ['ROLE_USER'], // Admin inherits from User
17
+ * ROLE_SUPER_ADMIN: ['ROLE_ADMIN'], // Super admin inherits from Admin
18
+ * ROLE_MANAGER: ['ROLE_USER'], // Manager also inherits from User
19
+ * })
20
+ *
21
+ * hierarchy.getReachableRoles('ROLE_ADMIN')
22
+ * // Returns: ['ROLE_ADMIN', 'ROLE_USER']
23
+ * ```
24
+ */
25
+ export class RoleHierarchy {
26
+ readonly map: RoleHierarchyConfig
27
+
28
+ constructor(hierarchy: RoleHierarchyConfig = {}) {
29
+ this.map = hierarchy
30
+ }
31
+
32
+ /**
33
+ * Resolve all roles reachable from a given role (topological traversal)
34
+ *
35
+ * Performs BFS traversal of the role graph to find all inherited roles.
36
+ * Handles cycles gracefully by tracking visited nodes.
37
+ */
38
+ getReachableRoles(role: string): string[] {
39
+ const visited = new Set<string>()
40
+ const queue: string[] = [role]
41
+
42
+ while (queue.length > 0) {
43
+ const current = queue.shift()!
44
+ if (visited.has(current)) continue
45
+ visited.add(current)
46
+
47
+ const parents = this.map[current] || []
48
+ queue.push(...parents)
49
+ }
50
+
51
+ return [...visited]
52
+ }
53
+
54
+ /**
55
+ * Check if user with given roles has the required role
56
+ *
57
+ * A user has a role if:
58
+ * 1. They are directly assigned that role, OR
59
+ * 2. They have a role that inherits from the required role
60
+ */
61
+ isGrantedRole(userRoles: string | string[], requiredRole: string): boolean {
62
+ const roles = Array.isArray(userRoles) ? userRoles : [userRoles]
63
+
64
+ for (const role of roles) {
65
+ const reachable = this.getReachableRoles(role)
66
+ if (reachable.includes(requiredRole)) return true
67
+ }
68
+
69
+ return false
70
+ }
71
+
72
+ /**
73
+ * Get all roles that can reach a given role (reverse lookup)
74
+ *
75
+ * Useful for finding which roles would grant a specific permission.
76
+ */
77
+ getRolesGranting(targetRole: string): string[] {
78
+ const grantors: string[] = []
79
+
80
+ for (const role of Object.keys(this.map)) {
81
+ if (this.isGrantedRole([role], targetRole)) {
82
+ grantors.push(role)
83
+ }
84
+ }
85
+
86
+ // Also check the target role itself
87
+ if (!grantors.includes(targetRole)) {
88
+ grantors.push(targetRole)
89
+ }
90
+
91
+ return grantors
92
+ }
93
+
94
+ /**
95
+ * Validate the hierarchy for cycles (optional sanity check)
96
+ */
97
+ validate(): boolean {
98
+ const visiting = new Set<string>()
99
+ const visited = new Set<string>()
100
+
101
+ const hasCycle = (role: string): boolean => {
102
+ if (visited.has(role)) return false
103
+ if (visiting.has(role)) return true
104
+
105
+ visiting.add(role)
106
+ const parents = this.map[role] || []
107
+ for (const parent of parents) {
108
+ if (hasCycle(parent)) return true
109
+ }
110
+ visiting.delete(role)
111
+ visited.add(role)
112
+ return false
113
+ }
114
+
115
+ for (const role of Object.keys(this.map)) {
116
+ if (hasCycle(role)) return false
117
+ }
118
+
119
+ return true
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Create a RoleHierarchy instance from config
125
+ */
126
+ export function createRoleHierarchy(config: RoleHierarchyConfig = {}): RoleHierarchy {
127
+ return new RoleHierarchy(config)
128
+ }
@@ -0,0 +1,202 @@
1
+ import { RoleHierarchy, type RoleHierarchyConfig } from './RoleHierarchy'
2
+ import { PermissionMatcher } from '../../security/PermissionMatcher'
3
+ import { StaticRoleProvider } from '../../security/StaticRolesProvider'
4
+ import type { AuthUser } from './EntityAuthAdapter'
5
+
6
+ /**
7
+ * RolesProvider adapter interface (from security module)
8
+ */
9
+ export interface RoleProvider {
10
+ getPermissions(role: string): string[]
11
+ getRoles(): string[]
12
+ getHierarchy(): RoleHierarchyConfig
13
+ getAnonymousRole(): string
14
+ getRoleMeta(role: string): { label?: string; description?: string } | null
15
+ readonly canPersist: boolean
16
+ }
17
+
18
+ /**
19
+ * SecurityChecker constructor options
20
+ */
21
+ export interface SecurityCheckerOptions {
22
+ rolesProvider?: RoleProvider
23
+ roleHierarchy?: RoleHierarchy | RoleHierarchyConfig
24
+ rolePermissions?: Record<string, string[]>
25
+ getCurrentUser: () => AuthUser | null
26
+ }
27
+
28
+ /**
29
+ * SecurityChecker config for factory
30
+ */
31
+ export interface SecurityCheckerConfig {
32
+ rolesProvider?: RoleProvider
33
+ role_hierarchy?: RoleHierarchyConfig
34
+ role_permissions?: Record<string, string[]>
35
+ getCurrentUser: () => AuthUser | null
36
+ }
37
+
38
+ /**
39
+ * SecurityChecker - Symfony-inspired permission checking
40
+ *
41
+ * Provides the `isGranted(attribute, subject?)` contract for checking permissions.
42
+ * Supports both role checks (ROLE_*) and permission checks with wildcards.
43
+ *
44
+ * Permission format: namespace:target:action
45
+ * - entity:books:read - Entity CRUD
46
+ * - auth:impersonate - System feature
47
+ * - admin:config:edit - Admin feature
48
+ *
49
+ * Wildcard patterns (like signals):
50
+ * - `*` matches exactly one segment
51
+ * - `**` matches zero or more segments (greedy)
52
+ */
53
+ export class SecurityChecker {
54
+ protected _rolesProvider: RoleProvider
55
+ protected _legacyRoleHierarchy: RoleHierarchy | null
56
+ readonly getCurrentUser: () => AuthUser | null
57
+
58
+ constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
59
+ // Support both new rolesProvider and legacy rolePermissions
60
+ if (rolesProvider) {
61
+ this._rolesProvider = rolesProvider
62
+ this._legacyRoleHierarchy = null
63
+ } else {
64
+ // Legacy: create static provider from rolePermissions
65
+ const hierarchyConfig =
66
+ roleHierarchy instanceof RoleHierarchy ? {} : (roleHierarchy as RoleHierarchyConfig) || {}
67
+
68
+ this._rolesProvider = new StaticRoleProvider({
69
+ role_hierarchy: hierarchyConfig,
70
+ role_permissions: rolePermissions || {},
71
+ })
72
+
73
+ this._legacyRoleHierarchy =
74
+ roleHierarchy instanceof RoleHierarchy
75
+ ? roleHierarchy
76
+ : new RoleHierarchy(hierarchyConfig)
77
+ }
78
+
79
+ this.getCurrentUser = getCurrentUser
80
+ }
81
+
82
+ /**
83
+ * Get role hierarchy (dynamically resolved from rolesProvider)
84
+ */
85
+ get roleHierarchy(): RoleHierarchy {
86
+ if (this._legacyRoleHierarchy) {
87
+ return this._legacyRoleHierarchy
88
+ }
89
+ return new RoleHierarchy(this._rolesProvider.getHierarchy())
90
+ }
91
+
92
+ /**
93
+ * Get roles provider adapter
94
+ */
95
+ get rolesProvider(): RoleProvider {
96
+ return this._rolesProvider
97
+ }
98
+
99
+ /**
100
+ * Get role permissions (for backward compatibility / debug panel)
101
+ */
102
+ get rolePermissions(): Record<string, string[]> {
103
+ const roles = this._rolesProvider.getRoles()
104
+ const perms: Record<string, string[]> = {}
105
+ for (const role of roles) {
106
+ perms[role] = this._rolesProvider.getPermissions(role)
107
+ }
108
+ return perms
109
+ }
110
+
111
+ /**
112
+ * Check if current user is granted an attribute (role or permission)
113
+ *
114
+ * This is the main contract method, similar to Symfony's isGranted().
115
+ */
116
+ isGranted(attribute: string, _subject: unknown = null): boolean {
117
+ const user = this.getCurrentUser()
118
+ if (!user) return false
119
+
120
+ // 1. Check if it's a role (ROLE_*)
121
+ if (attribute.startsWith('ROLE_')) {
122
+ return this.roleHierarchy.isGrantedRole(user.roles || [user.role || ''], attribute)
123
+ }
124
+
125
+ // 2. Check if it's a permission (with wildcard support)
126
+ const userPerms = this.getUserPermissions(user)
127
+ return PermissionMatcher.any(userPerms, attribute)
128
+ }
129
+
130
+ /**
131
+ * Get all permissions for a user (resolved from roles + user overrides)
132
+ */
133
+ getUserPermissions(user: AuthUser): string[] {
134
+ const roles = user.roles || [user.role || '']
135
+ const perms = new Set<string>()
136
+
137
+ for (const role of roles) {
138
+ if (!role) continue
139
+ const reachable = this.roleHierarchy.getReachableRoles(role)
140
+ for (const r of reachable) {
141
+ const rolePerms = this._rolesProvider.getPermissions(r)
142
+ rolePerms.forEach((p) => perms.add(p))
143
+ }
144
+ }
145
+
146
+ // User-specific permission overrides
147
+ if (user.permissions && Array.isArray(user.permissions)) {
148
+ user.permissions.forEach((p) => perms.add(p))
149
+ }
150
+
151
+ return [...perms]
152
+ }
153
+
154
+ /**
155
+ * Check if user can assign a role to another user
156
+ */
157
+ canAssignRole(targetRole: string): boolean {
158
+ return this.isGranted('security:roles:assign') && this.isGranted(targetRole)
159
+ }
160
+
161
+ /**
162
+ * Get all roles that current user can assign
163
+ */
164
+ getAssignableRoles(): string[] {
165
+ if (!this.isGranted('security:roles:assign')) return []
166
+
167
+ const user = this.getCurrentUser()
168
+ if (!user) return []
169
+
170
+ const userRoles = user.roles || [user.role || '']
171
+ const assignable = new Set<string>()
172
+
173
+ for (const role of userRoles) {
174
+ if (!role) continue
175
+ const reachable = this.roleHierarchy.getReachableRoles(role)
176
+ reachable.forEach((r) => assignable.add(r))
177
+ }
178
+
179
+ return [...assignable]
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Create a SecurityChecker instance from config
185
+ */
186
+ export function createSecurityChecker(config: SecurityCheckerConfig): SecurityChecker {
187
+ if (config.rolesProvider) {
188
+ return new SecurityChecker({
189
+ rolesProvider: config.rolesProvider,
190
+ getCurrentUser: config.getCurrentUser,
191
+ })
192
+ }
193
+
194
+ // Legacy config: auto-create static provider
195
+ return new SecurityChecker({
196
+ rolesProvider: new StaticRoleProvider({
197
+ role_hierarchy: config.role_hierarchy || {},
198
+ role_permissions: config.role_permissions || {},
199
+ }),
200
+ getCurrentUser: config.getCurrentUser,
201
+ })
202
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Auth Factory/Resolver Pattern
3
+ *
4
+ * Enables declarative auth adapter configuration with auto-resolution.
5
+ * Follows the same pattern as storage/factory.js for consistency.
6
+ */
7
+
8
+ import { EntityAuthAdapter, type EntityAuthAdapterOptions, type AuthUser } from './EntityAuthAdapter'
9
+ import { PermissiveAuthAdapter } from './PermissiveAdapter'
10
+ import type { CompositeAuthAdapter, CompositeAuthConfig } from './CompositeAuthAdapter'
11
+
12
+ /**
13
+ * Auth adapter class type
14
+ */
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ type AuthAdapterClass = new (options?: any) => EntityAuthAdapter
17
+
18
+ /**
19
+ * Built-in auth adapter types
20
+ */
21
+ export const authTypes: Record<string, AuthAdapterClass> = {
22
+ permissive: PermissiveAuthAdapter,
23
+ }
24
+
25
+ /**
26
+ * Parsed auth pattern
27
+ */
28
+ export interface ParsedAuthPattern {
29
+ type: string
30
+ scope?: string
31
+ [key: string]: unknown
32
+ }
33
+
34
+ /**
35
+ * Auth config object
36
+ */
37
+ export interface AuthConfig {
38
+ type?: string
39
+ [key: string]: unknown
40
+ }
41
+
42
+ /**
43
+ * Auth factory context
44
+ */
45
+ export interface AuthFactoryContext {
46
+ authTypes?: Record<string, AuthAdapterClass>
47
+ authResolver?: AuthResolver
48
+ CompositeAuthAdapter?: new (config: CompositeAuthConfig, context: AuthFactoryContext) => CompositeAuthAdapter
49
+ }
50
+
51
+ /**
52
+ * Auth resolver function type
53
+ */
54
+ export type AuthResolver = (config: AuthConfig, context: AuthFactoryContext) => EntityAuthAdapter
55
+
56
+ /**
57
+ * Parse auth pattern 'type' or 'type:scope'
58
+ */
59
+ export function parseAuthPattern(pattern: string): ParsedAuthPattern | null {
60
+ if (typeof pattern !== 'string') return null
61
+
62
+ const match = pattern.match(/^(\w+)(?::(.+))?$/)
63
+ if (match && match[1]) {
64
+ const type = match[1]
65
+ const scope = match[2]
66
+ return scope ? { type, scope } : { type }
67
+ }
68
+ return null
69
+ }
70
+
71
+ /**
72
+ * Default auth resolver - creates adapter instance from config
73
+ */
74
+ export function defaultAuthResolver(config: AuthConfig, context: AuthFactoryContext = {}): EntityAuthAdapter {
75
+ const { type, ...rest } = config
76
+
77
+ if (!type) {
78
+ throw new Error('[authFactory] Config object requires "type" property')
79
+ }
80
+
81
+ // Merge built-in types with custom types from context
82
+ const allTypes = { ...authTypes, ...context.authTypes }
83
+ const AdapterClass = allTypes[type]
84
+
85
+ if (!AdapterClass) {
86
+ throw new Error(
87
+ `[authFactory] Unknown auth type: "${type}". ` +
88
+ `Available: ${Object.keys(allTypes).join(', ')}`
89
+ )
90
+ }
91
+
92
+ return new AdapterClass(rest)
93
+ }
94
+
95
+ /**
96
+ * Check if config is a composite auth config
97
+ */
98
+ function isCompositeConfig(config: unknown): config is CompositeAuthConfig {
99
+ return (
100
+ config !== null &&
101
+ typeof config === 'object' &&
102
+ 'default' in config &&
103
+ !('type' in config)
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Auth factory input types
109
+ */
110
+ export type AuthFactoryInput =
111
+ | EntityAuthAdapter
112
+ | ((options?: EntityAuthAdapterOptions) => AuthUser | null)
113
+ | string
114
+ | CompositeAuthConfig
115
+ | AuthConfig
116
+ | null
117
+ | undefined
118
+
119
+ /**
120
+ * Auth factory - normalizes input and resolves to adapter
121
+ *
122
+ * Handles:
123
+ * - AuthAdapter instance → return directly (backward compatible)
124
+ * - Function → wrap as EntityAuthAdapter with getCurrentUser callback
125
+ * - String pattern 'type' → parse and resolve
126
+ * - Config object with 'type' → resolve via registry
127
+ * - Config object with 'default' → create CompositeAuthAdapter
128
+ */
129
+ export function authFactory(
130
+ config: AuthFactoryInput,
131
+ context: AuthFactoryContext = {}
132
+ ): EntityAuthAdapter {
133
+ const { authResolver = defaultAuthResolver } = context
134
+
135
+ // Null/undefined → permissive (safe default)
136
+ if (config == null) {
137
+ return new PermissiveAuthAdapter()
138
+ }
139
+
140
+ // Function → wrap in EntityAuthAdapter with getCurrentUser callback
141
+ if (typeof config === 'function') {
142
+ return new EntityAuthAdapter({ getCurrentUser: config as () => AuthUser | null })
143
+ }
144
+
145
+ // Already an EntityAuthAdapter instance → return directly (backward compatible)
146
+ if (config instanceof EntityAuthAdapter) {
147
+ return config
148
+ }
149
+
150
+ // Duck-typed adapter (has canPerform method)
151
+ if (
152
+ typeof config === 'object' &&
153
+ 'canPerform' in config &&
154
+ typeof config.canPerform === 'function'
155
+ ) {
156
+ return config as unknown as EntityAuthAdapter
157
+ }
158
+
159
+ // String pattern → parse and resolve
160
+ if (typeof config === 'string') {
161
+ const parsed = parseAuthPattern(config)
162
+ if (!parsed) {
163
+ throw new Error(`[authFactory] Invalid auth pattern: "${config}"`)
164
+ }
165
+ return authResolver(parsed, context)
166
+ }
167
+
168
+ // Config object
169
+ if (typeof config === 'object') {
170
+ // Composite config: { default: ..., mapping: { ... } }
171
+ if (isCompositeConfig(config)) {
172
+ const CompositeClass = context.authTypes?.composite || context.CompositeAuthAdapter
173
+ if (!CompositeClass) {
174
+ throw new Error(
175
+ '[authFactory] Composite config requires CompositeAuthAdapter. ' +
176
+ 'Pass it via context.CompositeAuthAdapter or register as context.authTypes.composite'
177
+ )
178
+ }
179
+ return new CompositeClass(config, context)
180
+ }
181
+
182
+ // Simple config with type: { type: 'jwt', ... }
183
+ if ('type' in config && config.type) {
184
+ return authResolver(config as AuthConfig, context)
185
+ }
186
+
187
+ throw new Error('[authFactory] Config object requires either "type" or "default" property')
188
+ }
189
+
190
+ throw new Error(
191
+ `[authFactory] Invalid auth config: ${typeof config}. ` +
192
+ 'Expected AuthAdapter instance, string, or config object.'
193
+ )
194
+ }
195
+
196
+ /**
197
+ * Create a custom auth factory with bound context
198
+ */
199
+ export function createAuthFactory(
200
+ context: AuthFactoryContext
201
+ ): (config: AuthFactoryInput) => EntityAuthAdapter {
202
+ return (config) => authFactory(config, context)
203
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Auth Module
3
+ *
4
+ * AuthAdapter interface and implementations for scope/silo permission checks.
5
+ * Includes Symfony-inspired role hierarchy and security checker.
6
+ *
7
+ * Multi-source auth:
8
+ * - CompositeAuthAdapter routes to different adapters based on entity patterns
9
+ * - authFactory normalizes config (instance, string, or object)
10
+ */
11
+
12
+ // Interface
13
+ export { EntityAuthAdapter, AuthActions } from './EntityAuthAdapter'
14
+ export type { AuthUser, EntityAuthAdapterOptions, AuthAction } from './EntityAuthAdapter'
15
+
16
+ // Role Hierarchy (topological resolution)
17
+ export { RoleHierarchy, createRoleHierarchy } from './RoleHierarchy'
18
+ export type { RoleHierarchyConfig } from './RoleHierarchy'
19
+
20
+ // Security Checker (isGranted contract)
21
+ export { SecurityChecker, createSecurityChecker } from './SecurityChecker'
22
+ export type {
23
+ RoleProvider,
24
+ SecurityCheckerOptions,
25
+ SecurityCheckerConfig,
26
+ } from './SecurityChecker'
27
+
28
+ // Implementations
29
+ export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter'
30
+ export { CompositeAuthAdapter, createCompositeAuthAdapter } from './CompositeAuthAdapter'
31
+ export type { CompositeAuthConfig } from './CompositeAuthAdapter'
32
+
33
+ // Factory/Resolver pattern (like storage/factory.ts)
34
+ export {
35
+ authFactory,
36
+ createAuthFactory,
37
+ authTypes,
38
+ parseAuthPattern,
39
+ defaultAuthResolver,
40
+ } from './factory'
41
+ export type {
42
+ ParsedAuthPattern,
43
+ AuthConfig,
44
+ AuthFactoryContext,
45
+ AuthResolver,
46
+ AuthFactoryInput,
47
+ } from './factory'