@antelopejs/interface-dms 0.10.4 → 0.12.0

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 (374) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +21 -10
  3. package/dist/attachments.d.ts +15 -2
  4. package/dist/attachments.js +14 -1
  5. package/dist/attachments.js.map +1 -1
  6. package/dist/auth/db/models/sessions.model.d.ts +7 -5
  7. package/dist/auth/db/models/sessions.model.js +2 -9
  8. package/dist/auth/db/models/sessions.model.js.map +1 -1
  9. package/dist/auth/db/models/userExternalIdentities.model.d.ts +7 -5
  10. package/dist/auth/db/models/users.model.d.ts +7 -5
  11. package/dist/auth/db/tables/users.table.d.ts +2 -2
  12. package/dist/auth/db/tables/users.table.js.map +1 -1
  13. package/dist/auth/index.d.ts +4 -4
  14. package/dist/auth/internal/jwt-auth-decorator.d.ts +1 -1
  15. package/dist/auth/internal/jwt-auth-decorator.js +6 -3
  16. package/dist/auth/internal/jwt-auth-decorator.js.map +1 -1
  17. package/dist/auth/internal/request-authenticators.d.ts +12 -0
  18. package/dist/auth/internal/request-authenticators.js +53 -10
  19. package/dist/auth/internal/request-authenticators.js.map +1 -1
  20. package/dist/base/activity-feed.d.ts +34 -32
  21. package/dist/base/activity-feed.js +2 -0
  22. package/dist/base/activity-feed.js.map +1 -1
  23. package/dist/base/banner.d.ts +12 -10
  24. package/dist/base/banner.js +2 -0
  25. package/dist/base/banner.js.map +1 -1
  26. package/dist/base/block-registry/internal/describe.js +3 -9
  27. package/dist/base/block-registry/internal/describe.js.map +1 -1
  28. package/dist/base/block-registry/internal/ui.js +1 -1
  29. package/dist/base/block-registry/internal/ui.js.map +1 -1
  30. package/dist/base/card.d.ts +11 -9
  31. package/dist/base/card.js +2 -0
  32. package/dist/base/card.js.map +1 -1
  33. package/dist/base/chart-card.d.ts +4 -2
  34. package/dist/base/chart-card.js +2 -0
  35. package/dist/base/chart-card.js.map +1 -1
  36. package/dist/base/chart-schemas.d.ts +148 -148
  37. package/dist/base/chart.d.ts +65 -0
  38. package/dist/base/chart.js +65 -0
  39. package/dist/base/chart.js.map +1 -1
  40. package/dist/base/code-block.d.ts +5 -3
  41. package/dist/base/code-block.js +2 -0
  42. package/dist/base/code-block.js.map +1 -1
  43. package/dist/base/custom.d.ts +5 -0
  44. package/dist/base/custom.js +5 -0
  45. package/dist/base/custom.js.map +1 -1
  46. package/dist/base/data-types/default/address.d.ts +5 -0
  47. package/dist/base/data-types/default/address.js +5 -0
  48. package/dist/base/data-types/default/address.js.map +1 -1
  49. package/dist/base/data-types/default/choices.d.ts +20 -0
  50. package/dist/base/data-types/default/choices.js +20 -0
  51. package/dist/base/data-types/default/choices.js.map +1 -1
  52. package/dist/base/data-types/default/files.d.ts +22 -5
  53. package/dist/base/data-types/default/files.js +20 -0
  54. package/dist/base/data-types/default/files.js.map +1 -1
  55. package/dist/base/data-types/default/numbers.d.ts +15 -0
  56. package/dist/base/data-types/default/numbers.js +25 -9
  57. package/dist/base/data-types/default/numbers.js.map +1 -1
  58. package/dist/base/data-types/default/relations.d.ts +10 -2
  59. package/dist/base/data-types/default/relations.js +9 -6
  60. package/dist/base/data-types/default/relations.js.map +1 -1
  61. package/dist/base/data-types/default/strings.d.ts +25 -0
  62. package/dist/base/data-types/default/strings.js +25 -0
  63. package/dist/base/data-types/default/strings.js.map +1 -1
  64. package/dist/base/data-types/default-types.d.ts +1 -1
  65. package/dist/base/data-types/field-types.d.ts +10 -0
  66. package/dist/base/data-types/field-types.js +10 -0
  67. package/dist/base/data-types/field-types.js.map +1 -1
  68. package/dist/base/data-types/internal/relation-endpoints.js +1 -0
  69. package/dist/base/data-types/internal/relation-endpoints.js.map +1 -1
  70. package/dist/base/data-types/status-type.d.ts +5 -0
  71. package/dist/base/data-types/status-type.js +5 -0
  72. package/dist/base/data-types/status-type.js.map +1 -1
  73. package/dist/base/display.d.ts +6 -6
  74. package/dist/base/empty-state.d.ts +15 -13
  75. package/dist/base/empty-state.js +2 -0
  76. package/dist/base/empty-state.js.map +1 -1
  77. package/dist/base/form-block-schema.d.ts +50 -50
  78. package/dist/base/form-schema.d.ts +2 -0
  79. package/dist/base/form-schema.js +2 -0
  80. package/dist/base/form-schema.js.map +1 -1
  81. package/dist/base/grid.d.ts +4 -0
  82. package/dist/base/grid.js +4 -0
  83. package/dist/base/grid.js.map +1 -1
  84. package/dist/base/guard.d.ts +30 -28
  85. package/dist/base/guard.js +2 -0
  86. package/dist/base/guard.js.map +1 -1
  87. package/dist/base/helpers/archive-mode-helpers.d.ts +1 -1
  88. package/dist/base/helpers/file-refs.js +15 -15
  89. package/dist/base/helpers/file-refs.js.map +1 -1
  90. package/dist/base/internal/form-schema-validation.js +7 -1
  91. package/dist/base/internal/form-schema-validation.js.map +1 -1
  92. package/dist/base/internal/realtime-topics.js.map +1 -1
  93. package/dist/base/internal/searchable.d.ts +2 -1
  94. package/dist/base/internal/searchable.js +1 -3
  95. package/dist/base/internal/searchable.js.map +1 -1
  96. package/dist/base/key-value-list.d.ts +21 -19
  97. package/dist/base/key-value-list.js +2 -0
  98. package/dist/base/key-value-list.js.map +1 -1
  99. package/dist/base/kpi-card.d.ts +4 -2
  100. package/dist/base/kpi-card.js +2 -0
  101. package/dist/base/kpi-card.js.map +1 -1
  102. package/dist/base/live-toggle.d.ts +2 -0
  103. package/dist/base/live-toggle.js +2 -0
  104. package/dist/base/live-toggle.js.map +1 -1
  105. package/dist/base/meter.d.ts +23 -21
  106. package/dist/base/meter.js +2 -0
  107. package/dist/base/meter.js.map +1 -1
  108. package/dist/base/nav-card-grid.d.ts +24 -19
  109. package/dist/base/nav-card-grid.js +5 -0
  110. package/dist/base/nav-card-grid.js.map +1 -1
  111. package/dist/base/page-toolbar.d.ts +2 -0
  112. package/dist/base/page-toolbar.js +2 -0
  113. package/dist/base/page-toolbar.js.map +1 -1
  114. package/dist/base/period-selector.d.ts +3 -1
  115. package/dist/base/period-selector.js +2 -0
  116. package/dist/base/period-selector.js.map +1 -1
  117. package/dist/base/placeholder.d.ts +2 -0
  118. package/dist/base/placeholder.js +2 -0
  119. package/dist/base/placeholder.js.map +1 -1
  120. package/dist/base/resource-form-schema.d.ts +2 -2
  121. package/dist/base/searchable.d.ts +3 -2
  122. package/dist/base/searchable.js +2 -2
  123. package/dist/base/searchable.js.map +1 -1
  124. package/dist/base/section.d.ts +6 -2
  125. package/dist/base/section.js +4 -0
  126. package/dist/base/section.js.map +1 -1
  127. package/dist/base/stack.d.ts +6 -0
  128. package/dist/base/stack.js +6 -0
  129. package/dist/base/stack.js.map +1 -1
  130. package/dist/base/stat-group.d.ts +16 -14
  131. package/dist/base/stat-group.js +2 -0
  132. package/dist/base/stat-group.js.map +1 -1
  133. package/dist/base/tab.d.ts +10 -8
  134. package/dist/base/tab.js +2 -0
  135. package/dist/base/tab.js.map +1 -1
  136. package/dist/base/table-view/column-display.d.ts +1 -1
  137. package/dist/base/table-view/column-display.js.map +1 -1
  138. package/dist/base/table-view/data-functions.d.ts +10 -4
  139. package/dist/base/table-view/data-functions.js.map +1 -1
  140. package/dist/base/table-view/factory.d.ts +5 -0
  141. package/dist/base/table-view/factory.js +8 -4
  142. package/dist/base/table-view/factory.js.map +1 -1
  143. package/dist/base/table-view/internal/auth.d.ts +17 -3
  144. package/dist/base/table-view/internal/auth.js +26 -5
  145. package/dist/base/table-view/internal/auth.js.map +1 -1
  146. package/dist/base/table-view/internal/column-conditions.d.ts +8 -1
  147. package/dist/base/table-view/internal/column-conditions.js +25 -13
  148. package/dist/base/table-view/internal/column-conditions.js.map +1 -1
  149. package/dist/base/table-view/internal/column-display.d.ts +1 -2
  150. package/dist/base/table-view/internal/column-display.js +3 -9
  151. package/dist/base/table-view/internal/column-display.js.map +1 -1
  152. package/dist/base/table-view/internal/factory-helpers.d.ts +1 -1
  153. package/dist/base/table-view/internal/factory-helpers.js +17 -14
  154. package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
  155. package/dist/base/table-view/internal/factory-lifecycle-created.d.ts +2 -3
  156. package/dist/base/table-view/internal/factory-lifecycle-created.js +5 -5
  157. package/dist/base/table-view/internal/factory-lifecycle-created.js.map +1 -1
  158. package/dist/base/table-view/internal/factory-lifecycle-filter.d.ts +2 -2
  159. package/dist/base/table-view/internal/factory-private-actions.d.ts +2 -2
  160. package/dist/base/table-view/internal/files.d.ts +2 -1
  161. package/dist/base/table-view/internal/files.js +14 -5
  162. package/dist/base/table-view/internal/files.js.map +1 -1
  163. package/dist/base/table-view/internal/footer.d.ts +3 -3
  164. package/dist/base/table-view/internal/footer.js +16 -15
  165. package/dist/base/table-view/internal/footer.js.map +1 -1
  166. package/dist/base/table-view/internal/guards.d.ts +27 -5
  167. package/dist/base/table-view/internal/guards.js +127 -49
  168. package/dist/base/table-view/internal/guards.js.map +1 -1
  169. package/dist/base/table-view/internal/meta-helpers.d.ts +1 -1
  170. package/dist/base/table-view/internal/meta-helpers.js +3 -4
  171. package/dist/base/table-view/internal/meta-helpers.js.map +1 -1
  172. package/dist/base/table-view/internal/meta.js +2 -3
  173. package/dist/base/table-view/internal/meta.js.map +1 -1
  174. package/dist/base/table-view/internal/option-contract.d.ts +1 -1
  175. package/dist/base/table-view/internal/option-footer.d.ts +2 -2
  176. package/dist/base/table-view/internal/option-row-actions.d.ts +1 -1
  177. package/dist/base/table-view/internal/realtime.d.ts +52 -11
  178. package/dist/base/table-view/internal/realtime.js +23 -16
  179. package/dist/base/table-view/internal/realtime.js.map +1 -1
  180. package/dist/base/table-view/internal/resource-form.d.ts +1 -1
  181. package/dist/base/table-view/internal/resource-form.js +1 -1
  182. package/dist/base/table-view/internal/resource-form.js.map +1 -1
  183. package/dist/base/table-view/internal/row-actions-schema.d.ts +12 -12
  184. package/dist/base/table-view/internal/row-rule-shape.d.ts +11 -5
  185. package/dist/base/table-view/internal/row-rule-shape.js +60 -30
  186. package/dist/base/table-view/internal/row-rule-shape.js.map +1 -1
  187. package/dist/base/table-view/internal/row-rules.d.ts +28 -1
  188. package/dist/base/table-view/internal/row-rules.js +8 -19
  189. package/dist/base/table-view/internal/row-rules.js.map +1 -1
  190. package/dist/base/table-view/internal/tabs.d.ts +1 -1
  191. package/dist/base/table-view/internal/tabs.js +1 -4
  192. package/dist/base/table-view/internal/tabs.js.map +1 -1
  193. package/dist/base/table-view/internal/validation.d.ts +2 -2
  194. package/dist/base/table-view/internal/validation.js +7 -7
  195. package/dist/base/table-view/internal/validation.js.map +1 -1
  196. package/dist/base/table-view/meta.d.ts +7 -6
  197. package/dist/base/table-view/meta.js +12 -7
  198. package/dist/base/table-view/meta.js.map +1 -1
  199. package/dist/base/table-view/resource-form.d.ts +3 -1
  200. package/dist/base/table-view/resource-form.js +2 -0
  201. package/dist/base/table-view/resource-form.js.map +1 -1
  202. package/dist/base/table-view/routes.d.ts +73 -21
  203. package/dist/base/table-view/routes.js +13 -10
  204. package/dist/base/table-view/routes.js.map +1 -1
  205. package/dist/base/table-view/schema.d.ts +152 -152
  206. package/dist/base/table-view/source.js +4 -1
  207. package/dist/base/table-view/source.js.map +1 -1
  208. package/dist/base/table-view.d.ts +1 -1
  209. package/dist/base/table-view.js.map +1 -1
  210. package/dist/base/top-list-card.d.ts +35 -33
  211. package/dist/base/top-list-card.js +2 -0
  212. package/dist/base/top-list-card.js.map +1 -1
  213. package/dist/base/types/error.js +5 -2
  214. package/dist/base/types/error.js.map +1 -1
  215. package/dist/base/types/guards.d.ts +4 -4
  216. package/dist/base/types/json.d.ts +1 -10
  217. package/dist/base/types/row-action.d.ts +7 -7
  218. package/dist/component-options/confirm-dialog.d.ts +2 -0
  219. package/dist/component-options/confirm-dialog.js.map +1 -1
  220. package/dist/component-options/data-type.d.ts +2 -2
  221. package/dist/component-options/data-type.js +7 -7
  222. package/dist/component-options/data-type.js.map +1 -1
  223. package/dist/component-options/row-action.d.ts +14 -9
  224. package/dist/component-slots.d.ts +11 -2
  225. package/dist/component-slots.js +1 -0
  226. package/dist/component-slots.js.map +1 -1
  227. package/dist/component.d.ts +7 -8
  228. package/dist/component.js +17 -25
  229. package/dist/component.js.map +1 -1
  230. package/dist/data-controllers/members.d.ts +11 -7
  231. package/dist/data-controllers/members.js +1 -1
  232. package/dist/data-controllers/members.js.map +1 -1
  233. package/dist/data-controllers/roles.d.ts +32 -10
  234. package/dist/db/internal/exportJobs.model.d.ts +7 -5
  235. package/dist/db/internal/inviteResolutions.model.d.ts +7 -5
  236. package/dist/db/internal/tenantLifecycle.model.d.ts +7 -5
  237. package/dist/db/models/roles.model.d.ts +7 -5
  238. package/dist/db/models/tenantMembers.model.d.ts +7 -5
  239. package/dist/db/models/tenants.model.d.ts +7 -5
  240. package/dist/db/models/user_invites.model.d.ts +7 -5
  241. package/dist/guards.d.ts +1 -1
  242. package/dist/hooks.d.ts +1 -2
  243. package/dist/hooks.js +33 -26
  244. package/dist/hooks.js.map +1 -1
  245. package/dist/internal/attachments.d.ts +2 -2
  246. package/dist/internal/component-builder.js +2 -1
  247. package/dist/internal/component-builder.js.map +1 -1
  248. package/dist/internal/component-serialization.d.ts +2 -4
  249. package/dist/internal/component-serialization.js +4 -3
  250. package/dist/internal/component-serialization.js.map +1 -1
  251. package/dist/internal/component-slots.d.ts +19 -2
  252. package/dist/internal/component-slots.js +35 -12
  253. package/dist/internal/component-slots.js.map +1 -1
  254. package/dist/internal/component-watch-filter.d.ts +11 -0
  255. package/dist/internal/component-watch-filter.js +22 -0
  256. package/dist/internal/component-watch-filter.js.map +1 -0
  257. package/dist/internal/component-watches.js +0 -1
  258. package/dist/internal/component-watches.js.map +1 -1
  259. package/dist/internal/own-entry.d.ts +7 -0
  260. package/dist/internal/own-entry.js +13 -0
  261. package/dist/internal/own-entry.js.map +1 -0
  262. package/dist/invite-extension-payloads.d.ts +14 -2
  263. package/dist/invite-extensions/internal/delivery.js +1 -2
  264. package/dist/invite-extensions/internal/delivery.js.map +1 -1
  265. package/dist/invite-extensions/internal/edit.d.ts +2 -1
  266. package/dist/invite-extensions/internal/edit.js +8 -9
  267. package/dist/invite-extensions/internal/edit.js.map +1 -1
  268. package/dist/invite-extensions/internal/form-slot.js +8 -15
  269. package/dist/invite-extensions/internal/form-slot.js.map +1 -1
  270. package/dist/invite-extensions/internal/payload-slices.d.ts +33 -3
  271. package/dist/invite-extensions/internal/payload-slices.js +84 -19
  272. package/dist/invite-extensions/internal/payload-slices.js.map +1 -1
  273. package/dist/invite-extensions/internal/types.d.ts +10 -1
  274. package/dist/invite-resolution.js +6 -4
  275. package/dist/invite-resolution.js.map +1 -1
  276. package/dist/json.d.ts +10 -0
  277. package/dist/json.js +3 -0
  278. package/dist/json.js.map +1 -0
  279. package/dist/layout-banners.d.ts +5 -2
  280. package/dist/layout-banners.js.map +1 -1
  281. package/dist/page/controllers.d.ts +2 -2
  282. package/dist/page/internal/extension-validation.js +3 -2
  283. package/dist/page/internal/extension-validation.js.map +1 -1
  284. package/dist/page/internal/metadata-layout.js +1 -1
  285. package/dist/page/internal/metadata-layout.js.map +1 -1
  286. package/dist/page/internal/metadata-publish.js +3 -2
  287. package/dist/page/internal/metadata-publish.js.map +1 -1
  288. package/dist/page/internal/metadata-registration.js +5 -4
  289. package/dist/page/internal/metadata-registration.js.map +1 -1
  290. package/dist/page/internal/metadata-upload-registration.d.ts +3 -1
  291. package/dist/page/internal/metadata-upload-registration.js +9 -0
  292. package/dist/page/internal/metadata-upload-registration.js.map +1 -1
  293. package/dist/page/internal/metadata-uploads.d.ts +18 -5
  294. package/dist/page/internal/metadata-uploads.js +7 -8
  295. package/dist/page/internal/metadata-uploads.js.map +1 -1
  296. package/dist/page/internal/page-record.d.ts +2 -3
  297. package/dist/page/internal/page-record.js +19 -14
  298. package/dist/page/internal/page-record.js.map +1 -1
  299. package/dist/page/layout-filter.js +17 -6
  300. package/dist/page/layout-filter.js.map +1 -1
  301. package/dist/page/metadata.js +5 -7
  302. package/dist/page/metadata.js.map +1 -1
  303. package/dist/request-tenant.d.ts +10 -4
  304. package/dist/request-tenant.js +16 -34
  305. package/dist/request-tenant.js.map +1 -1
  306. package/dist/tenant-export.d.ts +2 -1
  307. package/dist/tenant-lifecycle.d.ts +13 -4
  308. package/dist/tenant-lifecycle.js +51 -18
  309. package/dist/tenant-lifecycle.js.map +1 -1
  310. package/dist/types.d.ts +20 -0
  311. package/dist/uploads.js +42 -59
  312. package/dist/uploads.js.map +1 -1
  313. package/dist/utils/fire-and-forget.js.map +1 -1
  314. package/dist/utils/internal/decorated-class.d.ts +14 -0
  315. package/dist/utils/internal/decorated-class.js +37 -0
  316. package/dist/utils/internal/decorated-class.js.map +1 -0
  317. package/dist/utils/internal/type-check.d.ts +9 -0
  318. package/dist/utils/internal/type-check.js +13 -5
  319. package/dist/utils/internal/type-check.js.map +1 -1
  320. package/dist/utils/internal/value-parser.d.ts +1 -1
  321. package/dist/utils/internal/value-parser.js +22 -9
  322. package/dist/utils/internal/value-parser.js.map +1 -1
  323. package/dist/watch-actions.d.ts +1 -1
  324. package/docs/1.core/1.introduction.md +82 -77
  325. package/docs/1.core/10.layout-banners.md +57 -45
  326. package/docs/1.core/11.modules-and-dynamic-menus.md +178 -0
  327. package/docs/1.core/12.frontend-modules-and-request-helpers.md +155 -0
  328. package/docs/1.core/13.tenant-data-export.md +120 -0
  329. package/docs/1.core/2.pages-and-modules.md +140 -367
  330. package/docs/1.core/3.components.md +21 -22
  331. package/docs/1.core/4.permissions.md +92 -71
  332. package/docs/1.core/5.tenancy.md +112 -86
  333. package/docs/1.core/6.invites.md +90 -65
  334. package/docs/1.core/7.hooks.md +96 -166
  335. package/docs/1.core/8.realtime.md +44 -45
  336. package/docs/1.core/9.quick-actions.md +37 -30
  337. package/docs/2.auth/1.introduction.md +33 -35
  338. package/docs/2.auth/2.authentication.md +60 -38
  339. package/docs/2.auth/3.tokens-and-sessions.md +56 -55
  340. package/docs/2.auth/4.users.md +79 -51
  341. package/docs/2.auth/5.external-identities.md +25 -26
  342. package/docs/3.base/1.introduction.md +64 -60
  343. package/docs/3.base/10.display-blocks.md +15 -16
  344. package/docs/3.base/11.page-layouts.md +273 -0
  345. package/docs/3.base/12.table-row-actions.md +122 -0
  346. package/docs/3.base/13.table-filters-and-forms.md +104 -0
  347. package/docs/3.base/14.table-events-and-helpers.md +114 -0
  348. package/docs/3.base/15.table-sources-and-gates.md +24 -0
  349. package/docs/3.base/2.types.md +44 -41
  350. package/docs/3.base/3.forms.md +27 -28
  351. package/docs/3.base/4.tables.md +34 -367
  352. package/docs/3.base/5.charts-and-widgets.md +28 -29
  353. package/docs/3.base/6.layout.md +21 -270
  354. package/docs/3.base/8.data-types.md +39 -40
  355. package/docs/3.base/9.helpers.md +36 -38
  356. package/docs/4.notifications/1.introduction.md +22 -26
  357. package/docs/4.notifications/2.categories-and-subjects.md +58 -55
  358. package/docs/4.notifications/3.sending.md +72 -68
  359. package/docs/5.html-render/1.introduction.md +119 -81
  360. package/docs/5.html-render/2.built-in-emails.md +71 -51
  361. package/docs/index.md +18 -29
  362. package/docs/tsconfig.json +4 -0
  363. package/package.json +11 -11
  364. package/docs/images/emails-admin-invite.png +0 -0
  365. package/docs/images/emails-change-notice.png +0 -0
  366. package/docs/images/emails-change-verification.png +0 -0
  367. package/docs/images/emails-digest-logo-dark.png +0 -0
  368. package/docs/images/emails-digest-logo-light.png +0 -0
  369. package/docs/images/emails-digest.png +0 -0
  370. package/docs/images/emails-export-partial.png +0 -0
  371. package/docs/images/emails-export-ready.png +0 -0
  372. package/docs/images/emails-reset-password.png +0 -0
  373. package/docs/images/emails-two-factor.png +0 -0
  374. package/docs/images/emails-user-validation.png +0 -0
@@ -0,0 +1,178 @@
1
+ ---
2
+ title: "Modules and dynamic menus"
3
+ description: "Register a module root in the sidebar, report its state on the modules catalog, and add menu entries resolved per request."
4
+ mode: reference
5
+ ---
6
+
7
+ A module is a feature area with its own root in the sidebar, under `/modules`. A dynamic menu provider adds menu entries resolved for each request, such as one entry per project a tenant owns. Both live in `@antelopejs/interface-dms/page`. Pages and categories, which modules group, are described in [Pages and modules](/docs/interfaces/core/pages-and-modules).
8
+
9
+ ## Declare a module
10
+
11
+ `RegisterModule(info)` creates the module root under `modulesCategory` and returns its `CategoryInfo`.
12
+
13
+ ```typescript
14
+ import { RegisterModule } from "@antelopejs/interface-dms/page";
15
+
16
+ RegisterModule({
17
+ id: "automation",
18
+ title: "$dms_automation.module.title",
19
+ description: "$dms_automation.module.description",
20
+ icon: "i-ph-flow-arrow",
21
+ defaultCategory: { displayName: "$dms_automation.module.pages", order: 0 },
22
+ });
23
+ ```
24
+
25
+ Call `RegisterModule` before any page that names the module id. A page resolves its module inside the `PageController` call of its class declaration, before `@RegisterPage()` runs, and an unknown module id throws. Registering the same id twice also throws.
26
+
27
+ ### `ModuleInfo`
28
+
29
+ | Option | Type | Description |
30
+ | ------ | ---- | ----------- |
31
+ | `id` | `string` | Module id. Also the URL segment under `/modules`. Required. |
32
+ | `title` | `string` | Display name of the module root. Required. |
33
+ | `description` | `string` | Description carried on the module root. Required. |
34
+ | `icon` | `string` | Icon name. Required. |
35
+ | `landingPage` | `string` | Id of the page the module opens on. `/modules/<id>` redirects to it and keeps the query and hash. Defaults to the first page by `order`. An id that matches no page of the module logs a warning and falls back to that default. |
36
+ | `version` | `string` | Version shown on the catalog tile (`1.4.0`). |
37
+ | `catalogCategory` | `string` | Category the module is filed under in the module catalog (`"Content"`, `"Developer"`), possibly a `$` i18n key. The Modules page and the module store both use it. Without one, a store module takes the category of the store catalog, and any other module goes under "Other". It has no relation to menu categories (`MenuOptions.category`, `defaultCategory`). |
38
+ | `status` | `(context: ModuleCatalogContext) => MaybePromise<ModuleStatus>` | Reports the module's state on its catalog tile. |
39
+ | `readout` | `(context: ModuleCatalogContext) => MaybePromise<ModuleReadoutLine[]>` | Returns short lines that say what the module is doing now. |
40
+ | `defaultCategory` | `{ displayName?, icon?, order?, urlSlug? }` | Groups the module's pages that declare `module` without a `category`. Without it, those pages attach directly to the module root. |
41
+
42
+ The generated default category has the id `pages` and the `urlSlug` `"pages"` unless you set one. Its label falls back to `"$menu.section.pages"` when `displayName` is empty or omitted.
43
+
44
+ ### Catalog hooks
45
+
46
+ The DMS calls `status` and `readout` on each load of the modules catalog. The catalog is for platform owners only. Each hook receives a `ModuleCatalogContext`:
47
+
48
+ ```typescript fragment
49
+ interface ModuleCatalogContext {
50
+ user: User | undefined;
51
+ tenantId: string;
52
+ }
53
+ ```
54
+
55
+ `ModuleStatus` is `"live" | "beta" | "update" | "attention"`. `live` means the module works normally, `beta` that it is usable and still changing, `update` that a newer version is available, and `attention` that a platform owner must act, for example on a failing job. A module without `status` shows as `live`.
56
+
57
+ `ModuleReadoutLine` is `{ text: string; tone?: ModuleReadoutTone }`, where `text` may be a `$` i18n key and `ModuleReadoutTone` is `"success" | "info" | "warning" | "error"`. The tile shows at most three lines. A line with empty text is dropped, and a tone outside the four values shows as `info`.
58
+
59
+ A hook that throws, or that does not answer within two seconds, is ignored and logged once. The tile then shows `live` and no readout. A `status` that returns a value outside `ModuleStatus` also falls back to `live`.
60
+
61
+ ### Attach pages to a module
62
+
63
+ Set `module` on the page options. `category` stays available to group pages inside the module:
64
+
65
+ ```typescript fragment
66
+ import {
67
+ PageController,
68
+ RegisterPage,
69
+ } from "@antelopejs/interface-dms/page";
70
+ import "./module";
71
+
72
+ @RegisterPage()
73
+ export class OverviewPage extends PageController("overview", {
74
+ displayName: "$dms_automation.overview.title",
75
+ icon: "i-ph-gauge",
76
+ module: "automation",
77
+ order: 0,
78
+ }) {}
79
+ ```
80
+
81
+ The bare `./module` import runs the file that calls `RegisterModule`, so the module exists before the page declares itself.
82
+
83
+ A page that declares both `module` and a `category` must name a category under that module's root. Otherwise registration throws. A page or category that declares `module` cannot sit under `settingsCategory` either. Registration throws for that combination too.
84
+
85
+ ### Module URLs and ids
86
+
87
+ `MODULE_URL_PREFIX` is `"/modules"`. The module root's `urlSlug` is the module id and its parent is `modulesCategory`, so module pages resolve under `/modules/<moduleId>/...`. Their ids follow the same chain. The module root's `fullId` is `modules.<moduleId>`. A page attached to the root is `modules.<moduleId>.<pageId>`, and a page in the default category is `modules.<moduleId>.pages.<pageId>`.
88
+
89
+ `isInsideModule(entry)` returns whether an entry belongs to a module. It accepts any object with optional `module`, `category` and `isModuleRoot` fields, so a `CategoryInfo` or a `PageInfo` qualifies. It returns `true` when the entry is a module root or declares `module`, and otherwise walks the category chain looking for a module root.
90
+
91
+ ```typescript fragment
92
+ import { isInsideModule } from "@antelopejs/interface-dms/page";
93
+
94
+ if (isInsideModule(pageInfo)) {
95
+ showOwnerBadge();
96
+ }
97
+ ```
98
+
99
+ The permissions of a module entry are marked module-scoped instead of entering the grantable permission tree, and a module page is served to platform owners only. [Module-scoped permissions](/docs/interfaces/core/permissions#module-scoped-permissions) describes the consequences.
100
+
101
+ ## Contribute menu entries at request time
102
+
103
+ `Category`, `PageController` and `RegisterModule` declare the menu at import time. A dynamic menu provider adds entries resolved for each request, such as one entry for each project or workspace the tenant owns.
104
+
105
+ ```typescript fragment
106
+ import { RegisterDynamicMenuProvider } from "@antelopejs/interface-dms/page";
107
+
108
+ RegisterDynamicMenuProvider("pages.projects", async (user, tenantId) => {
109
+ const projects = await listProjects(tenantId);
110
+ return projects.map((project, index) => ({
111
+ id: project.slug,
112
+ displayName: project.name,
113
+ fullSlug: "/projects/:projectId",
114
+ params: { projectId: project.id },
115
+ icon: "i-ph-folder",
116
+ order: index,
117
+ }));
118
+ });
119
+ ```
120
+
121
+ ```typescript fragment
122
+ function RegisterDynamicMenuProvider(
123
+ categoryFullId: string,
124
+ resolver: DynamicMenuResolver,
125
+ ): () => void;
126
+
127
+ type DynamicMenuResolver = (
128
+ user: User | undefined,
129
+ tenantId: string,
130
+ permissions: Set<string>,
131
+ ) => MaybePromise<DynamicMenuItem[]>;
132
+ ```
133
+
134
+ A registration is a `DynamicMenuProviderInfo`, which pairs the `categoryFullId` and the `resolver`. The resolver runs during `/sitelayout`. Its entries go into that request's copy of the tree only, and the global navigation tree never changes, so one tenant's entries never reach another tenant's site layout. The dynamic and static children of a category sort together by `order`, then by display name and id, so entries of equal rank keep the same order on every host and across restarts.
135
+
136
+ `permissions` is the caller's effective permission set as the parent category sees it. The set is empty while a tenant access gate denies the tenant, unless the category is flagged `bypassTenantAccessGate`, which keeps the real set. `user` is `undefined` for an unauthenticated request.
137
+
138
+ Providers run concurrently. A provider that throws, or that does not answer within two seconds, contributes nothing to that request and is reported once. The rest of the menu still resolves, and the provider's entries return when it answers again.
139
+
140
+ The registration belongs to the registering module, not to the category it targets. A provider whose category is not registered stays dormant and resolves as soon as the category appears, so module start order and hot reloads need no coordination. The registration is removed when the registering module unloads. The returned function removes it earlier, for a module that stops providing entries while it stays loaded.
141
+
142
+ ### `DynamicMenuItem`
143
+
144
+ An item is not a page. It links to a registered page, so one page can back any number of entries. That page typically has `:name` segments in its slug, or declares `validation.requiredQueryParams`.
145
+
146
+ `params` fills the `:name` segments of `fullSlug` with URL-encoded values, and the entry links to the result. `fullSlug: "/projects/:projectId"` with `params: { projectId: "acme" }` links to `/projects/acme`. The values must fill every segment and nothing else. An entry that leaves a segment empty, or that names a parameter the slug does not have, is skipped with a warning.
147
+
148
+ | Field | Type | Description |
149
+ | ----- | ---- | ----------- |
150
+ | `id` | `string` | Unique among siblings. The DMS appends it to the parent's `fullId` with a dot, and top-level entries use the provider category as their parent. An id that contains a dot is skipped with a warning. For a duplicate sibling id, the first admitted entry wins and the others are skipped with a warning. Required. |
151
+ | `displayName` | `string` | Entry label. Required. |
152
+ | `fullSlug` | `string` | Full slug of the page the entry links to, `:name` segments included. Required. |
153
+ | `params` | `MenuItemParams` | Values for the `:name` segments of `fullSlug`. `MenuItemParams` is `Record<string, string>`. |
154
+ | `query` | `MenuItemQuery` | Query parameters added to the link. |
155
+ | `description` | `string` | Longer description. |
156
+ | `icon` | `string` | Icon name. |
157
+ | `order` | `number` | Sort order among the category's children, dynamic and static alike. |
158
+ | `variant` | `MenuItemVariant` | Semantic look. |
159
+ | `status` | `MenuItemStatus` | Status dot. It trails a leaf entry and sits on the leading icon of a parent, so a parent with a status declares an icon. |
160
+ | `badge` | `string` | Short count or label at the end of the entry (`3`, `new`). |
161
+ | `permissionId` | `string` | Permission required to see the entry, as a raw id. Unset means always visible. |
162
+ | `children` | `readonly DynamicMenuItem[]` | Nested entries. An omitted or empty array means a leaf. |
163
+
164
+ The DMS lists an entry only when the caller can access its target page, so the menu never offers a link that the target refuses. Each child is admitted on its own target access and permission, and a parent that is not admitted drops its whole subtree. The tree is at most three levels deep, counting provider entries as level 1. Deeper entries are dropped with a warning.
165
+
166
+ `permissionId` is a raw id, unlike `MenuOptions.permission`, which takes a whole permission or an `Action`. It is matched against the permission set the caller's browser receives, the way the browser matches it: the wildcard or a literal grant, never a `defaultGranted` fallback. The sidebar therefore never offers an entry whose controls would be inert. While a gate denies the tenant, that set holds only the surfaces flagged `bypassTenantAccessGate`, so a recovery entry declares its permission under its target page, as in `<recoveryPageFullId>.settle`.
167
+
168
+ `permissionId` decides only whether the entry is listed. The target page receives the entry's `query` like any other input from the caller, and it authorizes the value itself.
169
+
170
+ ### `NotifyMenuChanged`
171
+
172
+ ```typescript fragment
173
+ const NotifyMenuChanged: (tenantId?: string) => void;
174
+ ```
175
+
176
+ Call `NotifyMenuChanged` when the data behind a provider changes, for example after a project is created or deleted, so connected clients resync. The signal carries no data. Each client fetches its own `/sitelayout` again, resolved against its own permissions.
177
+
178
+ Pass the `tenantId` whose data changed, so only that tenant's sessions fetch again. Without it, the menu of every connected session is invalidated, which suits changes that are not scoped to a tenant.
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: "Frontend modules and request helpers"
3
+ description: "Register the frontend a module ships, sign upload fields, build public links, read the client address, and save native files from a write route."
4
+ mode: reference
5
+ ---
6
+
7
+ A module's backend uses these functions to ship a frontend, to serve components outside a registered page, and to handle requests. Each section names the subpath it imports from. Pages, which call most of them for you, are described in [Pages and modules](/docs/interfaces/core/pages-and-modules).
8
+
9
+ ## Ship a frontend module
10
+
11
+ `AddFrontendModule(config)` registers a frontend module that your module ships. Call it from your module's `construct()`.
12
+
13
+ ```typescript
14
+ import path from "node:path";
15
+ import { AddFrontendModule } from "@antelopejs/interface-dms/page";
16
+
17
+ export async function construct(): Promise<void> {
18
+ await AddFrontendModule({
19
+ name: "@acme/dms-billing",
20
+ sourcePath: path.join(__dirname, "../frontend-vue"),
21
+ renderer: { name: "vue", version: "3" },
22
+ configKey: "dmsBilling",
23
+ priority: 0,
24
+ options: { prefix: "Billing" },
25
+ });
26
+ }
27
+ ```
28
+
29
+ ### `AddFrontendModuleOptions`
30
+
31
+ | Option | Type | Description |
32
+ | ------ | ---- | ----------- |
33
+ | `name` | `string` | Logical frontend module name. The registry identity also includes the renderer name and major version. Required. |
34
+ | `sourcePath` | `string` | Absolute path to the frontend module directory. Required. |
35
+ | `renderer` | `FrontendRendererMetadata` | Framework the module is written for: a `name` and a `version` compared by major only. Use `{ name: "vue", version: "3" }` for the Inertia and Vue frontend. The `package.json` of `sourcePath` declares the loader releases the module runs on ([Frontend layer](/docs/building/frontend-layer#declaring-the-loader-releases-a-module-supports)). Required. |
36
+ | `options` | `FrontendModuleOptions` | JSON-like values handed to the layer's configuration. Defaults to `{}`. |
37
+ | `privateOptions` | `FrontendModuleOptions` | A second set of values for the layer, kept apart from `options` and left out of `GetFrontendModules()`. Defaults to `{}`. |
38
+ | `priority` | `number` | Order among registered layers. Defaults to `0`. |
39
+ | `configKey` | `string` | Configuration key the layer is published under. |
40
+ | `authEstablishEndpoints` | `string[]` | Backend routes under `/api/` from which the frontend server may open a session for the module, through its `/auth/establish` route. Each is an absolute path with no query string and no segment that climbs out. A malformed entry fails the registration. |
41
+ | `publicEndpoints` | `FrontendPublicEndpoint[]` | Backend routes the module serves to callers outside the dashboard, such as a provider's webhook. See [Public endpoints](#public-endpoints). |
42
+ | `backendProxies` | `FrontendBackendProxy[]` | Backend prefixes outside `/api/**` and `/dms/**` that the browser reaches through the frontend server, such as `{ path: "/media/**", auth: "none" }`. |
43
+
44
+ A `FrontendBackendProxy` has a `path` and an optional `auth`. `path` is `/x`, `/x/*` or `/x/**`, with a literal first segment outside the renderer's own paths (`/api/_dms`, `/@`, `/assets`, `/node_modules`). `auth` is `"session"`, the default, which adds the session's access token on the server and requires the frontend's own `Origin` on a state-changing request, or `"none"`, which never sends the session. A malformed entry fails the registration.
45
+
46
+ Within one logical name, renderer and major version, the registration with the highest `priority` wins, and among equal priorities the latest one wins. The manifest and archive endpoints select a renderer with the paired `renderer` and `rendererVersion` query parameters, matched on the major version. A selection that matches no registered renderer answers `404`, and a request that passes only one of the two parameters answers `400`. A request with neither selects `renderer=vue` and `rendererVersion=3`.
47
+
48
+ #### Public endpoints
49
+
50
+ The frontend server's proxy requires a same-site `Origin` on every state-changing request, forwards it with the signed-in user's session, and relays only a few headers. A webhook that a mail or payment provider calls through the dashboard's public URL fails both checks. Declare such a route public:
51
+
52
+ ```typescript fragment
53
+ await AddFrontendModule({
54
+ name: "@acme/dms-mailing",
55
+ sourcePath: path.join(__dirname, "../frontend-vue"),
56
+ renderer: { name: "vue", version: "3" },
57
+ publicEndpoints: [
58
+ {
59
+ path: "/api/mailing/events/*/*",
60
+ methods: ["POST"],
61
+ headers: ["x-mailing-webhook-secret"],
62
+ },
63
+ ],
64
+ });
65
+ ```
66
+
67
+ A request to a declared route needs no `Origin`, carries the declared headers, and is never sent the session, even from a signed-in browser. A cross-site form that posts to it acts as nobody, so the route authenticates its caller itself, with a shared secret or a signature.
68
+
69
+ A `FrontendPublicEndpoint` has three fields:
70
+
71
+ - `path` is absolute under `/api/`. Its first segment after `/api/` is literal, the module's own prefix and never `*`, and each later `*` matches exactly one segment. It has no query string, no prefix match, and no `.` or `..` segment.
72
+ - `methods` lists the methods it answers, among `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`. Defaults to `["POST"]`.
73
+ - `headers` names the module's own `x-` request headers. Headers that a proxy or the DMS gives a meaning to (`x-forwarded-*`, `x-real-ip`, `x-dms-*`, `x-inertia*`, method overrides) are refused.
74
+
75
+ A malformed entry fails the registration. The declarations reach the frontend server through the manifest it is built from, on an authenticated build only, and its `DMS_PUBLIC_ENDPOINTS` setting adds routes that no module declares.
76
+
77
+ ### Frontend module discovery
78
+
79
+ `GetFrontendModules()` returns a `FrontendModuleMetadata` for each registered frontend module: its `name`, `sourcePath`, `renderer`, `priority`, `options`, `authEstablishEndpoints`, `publicEndpoints`, and `backendProxies` with each `auth` filled in. It never returns `privateOptions`. `FrontendModulesChanged` is an event that fires whenever that list changes. Modules start in parallel, so a module that reads the list in its own `start()` registers a handler, reads the list once, and reads it again in the handler.
80
+
81
+ A module can also ship a pure `FrontendModuleFunction`, exported as `frontendModule` from the file its `package.json` names under `dmsFrontend`. It takes the module's configuration and a `FrontendModuleContext` (`{ dev: boolean }`) and returns a `FrontendModuleRegistration`, the same shape as `AddFrontendModuleOptions`, or `undefined` when the configuration registers no frontend. The backend passes the result to `AddFrontendModule`, and `@antelopejs/dms-frontend` calls the function itself, so a frontend builds and starts without a backend. The function reads only its arguments. Any `privateOptions` it returns hold their shape only, every value an empty string, and the backend and the frontend server fill in the secrets.
82
+
83
+ ## Stamp upload tokens
84
+
85
+ `StampUploadFieldTokens(options)` from `@antelopejs/interface-dms/uploads` takes a component's serialized options and returns them with a signed upload token in every file and image field. That covers the component's own fields and the forms it embeds, such as a relation field's inline add form or a table view's `new`, `edit` and `view` forms. The token binds each field to the storage and path it declares, so a client cannot presign an upload elsewhere.
86
+
87
+ The builders that carry upload fields attach it through `transformOptions`, and page registration stamps the authoritative declaration, so pages declared with `@RegisterPage()` are already covered. Call it yourself only to serialize and serve components outside that path. A token produced outside a registered page stays unbound and cannot authorize an upload.
88
+
89
+ ```typescript fragment
90
+ import { StampUploadFieldTokens } from "@antelopejs/interface-dms/uploads";
91
+
92
+ const components = await StampUploadFieldTokens(layout.components);
93
+ ```
94
+
95
+ Options without an upload field come back untouched. Otherwise the return value is a deep clone, because serialization runs again on a hot reload and each pass must start from the declaration as written.
96
+
97
+ ## Public URLs
98
+
99
+ `GetApiBaseUrl()` from `@antelopejs/interface-dms/api-base-url` and `GetClientBaseUrl()` from `@antelopejs/interface-dms/client-base-url` return the public origins of the DMS API and of the dashboard. They come from the DMS configuration (`apiBaseUrl`, `clientBaseUrl`), and under `ajs dms dev` they follow the dev server's origins. Build an absolute link on them, not on the request's headers. Behind a reverse proxy that forwards no `X-Forwarded-Host`, the request knows only the internal host.
100
+
101
+ ```typescript
102
+ import { GetApiBaseUrl } from "@antelopejs/interface-dms/api-base-url";
103
+ import { GetClientBaseUrl } from "@antelopejs/interface-dms/client-base-url";
104
+
105
+ const apiBase = await GetApiBaseUrl();
106
+ const downloadUrl = `${apiBase}/i18n/site/fr.json`;
107
+ const inviteUrl = `${await GetClientBaseUrl()}/auth/signup`;
108
+ ```
109
+
110
+ Both return `string | undefined`, such as `"https://api.example.com"`. They return `undefined` only when the configuration clears the value.
111
+
112
+ ## Client address
113
+
114
+ `getClientIp(context)` from `@antelopejs/interface-dms/client-ip` returns the address a request came from, resolved the way the DMS resolves the address it records on a session:
115
+
116
+ - When `auth.clientIpHeader` is configured, from the header the hosting platform writes ([Client address from the platform](/docs/building/configuration#client-address-from-the-platform)).
117
+ - Otherwise behind the `auth.trustedProxies` reverse proxies of the configuration ([Client address behind proxies](/docs/building/configuration#client-address-behind-proxies)). Each trusted hop vouches for the `x-forwarded-for` entry it appended, and nothing to the left of it counts.
118
+
119
+ An IPv4-mapped address (`::ffff:10.0.0.1`) comes back as plain IPv4. Key a rate limit or an anonymous visitor on it, not on `x-forwarded-for` read by hand, so the operator declares the proxies once for the DMS and every module.
120
+
121
+ ```typescript
122
+ import { Context, Controller, Post, type RequestContext } from "@antelopejs/interface-api";
123
+ import { getClientIp } from "@antelopejs/interface-dms/client-ip";
124
+
125
+ export class CollectController extends Controller("/api/collect") {
126
+ @Context()
127
+ declare requestContext: RequestContext;
128
+
129
+ @Post("/")
130
+ async collect(): Promise<{ address: string }> {
131
+ const address = await getClientIp(this.requestContext);
132
+ return { address };
133
+ }
134
+ }
135
+ ```
136
+
137
+ It returns an empty string when the request carries no address. `ResolveClientIp(source)` applies the same resolution to values a module already holds, such as a request it queued for later. Its `ClientAddressSource` has `socketAddress` and `forwardedFor`, both `string | undefined`, and optional `headers`, where the `auth.clientIpHeader` header is read.
138
+
139
+ ## `SaveComponentFiles`
140
+
141
+ `SaveComponentFiles<T>(request: AttachmentSaveRequest, save: (promoted: Record<string, unknown>) => Promise<AttachmentSaveResult<T>>): Promise<T>` comes from `@antelopejs/interface-dms/attachments`. Call it from a write route, after authorization, to validate where native files came from, promote staging keys, and persist once through `save`. The [file storage guide](/docs/components/file-storage) covers the form and upload flow and storage authorization.
142
+
143
+ `AttachmentSaveRequest` contains:
144
+
145
+ | Field | Type | Contract |
146
+ | --- | --- | --- |
147
+ | `context` | `RequestContext` | The authorized server request. |
148
+ | `componentIds` | `string[]` | Trusted component identities, never ids the client supplies. |
149
+ | `fields` | `AttachmentField[]` | Attachment fields the server declares. |
150
+ | `submitted` | `Record<string, unknown>` | The submitted values. |
151
+ | `before` | `Record<string, unknown>` | The values persisted before, loaded by the server. |
152
+
153
+ Each `AttachmentField` has `id` (matching the form's `attachmentField`), `key` (the document field), and `kind: "file" | "image"`. The optional `storage`, `visibility: "private" | "public"` and `constraints: UploadConstraints` select the provider and the upload policy. `UploadConstraints` comes from `@antelopejs/interface-file-storage`.
154
+
155
+ The callback returns an `AttachmentSaveResult<T>` of `{ document, result }`. `document` holds the values actually saved, used for cleanup, and `result` goes back to the caller. Retained objects keep their declared visibility. Removed native files are deleted only after persistence succeeds, and cleanup failures are logged. A failed save leaves promoted objects in place, because another save may reference them. Cleanup is not reference-counted, so the caller must not share file keys between documents. The internal attachment validators and the table view save proxies are not entry points for consumers.
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: "Tenant data export"
3
+ description: "How a module contributes to the tenant data export. The contributor handler, the archive it writes into, and the manifest that records the outcome."
4
+ mode: reference
5
+ ---
6
+
7
+ `Hook.TENANT_DATA_EXPORT` asks every module for its part of one tenant's data, and the DMS assembles the parts into a single ZIP archive. It is the one hook whose handler produces something. A handler receives the tenant id, an archive to write into, and an `AbortSignal`. It contributes in either or both of two ways:
8
+
9
+ - It returns a `TenantDataExportContribution`. The DMS serializes its `data` to `modules/<moduleId>.json`.
10
+ - It writes entries into the `archive`, for data too large to hold in memory or that is not JSON.
11
+
12
+ The types on this page live in `@antelopejs/interface-dms/tenant-export`. `@antelopejs/interface-dms/hooks` re-exports them, so either import path works. [Hooks](/docs/interfaces/core/hooks) covers the hook registry this contract builds on.
13
+
14
+ ## Register a contributor
15
+
16
+ `RegisterTenantDataExportContributor` registers the handler with an explicit module id. That id namespaces the contributor's archive entries, so it no longer depends on the call site the runtime attributes the registration to. Prefer it to a bare `RegisterHook`.
17
+
18
+ ```typescript fragment
19
+ import { RegisterTenantDataExportContributor } from "@antelopejs/interface-dms/hooks";
20
+
21
+ RegisterTenantDataExportContributor(
22
+ "acme-billing",
23
+ async (tenantId, archive, signal) => {
24
+ await archive.addStream(
25
+ "invoices.ndjson",
26
+ streamInvoices(tenantId, signal),
27
+ );
28
+ return {
29
+ moduleId: "acme-billing",
30
+ data: await collectBillingSettings(tenantId),
31
+ };
32
+ },
33
+ );
34
+ ```
35
+
36
+ The helper's signature:
37
+
38
+ ```typescript
39
+ import type { Hook, HookHandler } from "@antelopejs/interface-dms/hooks";
40
+
41
+ declare function RegisterTenantDataExportContributor(
42
+ moduleId: string,
43
+ handler: HookHandler<Hook.TENANT_DATA_EXPORT>,
44
+ ): void;
45
+ ```
46
+
47
+ The handler's arguments are `tenantId: string`, `archive: TenantExportArchive` and `signal: AbortSignal`. It returns a `TenantDataExportContribution`, or nothing when it only writes archive entries.
48
+
49
+ ```typescript
50
+ interface TenantDataExportContribution {
51
+ moduleId: string;
52
+ data: unknown;
53
+ }
54
+ ```
55
+
56
+ ## Module ids in the archive
57
+
58
+ The DMS derives each contributor's directory name from the module id its registration recorded:
59
+
60
+ - Every character other than a letter, a digit, `.`, `_` or `-` becomes `-`. A registration under `@acme/dms-billing` writes under `modules/-acme-dms-billing/`.
61
+ - A registration with no module id gets `module-<index>`, its position among the contributors.
62
+ - When two contributors resolve to the same name, the second gets the suffix `-2`, the third `-3`, and so on.
63
+
64
+ The `moduleId` a contribution returns does not move its entries. When it differs from the resolved name, the manifest records it as `declaredModuleId`. Return the same id the registration uses, written in the allowed characters, to keep the two equal.
65
+
66
+ ## `TenantExportArchive`
67
+
68
+ ```typescript
69
+ import type { Readable } from "node:stream";
70
+ import type { JsonValue } from "@antelopejs/interface-dms/json";
71
+
72
+ interface TenantExportArchive {
73
+ addJson(path: string, data: JsonValue): Promise<void>;
74
+ addFile(path: string, localPath: string): Promise<void>;
75
+ addStream(path: string, stream: Readable): Promise<void>;
76
+ }
77
+ ```
78
+
79
+ | Method | Adds |
80
+ | ------ | ---- |
81
+ | `addJson` | `data`, a JSON value (`JsonValue` from `@antelopejs/interface-dms/json`), serialized as a JSON entry. |
82
+ | `addFile` | An artifact already on disk, read from `localPath`. The call rejects when `localPath` does not exist. |
83
+ | `addStream` | A remote or generated dump, without holding it in memory. |
84
+
85
+ Paths are relative. The DMS prefixes every entry with `modules/<moduleId>/`, so two contributors can both write `invoices.ndjson` without a collision. It drops empty, `.` and `..` segments from the path, and rejects a path left with no segment.
86
+
87
+ Each method resolves only once the archiver has consumed the entry. Awaiting each call passes the archiver's back-pressure to whatever produces the data, and keeps memory flat at any export size. A call that is not awaited loses both.
88
+
89
+ The archive accepts writes only while the handler runs. A write after the handler has settled throws `Tenant export archive for '<moduleId>' is no longer writable`.
90
+
91
+ ## Abort signal
92
+
93
+ The `signal` aborts when an export job runs longer than its `maxExecutionTimeMs`, or when the caller of `buildTenantExportArchive` aborts the signal it passed. Pass it down to whatever produces your data, so an aborted export stops reading. An abort fails the whole export. The DMS does not write a partial archive for it.
94
+
95
+ ## Manifest
96
+
97
+ Every run writes a `manifest.json` at the archive root. It records what each contributor produced:
98
+
99
+ ```typescript
100
+ interface TenantExportManifest {
101
+ tenantId: string;
102
+ exportedAt: string;
103
+ partial: boolean;
104
+ modules: TenantExportModuleReport[];
105
+ }
106
+
107
+ interface TenantExportModuleReport {
108
+ moduleId: string;
109
+ declaredModuleId?: string;
110
+ entries: string[];
111
+ failed: boolean;
112
+ error?: string;
113
+ }
114
+ ```
115
+
116
+ Contributors run one after another, in registration order. A contributor that throws does not fail the export, unless the export was aborted or the ZIP stream broke. Its report carries `failed: true` and the error message, keeps the entries it wrote before it failed, and `partial` is `true` on the manifest. A consumer reads `partial` to tell an incomplete archive from a complete one.
117
+
118
+ `declaredModuleId` is present when the returned contribution named another id than the one the archive namespaced the entries under. It is a sign that the registration and the return value disagree.
119
+
120
+ `buildTenantExportArchive` runs the contributors and writes the ZIP, and `startTenantExportJob` runs it as a background export job. Both are covered in [Export a tenant's data](/docs/interfaces/base/helpers#export-a-tenants-data).