adminizer 5.0.0-build.9 → 5.1.0-build.24

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 (232) hide show
  1. package/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
  2. package/assets/add-31tS-g4V.js +1 -0
  3. package/assets/add-group-Cf3zT7WE.js +1 -0
  4. package/assets/add-user-C62wOna-.js +1 -0
  5. package/assets/ai-assistant/agent.es.js +28464 -0
  6. package/assets/app-DPmI-MvN.css +1 -0
  7. package/assets/app-vdHbjOQX.js +138 -0
  8. package/assets/arc-DP4o1zz3.js +1 -0
  9. package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
  10. package/assets/asmcrypto-DZeQH0LC.js +1 -0
  11. package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
  12. package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
  13. package/assets/{catalog-dk6a0I1u.js → catalog-C-y5rMaG.js} +3 -3
  14. package/assets/channel-CkJRUsq_.js +1 -0
  15. package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
  16. package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
  17. package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
  18. package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
  19. package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
  20. package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
  21. package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
  22. package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
  23. package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
  24. package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
  25. package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
  26. package/assets/controls/ckeditor.es.js +9382 -9320
  27. package/assets/controls/handsontable.es.js +11971 -11938
  28. package/assets/controls/jsoneditor.es.js +10442 -10233
  29. package/assets/controls/toast-ui.es.js +1308 -1279
  30. package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
  31. package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
  32. package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
  33. package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  34. package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
  35. package/assets/dashboard-X8k4X6Sw.js +3 -0
  36. package/assets/debounce-B3mXI_sx.js +1 -0
  37. package/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
  39. package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
  40. package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
  41. package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
  42. package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
  43. package/assets/docs-BiZOD7PP.js +1 -0
  44. package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
  45. package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
  46. package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
  47. package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
  48. package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
  49. package/assets/history-CVBJUNSQ.js +1 -0
  50. package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
  51. package/assets/init-Gi6I4Gst.js +1 -0
  52. package/assets/init-user-xSvnFTF0.js +1 -0
  53. package/assets/isObject-DL-BuaBl.js +1 -0
  54. package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
  55. package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
  56. package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
  57. package/assets/katex-HP8lGamR.js +257 -0
  58. package/assets/layout-CN3kt_jt.js +1 -0
  59. package/assets/linear-nvkaQ6BA.js +1 -0
  60. package/assets/{list-CcMtS4hD.js → list-CrIU2eZz.js} +2 -2
  61. package/assets/login-CX9xVjCn.js +1 -0
  62. package/assets/manifest.json +834 -58
  63. package/assets/media-manager-BfYftLIH.js +6 -0
  64. package/assets/mermaid.core-C0V1NaVJ.js +310 -0
  65. package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
  66. package/assets/module-DPEvnad3.js +1 -0
  67. package/assets/notification-Dqpt5AIZ.js +20 -0
  68. package/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
  70. package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
  71. package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
  72. package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
  73. package/assets/register-CF4KQ97e.js +1 -0
  74. package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
  75. package/assets/ru-CFJ4fysR.js +1 -0
  76. package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
  77. package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
  78. package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
  79. package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
  80. package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
  81. package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
  82. package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
  83. package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
  84. package/assets/toNumber-DZb-r9sy.js +1 -0
  85. package/assets/use-appearance-DYTIKpT2.js +1 -0
  86. package/assets/user-filters-list-fR0YNJ3l.js +1 -0
  87. package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
  88. package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
  89. package/assets/welcome-v11MTsRK.js +1 -0
  90. package/assets/with-app-layout-CH6RIib1.js +31 -0
  91. package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
  92. package/controllers/access-rights/permissionOptions.d.ts +2 -0
  93. package/controllers/access-rights/permissionOptions.js +23 -0
  94. package/controllers/add.js +2 -2
  95. package/controllers/addGroup.js +15 -6
  96. package/controllers/ai/AiAgentController.d.ts +66 -0
  97. package/controllers/ai/AiAgentController.js +435 -0
  98. package/controllers/ai/AiAssistantController.d.ts +0 -4
  99. package/controllers/ai/AiAssistantController.js +4 -78
  100. package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
  101. package/controllers/docs/index.d.ts +22 -0
  102. package/controllers/docs/index.js +126 -0
  103. package/controllers/edit.js +2 -2
  104. package/controllers/editGroup.js +15 -6
  105. package/controllers/filter-fields/savedFilters.js +1 -1
  106. package/controllers/history-actions/HistoryController.js +7 -7
  107. package/controllers/inlineUpdate.js +10 -3
  108. package/controllers/list.js +1 -1
  109. package/controllers/login.js +1 -1
  110. package/controllers/notifications/NotificationController.js +14 -8
  111. package/controllers/view.js +3 -3
  112. package/controllers/widgets/filterInfo.js +1 -1
  113. package/helpers/accessRightsHelper.d.ts +30 -3
  114. package/helpers/accessRightsHelper.js +140 -12
  115. package/helpers/assetVersionHelper.d.ts +14 -0
  116. package/helpers/assetVersionHelper.js +42 -0
  117. package/helpers/configHelper.js +19 -18
  118. package/helpers/controllerHelper.d.ts +1 -0
  119. package/helpers/controllerHelper.js +25 -15
  120. package/helpers/fieldsHelper.d.ts +2 -1
  121. package/helpers/fieldsHelper.js +11 -5
  122. package/helpers/inertiaActionsHelper.d.ts +1 -1
  123. package/helpers/inertiaActionsHelper.js +4 -5
  124. package/helpers/inertiaAddHelper.d.ts +1 -1
  125. package/helpers/inertiaAddHelper.js +7 -6
  126. package/helpers/inertiaGroupHelper.js +10 -2
  127. package/helpers/inertiaListHelper.d.ts +1 -1
  128. package/helpers/inertiaListHelper.js +7 -8
  129. package/helpers/inertiaMenuHelper.d.ts +7 -2
  130. package/helpers/inertiaMenuHelper.js +12 -27
  131. package/helpers/menuHelper.d.ts +11 -1
  132. package/helpers/menuHelper.js +12 -0
  133. package/helpers/modelResourceHelper.d.ts +9 -0
  134. package/helpers/modelResourceHelper.js +43 -0
  135. package/helpers/navigationAccessHelper.d.ts +13 -0
  136. package/helpers/navigationAccessHelper.js +45 -0
  137. package/index.d.ts +11 -0
  138. package/index.js +11 -0
  139. package/interfaces/adminpanelConfig.d.ts +160 -2
  140. package/interfaces/queryCriteria.d.ts +13 -0
  141. package/interfaces/types.d.ts +169 -0
  142. package/lib/Adminizer.d.ts +8 -0
  143. package/lib/Adminizer.js +29 -2
  144. package/lib/DataAccessor.d.ts +100 -3
  145. package/lib/DataAccessor.js +454 -245
  146. package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
  147. package/lib/access-graph/AccessGraphResolver.js +605 -0
  148. package/lib/access-graph/RecordAccessCache.d.ts +23 -0
  149. package/lib/access-graph/RecordAccessCache.js +30 -0
  150. package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
  151. package/lib/access-graph/RecordAccessResolver.js +295 -0
  152. package/lib/access-graph/shared.d.ts +60 -0
  153. package/lib/access-graph/shared.js +121 -0
  154. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  155. package/lib/admin-links/AdminLinkHandler.js +183 -0
  156. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  157. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  158. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  159. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  160. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  161. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  162. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
  163. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
  164. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  165. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  166. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  167. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
  168. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  169. package/lib/ai-assistant/builtinAgentSkills.js +259 -0
  170. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  171. package/lib/ai-assistant/jsonSafe.js +81 -0
  172. package/lib/app-manager/AdminizerApp.d.ts +31 -2
  173. package/lib/app-manager/AppManager.js +43 -42
  174. package/lib/app-manager/ConfigLayerHandler.js +4 -0
  175. package/lib/docs/AbstractDocumentation.d.ts +100 -0
  176. package/lib/docs/AbstractDocumentation.js +108 -0
  177. package/lib/docs/DocumentationHandler.d.ts +100 -0
  178. package/lib/docs/DocumentationHandler.js +198 -0
  179. package/lib/docs/FileDocumentation.d.ts +47 -0
  180. package/lib/docs/FileDocumentation.js +189 -0
  181. package/lib/docs/docsAgentSkills.d.ts +10 -0
  182. package/lib/docs/docsAgentSkills.js +93 -0
  183. package/lib/filters/FilterService.js +4 -3
  184. package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
  185. package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
  186. package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
  187. package/lib/model/AbstractModel.d.ts +48 -0
  188. package/lib/model/AbstractModel.js +14 -4
  189. package/lib/model/ModelHandler.d.ts +50 -2
  190. package/lib/model/ModelHandler.js +117 -8
  191. package/lib/model/adapter/sequelize.d.ts +40 -1
  192. package/lib/model/adapter/sequelize.js +243 -77
  193. package/lib/model/adapter/typeorm.d.ts +16 -0
  194. package/lib/model/adapter/typeorm.js +136 -9
  195. package/lib/notifications/GeneralNotificationService.js +6 -5
  196. package/lib/notifications/SystemNotificationService.js +1 -1
  197. package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
  198. package/lib/ui-i18n/uiTranslationKeys.js +42 -1
  199. package/lib/widgets/widgetHandler.js +10 -6
  200. package/models/Group.d.ts +2 -1
  201. package/package.json +35 -23
  202. package/policies/authPolicies.js +6 -6
  203. package/policies/permissionResolvers.d.ts +1 -0
  204. package/policies/permissionResolvers.js +3 -0
  205. package/system/Router.js +21 -3
  206. package/system/bindAccessRights.js +13 -3
  207. package/system/bindDocs.d.ts +13 -0
  208. package/system/bindDocs.js +77 -0
  209. package/system/bindInertia.js +30 -7
  210. package/system/buildInternalModelAccess.d.ts +8 -0
  211. package/system/buildInternalModelAccess.js +36 -4
  212. package/system/defaults.js +7 -1
  213. package/system/validateSystemModels.js +29 -9
  214. package/assets/add-CBlLpLr0.js +0 -1
  215. package/assets/add-group-H0IKIsNm.js +0 -1
  216. package/assets/add-user-CoTLmxtF.js +0 -1
  217. package/assets/app-DNXiApCk.css +0 -1
  218. package/assets/app-layout-xxJpLAvb.js +0 -1
  219. package/assets/app.js +0 -138
  220. package/assets/dashboard-DVJS5EPP.js +0 -3
  221. package/assets/debounce-BMmXVQ06.js +0 -1
  222. package/assets/history-bWRuzbye.js +0 -1
  223. package/assets/init-user-8aUohVDq.js +0 -1
  224. package/assets/login-CO4laf6m.js +0 -1
  225. package/assets/media-manager-BDbpSSjH.js +0 -6
  226. package/assets/module-CqO_BKsH.js +0 -1
  227. package/assets/notification-B5l2wwmi.js +0 -20
  228. package/assets/register-DBYIMXGQ.js +0 -1
  229. package/assets/ru-rYKdQbWF.js +0 -1
  230. package/assets/use-appearance-_EbbjHTh.js +0 -1
  231. package/assets/user-filters-list-C7kMKN5f.js +0 -1
  232. package/assets/welcome-6z-7sXnQ.js +0 -1
@@ -0,0 +1,126 @@
1
+ import { resolveModelResource } from '../../helpers/modelResourceHelper.js';
2
+ import { getUiTranslations } from '../../lib/ui-i18n/getUiTranslations.js';
3
+ import { DOCS_UI_TRANSLATION_KEYS } from '../../lib/ui-i18n/uiTranslationKeys.js';
4
+ import { DataAccessor } from '../../lib/DataAccessor.js';
5
+ /**
6
+ * Thin JSON proxy of the documentation subsystem: policies have checked the
7
+ * auth and the base token, `DocumentationHandler` applies per-document rights,
8
+ * the implementation does the actual work. No logic lives here.
9
+ */
10
+ function requestedLocale(req) {
11
+ const locale = typeof req.query.locale === 'string' ? req.query.locale.trim() : '';
12
+ return locale || req.adminizer.documentationHandler.localeFor(req.user);
13
+ }
14
+ function describeMeta(meta) {
15
+ return {
16
+ id: meta.id,
17
+ title: meta.title,
18
+ section: meta.section,
19
+ keywords: meta.keywords,
20
+ models: meta.models,
21
+ urls: meta.urls,
22
+ locales: meta.locales,
23
+ };
24
+ }
25
+ export async function docsList(req, res) {
26
+ const documents = await req.adminizer.documentationHandler.list(req.user, requestedLocale(req));
27
+ return res.json({ documents: documents.map(describeMeta) });
28
+ }
29
+ export async function docsKeywords(req, res) {
30
+ const keywords = await req.adminizer.documentationHandler.keywords(req.user, requestedLocale(req));
31
+ return res.json({ keywords: Object.fromEntries(keywords) });
32
+ }
33
+ export async function docsSearch(req, res) {
34
+ const keywords = typeof req.query.keywords === 'string'
35
+ ? req.query.keywords.split(',').map((keyword) => keyword.trim()).filter(Boolean)
36
+ : undefined;
37
+ const results = await req.adminizer.documentationHandler.search(req.user, {
38
+ query: typeof req.query.q === 'string' ? req.query.q : undefined,
39
+ keywords,
40
+ url: typeof req.query.url === 'string' ? req.query.url : undefined,
41
+ model: typeof req.query.model === 'string' ? req.query.model : undefined,
42
+ }, requestedLocale(req));
43
+ return res.json({
44
+ results: results.map((result) => ({ ...describeMeta(result.meta), snippets: result.snippets })),
45
+ });
46
+ }
47
+ export async function docsContext(req, res) {
48
+ const documents = await req.adminizer.documentationHandler.forContext(req.user, {
49
+ url: typeof req.query.url === 'string' ? req.query.url : undefined,
50
+ model: typeof req.query.model === 'string' ? req.query.model : undefined,
51
+ }, requestedLocale(req));
52
+ return res.json({ documents: documents.map(describeMeta) });
53
+ }
54
+ export async function docsDoc(req, res) {
55
+ const content = await req.adminizer.documentationHandler.get(req.user, String(req.params.id ?? ''), requestedLocale(req));
56
+ // A document the user may not read does not exist for them.
57
+ if (!content) {
58
+ return res.status(404).json({ error: req.i18n?.__('Not found') || 'Not found' });
59
+ }
60
+ return res.json({ meta: describeMeta(content.meta), markdown: content.markdown, locale: content.locale });
61
+ }
62
+ /**
63
+ * Resolves references written inside a document to document ids. How a
64
+ * reference is spelled is the implementation's business; the viewer only
65
+ * learns which of them point at a document this user may open — the rest stay
66
+ * unresolved and are rendered as plain text.
67
+ */
68
+ export async function docsResolve(req, res) {
69
+ const from = typeof req.query.from === 'string' ? req.query.from : '';
70
+ // `refs` is the comma-separated form the viewer uses; repeated `ref`
71
+ // parameters are accepted as well for hand-written calls.
72
+ const raw = req.query.ref;
73
+ const refs = [
74
+ ...(typeof req.query.refs === 'string' ? req.query.refs.split(',') : []),
75
+ ...(Array.isArray(raw) ? raw : [raw]),
76
+ ]
77
+ .filter((ref) => typeof ref === 'string' && ref.trim() !== '')
78
+ .map((ref) => ref.trim());
79
+ const resolved = {};
80
+ for (const ref of refs) {
81
+ const id = await req.adminizer.documentationHandler.resolveLink(req.user, from, ref);
82
+ if (id)
83
+ resolved[ref] = id;
84
+ }
85
+ return res.json({ resolved });
86
+ }
87
+ /**
88
+ * The viewer page. The document itself is fetched by the page through the API,
89
+ * so a deep link only has to name the document it opens with.
90
+ */
91
+ export async function docsPage(req) {
92
+ return req.Inertia.render({
93
+ component: 'docs',
94
+ props: {
95
+ title: req.i18n.__('Documentation'),
96
+ docId: typeof req.params.id === 'string' ? req.params.id : null,
97
+ i18nPage: getUiTranslations(req, DOCS_UI_TRANSLATION_KEYS),
98
+ },
99
+ });
100
+ }
101
+ /**
102
+ * Structure of a model resource as this user is allowed to see it — the same
103
+ * fields DataAccessor would hand to the list page, nothing more.
104
+ */
105
+ export async function docsSchema(req, res) {
106
+ const name = typeof req.query.model === 'string' ? req.query.model.trim() : '';
107
+ const resource = name ? resolveModelResource(req.adminizer, name) : undefined;
108
+ if (!resource) {
109
+ return res.status(404).json({ error: req.i18n.__('Model not found') });
110
+ }
111
+ if (!await req.adminizer.accessRightsHelper.hasPermission(`read-${resource.name}-model`, req.user)) {
112
+ return res.status(403).json({ error: req.i18n.__('Access denied') });
113
+ }
114
+ const fields = new DataAccessor(req.adminizer, req.user, resource, 'list').getFieldsConfig() ?? {};
115
+ return res.json({
116
+ model: resource.name,
117
+ title: req.i18n.__(resource.config?.title ?? resource.name),
118
+ url: resource.uri,
119
+ fields: Object.entries(fields).map(([key, field]) => ({
120
+ name: key,
121
+ title: field.config?.title ? req.i18n.__(field.config.title) : key,
122
+ type: String(field.config?.type ?? field.model?.type ?? 'string'),
123
+ required: Boolean(field.config?.required ?? field.model?.required),
124
+ })),
125
+ });
126
+ }
@@ -33,7 +33,7 @@ export default async function edit(req, res) {
33
33
  }
34
34
  let fields = dataAccessor.getFieldsConfig();
35
35
  // add deprecated 'records' to config
36
- fields = await FieldsHelper.loadAssociations(req, fields, "edit");
36
+ fields = await FieldsHelper.loadAssociations(req, fields, "edit", dataAccessor.recordAccessCache);
37
37
  // Save
38
38
  if (req.method.toUpperCase() === 'POST') {
39
39
  const identifierField = modelResource.config.identifierField || req.adminizer.config.identifierField;
@@ -134,7 +134,7 @@ export default async function edit(req, res) {
134
134
  });
135
135
  }
136
136
  }
137
- const props = inertiaAddHelper(req, modelResource, fields, record);
137
+ const props = await inertiaAddHelper(req, modelResource, fields, record);
138
138
  if (req.query?.without_layout) {
139
139
  return res.json({
140
140
  props: props
@@ -44,13 +44,22 @@ export default async function editGroup(req, res) {
44
44
  }
45
45
  }
46
46
  }
47
- if (key.startsWith("token-checkbox-") && req.body[key] === true) {
48
- for (let token of allTokens) {
49
- if (token.id == key.slice(15)) {
50
- tokensOfThisGroup.push(token.id);
51
- }
52
- }
47
+ }
48
+ for (const token of allTokens) {
49
+ if (req.body[`token-checkbox-${token.id}`] !== true)
50
+ continue;
51
+ if (!token.getOptions) {
52
+ tokensOfThisGroup.push(token.id);
53
+ continue;
53
54
  }
55
+ const submittedRights = req.body[`token-record-rights-${token.id}`];
56
+ const rights = Array.isArray(submittedRights)
57
+ ? Array.from(new Set(submittedRights.map(String)))
58
+ : [];
59
+ tokensOfThisGroup.push({
60
+ tokenId: token.id,
61
+ rights,
62
+ });
54
63
  }
55
64
  let updatedGroup;
56
65
  try {
@@ -125,7 +125,7 @@ export async function saveFilter(req, res) {
125
125
  }
126
126
  // Determine visibility and groupIds
127
127
  const isAdmin = req.user?.isAdministrator === true;
128
- const canManageGroupVisibility = isAdmin || req.adminizer.accessRightsHelper.hasPermission(GROUP_FILTER_VISIBILITY_TOKEN, req.user);
128
+ const canManageGroupVisibility = isAdmin || await req.adminizer.accessRightsHelper.hasPermission(GROUP_FILTER_VISIBILITY_TOKEN, req.user);
129
129
  let effectiveVisibility = 'private';
130
130
  let effectiveGroupIds;
131
131
  if (visibility === 'public' && isAdmin) {
@@ -3,13 +3,13 @@ import { getUiTranslations } from "../../lib/ui-i18n/getUiTranslations.js";
3
3
  import { HISTORY_UI_TRANSLATION_KEYS } from "../../lib/ui-i18n/uiTranslationKeys.js";
4
4
  export class HistoryController {
5
5
  static async index(req, res) {
6
- if (!HistoryController.checkHistoryPermission(req, res))
6
+ if (!await HistoryController.checkHistoryPermission(req, res))
7
7
  return;
8
8
  const adapter = HistoryController.getAdapter(req);
9
9
  if (req.method.toUpperCase() === 'GET') {
10
- const rawModels = adapter.getModels(req.user);
10
+ const rawModels = await adapter.getModels(req.user);
11
11
  let users = [];
12
- const accessToUsersHistory = req.adminizer.accessRightsHelper.enoughPermissions([
12
+ const accessToUsersHistory = await req.adminizer.accessRightsHelper.enoughPermissions([
13
13
  `users-history-${adapter.id}`
14
14
  ], req.user);
15
15
  if (accessToUsersHistory)
@@ -59,7 +59,7 @@ export class HistoryController {
59
59
  return res.status(405).end();
60
60
  }
61
61
  static async getAllModelHistory(req, res) {
62
- if (!HistoryController.checkHistoryPermission(req, res))
62
+ if (!await HistoryController.checkHistoryPermission(req, res))
63
63
  return;
64
64
  const { modelId, modelName } = req.body;
65
65
  if (!modelId || !modelName) {
@@ -80,7 +80,7 @@ export class HistoryController {
80
80
  }
81
81
  }
82
82
  static async getModelFieldsHistory(req, res) {
83
- if (!HistoryController.checkHistoryPermission(req, res))
83
+ if (!await HistoryController.checkHistoryPermission(req, res))
84
84
  return;
85
85
  const { historyId } = req.body;
86
86
  if (!historyId) {
@@ -103,12 +103,12 @@ export class HistoryController {
103
103
  const adapter = req.adminizer.config.history?.adapter ?? 'default';
104
104
  return req.adminizer.historyHandler.get(adapter);
105
105
  }
106
- static checkHistoryPermission(req, res) {
106
+ static async checkHistoryPermission(req, res) {
107
107
  if (!req.adminizer?.historyHandler) {
108
108
  res.status(401).json({ error: 'History system not initialized' });
109
109
  return false;
110
110
  }
111
- const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`history-${req.adminizer.config.history?.adapter ?? 'default'}`, req.user);
111
+ const hasPermission = await req.adminizer.accessRightsHelper.hasPermission(`history-${req.adminizer.config.history?.adapter ?? 'default'}`, req.user);
112
112
  if (!hasPermission) {
113
113
  res.sendStatus(403);
114
114
  return false;
@@ -62,10 +62,17 @@ export default async function inlineUpdate(req, res) {
62
62
  const convertedValue = convertInlineValue(newValue, fieldType);
63
63
  // Update record
64
64
  const id = req.params.id;
65
- const params = {};
66
- params[modelResource.config.identifierField || req.adminizer.config.identifierField] = id;
65
+ // Reuse the resolved identifier: without the primary-key fallback the key is
66
+ // `undefined`, the adapter drops the unknown condition, and the update hits every
67
+ // record the user may reach instead of the one addressed by :id.
68
+ const params = { [identifierField]: id };
67
69
  try {
68
- await modelResource.model.update(params, { [fieldName]: convertedValue }, dataAccessor);
70
+ const updated = await modelResource.model.update(params, { [fieldName]: convertedValue }, dataAccessor);
71
+ // Record access is enforced by the criteria, so a record out of the user's reach
72
+ // simply matches nothing. Reporting success would leave the edited value on screen.
73
+ if (!updated.length) {
74
+ return res.status(404).json({ error: 'Record not found' });
75
+ }
69
76
  return res.json({
70
77
  success: true,
71
78
  data: {
@@ -14,7 +14,7 @@ export default async function list(req, res) {
14
14
  }
15
15
  let dataAccessor = new DataAccessor(req.adminizer, req.user, modelResource, "list");
16
16
  let fields = dataAccessor.getFieldsConfig();
17
- const header = inertiaListHelper(modelResource, req, fields);
17
+ const header = await inertiaListHelper(modelResource, req, fields);
18
18
  const i18nPage = getUiTranslations(req, FILTER_UI_TRANSLATION_KEYS);
19
19
  // Parse pagination
20
20
  const defaultPageSize = req.adminizer.config.list?.defaultPageSize ?? 50;
@@ -61,7 +61,7 @@ export default async function login(req, res) {
61
61
  //Here we use the captchaSolution key to output messages unrelated to the form fields.
62
62
  return inertiaAdminMessage(req, "Profile expired, contact the administrator", 'captchaSolution');
63
63
  }
64
- if (!req.adminizer.accessRightsHelper.hasPermission('access-to-adminpanel', user)) {
64
+ if (!await req.adminizer.accessRightsHelper.hasPermission('access-to-adminpanel', user)) {
65
65
  return inertiaAdminMessage(req, "The user is not allowed to enter, please contact the administrator", 'captchaSolution');
66
66
  }
67
67
  const token = signUser({
@@ -7,7 +7,7 @@ export class NotificationController {
7
7
  return;
8
8
  if (req.method.toUpperCase() === 'POST') {
9
9
  const { s, notificationClass } = req.body;
10
- const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
10
+ const hasPermission = await req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
11
11
  if (!hasPermission) {
12
12
  res.status(403).json({ error: 'Forbidden' });
13
13
  return;
@@ -42,7 +42,7 @@ export class NotificationController {
42
42
  const services = req.adminizer.notificationHandler.getAllServices();
43
43
  let activeServices = [];
44
44
  for (const service of services) {
45
- const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user);
45
+ const hasPermission = await req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user);
46
46
  if (hasPermission) {
47
47
  activeServices.push({
48
48
  displayName: req.i18n.__(service.displayName),
@@ -68,12 +68,12 @@ export class NotificationController {
68
68
  res.write(': stream-open\n\n');
69
69
  const clientId = `user-${req.user?.id}-${Date.now()}`;
70
70
  // Function for sending events to the client
71
- const sendEvent = (event) => {
71
+ const sendEvent = async (event) => {
72
72
  // Filtering notifications by user rights
73
73
  if (event.type === 'notification') {
74
74
  const notificationClass = event.notificationClass;
75
75
  // UNIFIED rights check via AccessRightsHelper
76
- const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
76
+ const hasPermission = await req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
77
77
  if (!hasPermission) {
78
78
  return; // The user does not have rights to this notification class
79
79
  }
@@ -88,7 +88,10 @@ export class NotificationController {
88
88
  };
89
89
  // We connect the client to all services
90
90
  const services = req.adminizer.notificationHandler.getAllServices();
91
- const allowedServices = services.filter(service => req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user));
91
+ const allowedServices = (await Promise.all(services.map(async (service) => ({
92
+ service,
93
+ allowed: await req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user),
94
+ })))).filter(({ allowed }) => allowed).map(({ service }) => service);
92
95
  allowedServices.forEach(service => {
93
96
  service.addClient(clientId, sendEvent, req.user);
94
97
  // For a system service, add a client to CRUD channels
@@ -100,7 +103,7 @@ export class NotificationController {
100
103
  // });
101
104
  // }
102
105
  });
103
- sendEvent({
106
+ await sendEvent({
104
107
  type: 'connected',
105
108
  data: {
106
109
  message: 'Connected to unified notification stream',
@@ -144,7 +147,7 @@ export class NotificationController {
144
147
  const { notificationClass } = req.params;
145
148
  const { limit = 20, skip = 0, unreadOnly = false } = req.query;
146
149
  // Checking access rights
147
- const hasPermission = req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
150
+ const hasPermission = await req.adminizer.accessRightsHelper.hasPermission(`notification-${notificationClass}`, req.user);
148
151
  if (!hasPermission) {
149
152
  res.status(403).json({ error: 'Forbidden' });
150
153
  return;
@@ -170,7 +173,10 @@ export class NotificationController {
170
173
  try {
171
174
  // Filtering services by access rights
172
175
  const services = req.adminizer.notificationHandler.getAllServices();
173
- const allowedServices = services.filter(service => req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user));
176
+ const allowedServices = (await Promise.all(services.map(async (service) => ({
177
+ service,
178
+ allowed: await req.adminizer.accessRightsHelper.hasPermission(`notification-${service.notificationClass}`, req.user),
179
+ })))).filter(({ allowed }) => allowed).map(({ service }) => service);
174
180
  const allNotifications = [];
175
181
  for (const service of allowedServices) {
176
182
  const notifications = await service.getNotifications(req.user.id, Number(limit), Number(skip), unreadOnly === 'true');
@@ -29,7 +29,7 @@ export default async function view(req, res) {
29
29
  Adminizer.log.error(e);
30
30
  return res.status(500).send({ error: 'Internal Server Error' });
31
31
  }
32
- switch (modelResource.config.model) {
32
+ switch (modelResource.name) {
33
33
  case 'User':
34
34
  let groups;
35
35
  try {
@@ -71,7 +71,7 @@ export default async function view(req, res) {
71
71
  props: groupProps
72
72
  });
73
73
  default:
74
- fields = await FieldsHelper.loadAssociations(req, fields, "edit");
74
+ fields = await FieldsHelper.loadAssociations(req, fields, "edit", dataAccessor.recordAccessCache);
75
75
  for (const field of Object.keys(fields)) {
76
76
  let fieldConfigConfig = fields[field].config;
77
77
  if (fieldConfigConfig.type === 'mediamanager') {
@@ -83,7 +83,7 @@ export default async function view(req, res) {
83
83
  });
84
84
  }
85
85
  }
86
- const props = inertiaAddHelper(req, modelResource, fields, record, true);
86
+ const props = await inertiaAddHelper(req, modelResource, fields, record, true);
87
87
  return req.Inertia.render({
88
88
  component: 'add',
89
89
  props: props
@@ -28,7 +28,7 @@ export async function widgetFilterInfoController(req, res) {
28
28
  if (!filter || !filterService.canViewFilter(filter, user)) {
29
29
  return res.status(404).send({ error: req.i18n.__("Not found") });
30
30
  }
31
- if (!req.adminizer.accessRightsHelper.hasPermission(`read-${filter.modelName}-model`, user)) {
31
+ if (!await req.adminizer.accessRightsHelper.hasPermission(`read-${filter.modelName}-model`, user)) {
32
32
  return res.sendStatus(403);
33
33
  }
34
34
  if (req.method.toUpperCase() === "GET") {
@@ -1,6 +1,7 @@
1
1
  import { User } from "../models/User.js";
2
- import { AccessRightsToken } from "../interfaces/types.js";
2
+ import { AccessRightsToken, GroupPermissionGrant, PermissionContext } from "../interfaces/types.js";
3
3
  import { Adminizer } from "../lib/Adminizer.js";
4
+ export declare function parseGroupPermissionGrant(value: unknown): GroupPermissionGrant | null;
4
5
  export declare class AccessRightsHelper {
5
6
  private _tokens;
6
7
  adminizer: Adminizer;
@@ -8,13 +9,39 @@ export declare class AccessRightsHelper {
8
9
  registerToken(accessRightsToken: AccessRightsToken): void;
9
10
  unregisterToken(tokenId: string): boolean;
10
11
  hasToken(tokenId: string): boolean;
12
+ getToken(tokenId: string): AccessRightsToken | undefined;
11
13
  registerTokens(accessRightsTokens: AccessRightsToken[]): void;
12
14
  registerModelTokens(modelName: string, department?: string): void;
13
15
  getTokens(): AccessRightsToken[];
14
16
  getTokensByDepartment(department: string): AccessRightsToken[];
15
17
  getAllDepartments(): string[];
16
- enoughPermissions(tokens: string[], user: User): boolean;
17
- hasPermission(tokenId: string | undefined, user: User, context?: string): boolean;
18
+ enoughPermissions(tokens: string[], user: User): Promise<boolean>;
19
+ /** Fail-closed synchronous variant of {@link enoughPermissions}. */
20
+ enoughStaticPermissions(tokens: string[], user: User): boolean;
21
+ /**
22
+ * The single access decision: the token exists, a group of this user carries
23
+ * it, and — for a contextual token — its own `check` accepts the context.
24
+ *
25
+ * Always asynchronous on purpose. A `boolean | Promise<boolean>` union made
26
+ * `if (hasPermission(...))` compile while silently granting access, because
27
+ * a pending promise is truthy. Callers that genuinely cannot await use
28
+ * {@link hasStaticPermission} instead.
29
+ */
30
+ hasPermission(tokenId: string | undefined, user: User, context?: PermissionContext): Promise<boolean>;
31
+ /**
32
+ * Synchronous access decision for call sites that cannot await, such as the
33
+ * field configuration built by `DataAccessor`.
34
+ *
35
+ * A contextual token is denied here: its `check` is asynchronous and there
36
+ * is no context to hand it, so the only safe answer is "no". Model CRUD
37
+ * tokens never declare `check`, so the ordinary panel paths are unaffected.
38
+ */
39
+ hasStaticPermission(tokenId: string | undefined, user: User): boolean;
40
+ /** Returns trusted option IDs granted by all groups for a contextual token. */
41
+ getPermissionRights(tokenId: string | undefined, user: User): string[] | null;
42
+ private getRegisteredToken;
43
+ private hasAssignedPermission;
44
+ private runTokenCheck;
18
45
  }
19
46
  export declare class GroupsAccessRightsHelper {
20
47
  static hasAccess(user: User, groupsAccessRights?: string[]): boolean;
@@ -1,4 +1,20 @@
1
1
  import { Adminizer } from "../lib/Adminizer.js";
2
+ export function parseGroupPermissionGrant(value) {
3
+ if (!value ||
4
+ typeof value !== "object" ||
5
+ typeof value.tokenId !== "string" ||
6
+ !Array.isArray(value.rights) ||
7
+ !value.rights.every((right) => typeof right === "string" || typeof right === "number")) {
8
+ return null;
9
+ }
10
+ const parsed = value;
11
+ return {
12
+ tokenId: parsed.tokenId.toLowerCase(),
13
+ rights: Array.from(new Set(parsed.rights.map(String))),
14
+ };
15
+ }
16
+ /** `<verb>-<model>-model` — the id shape registered by {@link AccessRightsHelper.registerModelTokens}. */
17
+ const MODEL_CRUD_TOKEN_PATTERN = /^(create|read|update|delete)-.+-model$/;
2
18
  export class AccessRightsHelper {
3
19
  _tokens = [];
4
20
  adminizer;
@@ -10,6 +26,19 @@ export class AccessRightsHelper {
10
26
  if (!accessRightsToken.id || !accessRightsToken.name || !accessRightsToken.description || !accessRightsToken.department) {
11
27
  throw new Error("Adminpanel > Can not register token: Missed one or more required parameters");
12
28
  }
29
+ if (accessRightsToken.getOptions && typeof accessRightsToken.getOptions !== "function") {
30
+ throw new Error("Adminpanel > Can not register token: getOptions must be a function");
31
+ }
32
+ if (accessRightsToken.check && typeof accessRightsToken.check !== "function") {
33
+ throw new Error("Adminpanel > Can not register token: check must be a function");
34
+ }
35
+ // Model CRUD tokens are consulted from synchronous paths (DataAccessor field
36
+ // config), where a contextual `check` cannot run and is denied. Such a token would
37
+ // silently hide every field of the model, so it is rejected at registration.
38
+ if (accessRightsToken.check && MODEL_CRUD_TOKEN_PATTERN.test(accessRightsToken.id)) {
39
+ throw new Error(`Adminpanel > Can not register token "${accessRightsToken.id}": model CRUD tokens are ` +
40
+ `evaluated synchronously and cannot carry a contextual "check"`);
41
+ }
13
42
  for (let i = 0; i < this._tokens.length; i++) {
14
43
  if (this._tokens[i].id === accessRightsToken.id) {
15
44
  this._tokens.splice(i, 1);
@@ -31,6 +60,10 @@ export class AccessRightsHelper {
31
60
  const normalizedTokenId = tokenId.toLowerCase();
32
61
  return this._tokens.some((token) => token.id === normalizedTokenId);
33
62
  }
63
+ getToken(tokenId) {
64
+ const normalizedTokenId = tokenId.toLowerCase();
65
+ return this._tokens.find((token) => token.id === normalizedTokenId);
66
+ }
34
67
  registerTokens(accessRightsTokens) {
35
68
  for (let token of accessRightsTokens) {
36
69
  this.registerToken(token);
@@ -54,7 +87,7 @@ export class AccessRightsHelper {
54
87
  .map((token) => token.department)
55
88
  .filter((item, pos, self) => self.indexOf(item) === pos);
56
89
  }
57
- enoughPermissions(tokens, user) {
90
+ async enoughPermissions(tokens, user) {
58
91
  if (user.isAdministrator) {
59
92
  return true;
60
93
  }
@@ -62,30 +95,125 @@ export class AccessRightsHelper {
62
95
  if (!tokens.length) {
63
96
  return true;
64
97
  }
65
- return tokens.some((token) => this.hasPermission(token, user));
98
+ for (const token of tokens) {
99
+ if (await this.hasPermission(token, user)) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
66
104
  }
67
- hasPermission(tokenId, user, context) {
68
- if (!this.adminizer.config.auth.enable) {
105
+ /** Fail-closed synchronous variant of {@link enoughPermissions}. */
106
+ enoughStaticPermissions(tokens, user) {
107
+ if (user.isAdministrator) {
69
108
  return true;
70
109
  }
71
- if (user.isAdministrator) {
110
+ if (!tokens.length) {
72
111
  return true;
73
112
  }
74
- if (!tokenId) {
75
- Adminizer.log.warn(`AccessRightsHelper > hasPermission: missing accessRightsToken${context ? ` [${context}]` : ''}`);
113
+ return tokens.some((token) => this.hasStaticPermission(token, user));
114
+ }
115
+ /**
116
+ * The single access decision: the token exists, a group of this user carries
117
+ * it, and — for a contextual token — its own `check` accepts the context.
118
+ *
119
+ * Always asynchronous on purpose. A `boolean | Promise<boolean>` union made
120
+ * `if (hasPermission(...))` compile while silently granting access, because
121
+ * a pending promise is truthy. Callers that genuinely cannot await use
122
+ * {@link hasStaticPermission} instead.
123
+ */
124
+ async hasPermission(tokenId, user, context) {
125
+ if (!this.adminizer.config.auth.enable || user.isAdministrator) {
126
+ return true;
127
+ }
128
+ const token = this.getRegisteredToken(tokenId);
129
+ if (!token) {
130
+ return false;
131
+ }
132
+ if (!this.hasAssignedPermission(token.id, user)) {
133
+ return false;
134
+ }
135
+ if (!token.check) {
136
+ return true;
137
+ }
138
+ return this.runTokenCheck(token, user, context ?? {});
139
+ }
140
+ /**
141
+ * Synchronous access decision for call sites that cannot await, such as the
142
+ * field configuration built by `DataAccessor`.
143
+ *
144
+ * A contextual token is denied here: its `check` is asynchronous and there
145
+ * is no context to hand it, so the only safe answer is "no". Model CRUD
146
+ * tokens never declare `check`, so the ordinary panel paths are unaffected.
147
+ */
148
+ hasStaticPermission(tokenId, user) {
149
+ if (!this.adminizer.config.auth.enable || user.isAdministrator) {
150
+ return true;
151
+ }
152
+ const token = this.getRegisteredToken(tokenId);
153
+ if (!token) {
154
+ return false;
155
+ }
156
+ if (!this.hasAssignedPermission(token.id, user)) {
76
157
  return false;
77
158
  }
78
- tokenId = tokenId.toLowerCase();
79
- const tokenIsValid = this._tokens.some((token) => token.id === tokenId);
80
- if (!tokenIsValid) {
81
- Adminizer.log.error("Adminpanel > Token is not valid", tokenId, user.login);
159
+ if (token.check) {
160
+ Adminizer.log.warn("AccessRightsHelper > hasStaticPermission: contextual token denied in a synchronous check", token.id);
82
161
  return false;
83
162
  }
163
+ return true;
164
+ }
165
+ /** Returns trusted option IDs granted by all groups for a contextual token. */
166
+ getPermissionRights(tokenId, user) {
167
+ if (!this.adminizer.config.auth.enable) {
168
+ return null;
169
+ }
170
+ if (user.isAdministrator) {
171
+ return null;
172
+ }
173
+ const token = this.getRegisteredToken(tokenId);
174
+ if (!token) {
175
+ return [];
176
+ }
84
177
  if (!user.groups) {
85
178
  Adminizer.log.error('User has no groups');
179
+ return [];
180
+ }
181
+ const rights = user.groups.flatMap((group) => group.tokens ?? [])
182
+ .map(parseGroupPermissionGrant)
183
+ .filter((grant) => grant?.tokenId === token.id)
184
+ .flatMap((grant) => grant.rights);
185
+ return Array.from(new Set(rights));
186
+ }
187
+ getRegisteredToken(tokenId) {
188
+ if (!tokenId) {
189
+ Adminizer.log.warn("AccessRightsHelper > hasPermission: missing accessRightsToken");
190
+ return undefined;
191
+ }
192
+ const token = this.getToken(tokenId);
193
+ if (!token) {
194
+ Adminizer.log.error("Adminpanel > Token is not valid", tokenId);
195
+ }
196
+ return token;
197
+ }
198
+ hasAssignedPermission(tokenId, user) {
199
+ if (!this.adminizer.config.auth.enable || user.isAdministrator) {
200
+ return true;
201
+ }
202
+ if (!user.groups) {
203
+ Adminizer.log.error("User has no groups");
204
+ return false;
205
+ }
206
+ return user.groups.some((group) => group.tokens?.some((groupToken) => groupToken === tokenId || parseGroupPermissionGrant(groupToken)?.tokenId === tokenId));
207
+ }
208
+ async runTokenCheck(token, user, context) {
209
+ try {
210
+ const rights = this.getPermissionRights(token.id, user) ?? [];
211
+ return await token.check(user, { ...context, rights });
212
+ }
213
+ catch (error) {
214
+ Adminizer.log.error("AccessRightsHelper > token check failed", token.id, error);
86
215
  return false;
87
216
  }
88
- return user.groups.some((group) => group.tokens?.includes(tokenId));
89
217
  }
90
218
  }
91
219
  export class GroupsAccessRightsHelper {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Adminizer package version. Some built entries keep stable file names because
3
+ * they are imported by a hard-coded path and so cannot be looked up in the
4
+ * manifest (agent.es.js, controls/*.es.js and controls/*.css); for those this
5
+ * version is appended as a query string to invalidate browser caches on release.
6
+ *
7
+ * Only ever put this on a *self-contained* bundle. An entry whose split chunks
8
+ * import it back by name (app.js) must be content-hashed instead — a query
9
+ * string there makes the browser instantiate the module twice.
10
+ */
11
+ export declare function getAssetVersion(): string;
12
+ /** Appends ?v=<version> to a production asset URL; dev URLs pass through untouched. */
13
+ export declare function withAssetVersion(url: string): string;
14
+ export declare function withAssetVersion(url: string | undefined): string | undefined;