@antelopejs/dms 0.8.3 → 0.9.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 (351) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  118. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  120. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  121. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  122. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  123. package/frontend-vue/deferred-plugins.ts +3 -4
  124. package/frontend-vue/dms.email.ts +5 -1
  125. package/frontend-vue/dms.frontend.ts +15 -5
  126. package/frontend-vue/dms.server.ts +74 -0
  127. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  128. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  129. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  130. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  131. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  136. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  137. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  138. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  139. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  140. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  141. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  142. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  143. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  144. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  145. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  148. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  151. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  152. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  154. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  156. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  157. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  158. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  159. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  160. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  161. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  162. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  163. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  164. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  165. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  166. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  169. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  170. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  171. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  179. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  180. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  181. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  182. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  183. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  184. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  185. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  186. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  187. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  191. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  192. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  193. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  194. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  195. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  196. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  198. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  200. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  201. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  202. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  203. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  204. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  205. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  206. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  207. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  208. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  209. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  210. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  211. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  212. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  213. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  214. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  215. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  216. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  217. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  218. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  219. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  220. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  221. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  222. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  223. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  224. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  225. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  226. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  227. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  228. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  230. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  231. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  232. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  233. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  234. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  235. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  238. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  239. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  240. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  241. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  244. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  245. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  246. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  247. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  248. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  249. package/frontend-vue/package.json +2 -2
  250. package/frontend-vue/pnpm-lock.yaml +5 -5
  251. package/frontend-vue/server/private-options.ts +27 -0
  252. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  253. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  254. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  255. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  256. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  257. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  258. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  259. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  260. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  261. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  262. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  263. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  264. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  265. package/frontend-vue/tests/email-layout.test.ts +66 -4
  266. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  267. package/frontend-vue/tests/favicon.test.ts +131 -0
  268. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  269. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  270. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  271. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  272. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  273. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  274. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  275. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  276. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  277. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  278. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  279. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  280. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  281. package/frontend-vue/tests/module-routing.test.ts +2 -5
  282. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  283. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  284. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  285. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  286. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  287. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  288. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  289. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  290. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  291. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  292. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  293. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  294. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  295. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  296. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  297. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  298. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  299. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  300. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  301. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  302. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  303. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  304. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  305. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  306. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  307. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  308. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  309. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  310. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  311. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  312. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  313. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  314. package/frontend-vue/tests/server-routes.test.ts +582 -0
  315. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  316. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  317. package/frontend-vue/tests/side-panels.test.ts +8 -1
  318. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  319. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  320. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  321. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  322. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  323. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  324. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  325. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  326. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  327. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  328. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  329. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  330. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  331. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  332. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  333. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  334. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  335. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  336. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  337. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  338. package/frontend-vue/tests/user-language.test.ts +8 -5
  339. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  340. package/frontend-vue/vitest.config.ts +6 -0
  341. package/package.json +2 -2
  342. package/skills/dms/SKILL.md +2 -1
  343. package/skills/dms-module/SKILL.md +1 -1
  344. package/skills/dms-pages/SKILL.md +4 -1
  345. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  346. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  347. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  348. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  349. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  350. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  351. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -0,0 +1,582 @@
1
+ import { createHmac, randomUUID } from "node:crypto";
2
+ import { createServer, type IncomingMessage, type Server } from "node:http";
3
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
4
+ // The DMS serves its session, sign-in, provider sign-in and e-mail render
5
+ // routes from its own `dms.server.ts`: the renderer has none of its own
6
+ // since 0.6.0, so each request below reaches the DMS's routes alone.
7
+ import { readSealedCookie } from "@antelopejs/dms-frontend/templates/vue/server/auth/session.mjs";
8
+ import { serveModuleRoutes } from "@antelopejs/dms-frontend/templates/vue/server/module-testing.mjs";
9
+ import dmsServer from "../dms.server";
10
+
11
+ const RELAY_SECRET = "relay-secret-for-tests-only";
12
+ const RENDER_SECRET = "render-secret-for-tests-only";
13
+ const ESTABLISH_ENDPOINT = "/api/saas/register/finalize";
14
+ const UUID =
15
+ /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/g;
16
+
17
+ interface BackendCall {
18
+ method: string;
19
+ path: string;
20
+ body: unknown;
21
+ authorization?: string;
22
+ relay?: string;
23
+ forwardedFor?: string;
24
+ }
25
+
26
+ function accessToken(tenantId: string, serial: string): string {
27
+ const payload = Buffer.from(JSON.stringify({ tenantId, serial })).toString(
28
+ "base64url",
29
+ );
30
+ return `header.${payload}.signature`;
31
+ }
32
+
33
+ const USER = {
34
+ _id: "u1",
35
+ email: "ada@example.test",
36
+ name: "Ada",
37
+ language: "fr",
38
+ };
39
+
40
+ /** What the backend answers each path with. */
41
+ function backendAnswer(path: string, body: Record<string, unknown>) {
42
+ if (path === "/api/auth/login" && body.password === "wrong")
43
+ return { status: 401, body: { message: "error.auth.invalid_credentials" } };
44
+ if (path === "/api/auth/login" && body.email === "2fa@example.test")
45
+ return {
46
+ status: 200,
47
+ body: {
48
+ requires_2fa: true,
49
+ two_factor_token: "challenge-1",
50
+ methods: ["totp"],
51
+ },
52
+ };
53
+ if (
54
+ [
55
+ "/api/auth/login",
56
+ "/api/auth/signup",
57
+ "/api/auth/verify-2fa",
58
+ ESTABLISH_ENDPOINT,
59
+ "/api/auth/oauth/github/callback",
60
+ ].includes(path)
61
+ )
62
+ return {
63
+ status: 200,
64
+ body: {
65
+ access_token: accessToken("t1", "login"),
66
+ refresh_token: "refresh-1",
67
+ user: USER,
68
+ },
69
+ };
70
+ if (path === "/api/auth/refresh" && body.token === "revoked")
71
+ return { status: 401, body: { message: "error.auth.invalid_token" } };
72
+ if (path === "/api/auth/refresh" || path === "/api/auth/switch-tenant")
73
+ return {
74
+ status: 200,
75
+ body: {
76
+ access_token: accessToken("t2", path),
77
+ refresh_token: "refresh-2",
78
+ },
79
+ };
80
+ if (path === "/api/auth/me") return { status: 200, body: USER };
81
+ if (path === "/api/auth/oauth/github/authorize-url")
82
+ return {
83
+ status: 200,
84
+ body: {
85
+ state: "state-1",
86
+ authorizeUrl: "https://github.example/authorize",
87
+ },
88
+ };
89
+ return { status: 200, body: { ok: true } };
90
+ }
91
+
92
+ const calls: BackendCall[] = [];
93
+
94
+ function readBody(request: IncomingMessage): Promise<Record<string, unknown>> {
95
+ return new Promise((settle) => {
96
+ const chunks: Buffer[] = [];
97
+ request.on("data", (chunk: Buffer) => chunks.push(chunk));
98
+ request.on("end", () => {
99
+ const text = Buffer.concat(chunks).toString("utf8");
100
+ settle(text ? JSON.parse(text) : {});
101
+ });
102
+ });
103
+ }
104
+
105
+ async function listen(server: Server): Promise<string> {
106
+ await new Promise<void>((settle) => server.listen(0, "127.0.0.1", settle));
107
+ const address = server.address();
108
+ if (!address || typeof address !== "object") throw new Error("no address");
109
+ return `http://127.0.0.1:${address.port}`;
110
+ }
111
+
112
+ function close(server: Server): Promise<void> {
113
+ return new Promise((settle) => {
114
+ server.closeAllConnections();
115
+ server.close(() => settle());
116
+ });
117
+ }
118
+
119
+ interface Answer {
120
+ status: number;
121
+ location: string | null;
122
+ body: unknown;
123
+ cookies: { name: string; attributes: string; value: unknown }[];
124
+ calls: BackendCall[];
125
+ }
126
+
127
+ function normalized<T>(value: T): T {
128
+ return JSON.parse(
129
+ JSON.stringify(value ?? null)
130
+ .replaceAll(UUID, "<account>")
131
+ .replaceAll(/"updatedAt":\d+/g, '"updatedAt":"<stamp>"')
132
+ .replaceAll(/"expiresAt":\d+/g, '"expiresAt":"<time>"'),
133
+ );
134
+ }
135
+
136
+ /** A Set-Cookie header, its sealed value decoded. */
137
+ function decodedCookie(header: string) {
138
+ const [pair, ...attributes] = header.split("; ");
139
+ const separator = pair.indexOf("=");
140
+ const scopedName = pair.slice(0, separator);
141
+ const name = scopedName.replace(/_[0-9a-f]{12}$/, "");
142
+ const raw = pair.slice(separator + 1);
143
+ const value = raw
144
+ ? readSealedCookie({ headers: { cookie: pair } }, name)
145
+ : "";
146
+ return { name: scopedName, attributes: attributes.join("; "), value };
147
+ }
148
+
149
+ async function ask(
150
+ base: string,
151
+ path: string,
152
+ init: {
153
+ method?: string;
154
+ body?: unknown;
155
+ cookie?: string;
156
+ origin?: boolean;
157
+ headers?: Record<string, string>;
158
+ } = {},
159
+ ): Promise<Answer> {
160
+ calls.length = 0;
161
+ const headers: Record<string, string> = { ...init.headers };
162
+ if (init.cookie) headers.cookie = init.cookie;
163
+ if (init.origin !== false) headers.origin = base;
164
+ if (init.body !== undefined) headers["content-type"] = "application/json";
165
+ const response = await fetch(`${base}${path}`, {
166
+ method: init.method ?? "POST",
167
+ headers,
168
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
169
+ redirect: "manual",
170
+ });
171
+ const text = await response.text();
172
+ return normalized({
173
+ status: response.status,
174
+ location: response.headers.get("location"),
175
+ body: text ? JSON.parse(text) : null,
176
+ cookies: response.headers.getSetCookie().map(decodedCookie),
177
+ calls: calls.map((call) => ({ ...call })),
178
+ });
179
+ }
180
+
181
+ /** The cookies a browser keeps from an answer, as a Cookie header. */
182
+ async function cookiesOf(
183
+ base: string,
184
+ path: string,
185
+ init: Parameters<typeof ask>[2],
186
+ ): Promise<string> {
187
+ const response = await fetch(`${base}${path}`, {
188
+ method: init?.method ?? "POST",
189
+ headers: {
190
+ origin: base,
191
+ "content-type": "application/json",
192
+ ...(init?.cookie ? { cookie: init.cookie } : {}),
193
+ },
194
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body),
195
+ redirect: "manual",
196
+ });
197
+ await response.arrayBuffer();
198
+ return response.headers
199
+ .getSetCookie()
200
+ .map((header) => header.split(";", 1)[0])
201
+ .filter((pair) => !pair.endsWith("="))
202
+ .join("; ");
203
+ }
204
+
205
+ function renderToken(): string {
206
+ const header = Buffer.from('{"alg":"HS256","typ":"JWT"}').toString(
207
+ "base64url",
208
+ );
209
+ const now = Math.floor(Date.now() / 1000);
210
+ const payload = Buffer.from(
211
+ JSON.stringify({
212
+ namespace: "html-render-service",
213
+ purpose: "html-render",
214
+ aud: "dms-frontend",
215
+ jti: randomUUID(),
216
+ iat: now,
217
+ exp: now + 300,
218
+ }),
219
+ ).toString("base64url");
220
+ const signature = createHmac("sha256", RENDER_SECRET)
221
+ .update(`${header}.${payload}`)
222
+ .digest("base64url");
223
+ return `${header}.${payload}.${signature}`;
224
+ }
225
+
226
+ describe("the DMS's server routes", () => {
227
+ const environment = { ...process.env };
228
+ let backend: Server;
229
+ let moduleRoutes: Awaited<ReturnType<typeof serveModuleRoutes>>;
230
+
231
+ beforeAll(async () => {
232
+ backend = createServer(async (request, response) => {
233
+ const body = await readBody(request);
234
+ const path = new URL(request.url ?? "/", "http://backend").pathname;
235
+ calls.push({
236
+ method: request.method ?? "",
237
+ path,
238
+ body,
239
+ authorization: request.headers.authorization,
240
+ relay: request.headers["x-dms-oauth-relay"] as string | undefined,
241
+ forwardedFor: request.headers["x-forwarded-for"] as string | undefined,
242
+ });
243
+ const answer = backendAnswer(path, body);
244
+ response.writeHead(answer.status, { "content-type": "application/json" });
245
+ response.end(JSON.stringify(answer.body));
246
+ });
247
+ process.env.DMS_API_BASE_URL = await listen(backend);
248
+ process.env.DMS_SESSION_SECRET =
249
+ "a-test-only-session-secret-of-32-characters";
250
+ process.env.DMS_COOKIE_SECURE = "false";
251
+ process.env.DMS_OAUTH_RELAY_SECRET = RELAY_SECRET;
252
+ process.env.DMS_HTML_RENDER_SECRET = RENDER_SECRET;
253
+ process.env.DMS_AUTH_ESTABLISH_ENDPOINTS = ESTABLISH_ENDPOINT;
254
+ moduleRoutes = await serveModuleRoutes([{ id: "dms", setup: dmsServer }], {
255
+ privateOptions: {
256
+ dms: {
257
+ oauth: { relaySecret: RELAY_SECRET },
258
+ htmlRender: { serviceSecret: RENDER_SECRET },
259
+ },
260
+ },
261
+ authEstablishEndpoints: [ESTABLISH_ENDPOINT],
262
+ });
263
+ });
264
+
265
+ afterAll(async () => {
266
+ await moduleRoutes.close();
267
+ await close(backend);
268
+ process.env = environment;
269
+ });
270
+
271
+ /** Asks the DMS's routes. */
272
+ async function askDms(
273
+ path: string,
274
+ init:
275
+ | Parameters<typeof ask>[2]
276
+ | ((base: string) => Promise<Parameters<typeof ask>[2]>) = {},
277
+ ): Promise<Answer> {
278
+ const options =
279
+ typeof init === "function" ? await init(moduleRoutes.url) : init;
280
+ return ask(moduleRoutes.url, path, options);
281
+ }
282
+
283
+ it("declares every route and hook with no warning", () => {
284
+ expect(moduleRoutes.warnings).toEqual([]);
285
+ });
286
+
287
+ it("proxies the backend's /api and /dms prefixes with the session, and nothing else", () => {
288
+ const proxies = moduleRoutes.modules.proxies.map(
289
+ (proxy: { path: string; auth: string; owner: string }) => ({
290
+ path: proxy.path,
291
+ auth: proxy.auth,
292
+ }),
293
+ );
294
+ expect(proxies).toEqual(
295
+ expect.arrayContaining([
296
+ { path: "/api/**", auth: "session" },
297
+ { path: "/dms/**", auth: "session" },
298
+ ]),
299
+ );
300
+ expect(proxies).toHaveLength(2);
301
+ });
302
+
303
+ it("signs in for the browser session only, unless asked to keep it", async () => {
304
+ const browserOnly = await askDms("/auth/login", {
305
+ body: { email: "ada@example.test", password: "secret" },
306
+ });
307
+ expect(browserOnly.status).toBe(200);
308
+ expect(browserOnly.cookies.map((cookie) => cookie.attributes)).toEqual([
309
+ "Path=/; HttpOnly; SameSite=Lax",
310
+ "Path=/; HttpOnly; SameSite=Lax",
311
+ "Path=/; HttpOnly; SameSite=Lax",
312
+ ]);
313
+ const kept = await askDms("/auth/login", {
314
+ body: { email: "ada@example.test", password: "secret", keep_login: true },
315
+ });
316
+ expect(
317
+ kept.cookies.every((cookie) =>
318
+ cookie.attributes.endsWith("Max-Age=2592000"),
319
+ ),
320
+ ).toBe(true);
321
+ expect(kept.calls[0].forwardedFor).toBe("127.0.0.1");
322
+ });
323
+
324
+ it("refuses bad credentials with the backend's message, and a login from another site", async () => {
325
+ const refused = await askDms("/auth/login", {
326
+ body: { email: "ada@example.test", password: "wrong" },
327
+ });
328
+ expect(refused.status).toBe(401);
329
+ expect(refused.body).toEqual({ message: "error.auth.invalid_credentials" });
330
+ const crossSite = await askDms("/auth/login", {
331
+ origin: false,
332
+ body: { email: "ada@example.test", password: "secret" },
333
+ });
334
+ expect(crossSite.status).toBe(403);
335
+ expect(crossSite.calls).toEqual([]);
336
+ });
337
+
338
+ it("carries the keep-me-signed-in choice across the 2FA step", async () => {
339
+ const challenge = await askDms("/auth/login", {
340
+ body: { email: "2fa@example.test", password: "secret", keep_login: true },
341
+ });
342
+ expect(challenge.cookies[0].attributes).toBe(
343
+ "Path=/auth/verify-2fa; HttpOnly; SameSite=Lax; Max-Age=300",
344
+ );
345
+ const verified = await askDms("/auth/verify-2fa", async (base) => ({
346
+ cookie: await cookiesOf(base, "/auth/login", {
347
+ body: {
348
+ email: "2fa@example.test",
349
+ password: "secret",
350
+ keep_login: true,
351
+ },
352
+ }),
353
+ body: { token: "challenge-1", code: "123456" },
354
+ }));
355
+ expect(verified.status).toBe(200);
356
+ expect(
357
+ verified.cookies.some((cookie) =>
358
+ cookie.attributes.endsWith("Max-Age=2592000"),
359
+ ),
360
+ ).toBe(true);
361
+ });
362
+
363
+ it("serves, renews and ends the session on /api/_auth/session", async () => {
364
+ const signedIn = (base: string) =>
365
+ cookiesOf(base, "/auth/login", {
366
+ body: {
367
+ email: "ada@example.test",
368
+ password: "secret",
369
+ keep_login: true,
370
+ },
371
+ });
372
+ const read = await askDms("/api/_auth/session", async (base) => ({
373
+ method: "GET",
374
+ origin: false,
375
+ cookie: await signedIn(base),
376
+ }));
377
+ expect(read.body).toMatchObject({
378
+ user: USER,
379
+ session: { activeTenantId: "t1" },
380
+ });
381
+ const renewed = await askDms("/api/_auth/session", async (base) => ({
382
+ cookie: await signedIn(base),
383
+ }));
384
+ expect(renewed.body).toMatchObject({ session: { activeTenantId: "t2" } });
385
+ const ended = await askDms("/api/_auth/session", async (base) => ({
386
+ method: "DELETE",
387
+ cookie: await signedIn(base),
388
+ }));
389
+ expect(ended.calls.map((call) => call.path)).toEqual(["/api/auth/logout"]);
390
+ const other = await askDms("/api/_auth/session", {
391
+ method: "PUT",
392
+ origin: false,
393
+ });
394
+ expect(other.status).toBe(405);
395
+ });
396
+
397
+ it("switches and validates stored accounts, and forgets one", async () => {
398
+ const twoAccounts = async (base: string) => {
399
+ const first = await cookiesOf(base, "/auth/login", {
400
+ body: {
401
+ email: "ada@example.test",
402
+ password: "secret",
403
+ keep_login: true,
404
+ },
405
+ });
406
+ return cookiesOf(base, "/auth/login", {
407
+ cookie: first,
408
+ body: {
409
+ email: "ada@example.test",
410
+ password: "secret",
411
+ keep_login: true,
412
+ },
413
+ }).then((second) => `${first}; ${second}`);
414
+ };
415
+ const accountOf = (cookie: string) =>
416
+ cookie
417
+ .match(/dms_account_([0-9a-f]{32})_/)?.[1]
418
+ ?.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5");
419
+ for (const action of [
420
+ "switch-account",
421
+ "validate-account",
422
+ "remove-account",
423
+ ]) {
424
+ const answer = await askDms(`/auth/${action}`, async (base) => {
425
+ const cookie = await twoAccounts(base);
426
+ return { cookie, body: { accountId: accountOf(cookie) } };
427
+ });
428
+ expect(answer.status).toBe(200);
429
+ }
430
+ const expired = await askDms("/auth/switch-account", {
431
+ body: { accountId: randomUUID() },
432
+ });
433
+ expect(expired.status).toBe(401);
434
+ });
435
+
436
+ it("switches tenant, opens a declared session and relays the 2FA e-mail", async () => {
437
+ const tenant = await askDms("/auth/switch-tenant", async (base) => ({
438
+ cookie: await cookiesOf(base, "/auth/login", {
439
+ body: { email: "ada@example.test", password: "secret" },
440
+ }),
441
+ body: { tenantId: "t2" },
442
+ }));
443
+ expect(tenant.body).toMatchObject({ success: true });
444
+ expect(
445
+ (await askDms("/auth/switch-tenant", { body: { tenantId: "t2" } }))
446
+ .status,
447
+ ).toBe(401);
448
+ const established = await askDms("/auth/establish", {
449
+ body: { endpoint: ESTABLISH_ENDPOINT, payload: { token: "x" } },
450
+ });
451
+ expect(established.status).toBe(200);
452
+ const refused = await askDms("/auth/establish", {
453
+ body: { endpoint: "/api/auth/login", payload: {} },
454
+ });
455
+ expect(refused.status).toBe(403);
456
+ expect(refused.calls).toEqual([]);
457
+ const relayed = await askDms("/auth/request-2fa-email", {
458
+ body: { token: "challenge-1" },
459
+ });
460
+ expect(relayed.calls.map((call) => call.path)).toEqual([
461
+ "/api/auth/request-2fa-email",
462
+ ]);
463
+ });
464
+
465
+ it("runs a provider sign-in: start, callback, handoff", async () => {
466
+ const start = await askDms(
467
+ "/auth/oauth/github/start?redirect=%2Fprojects",
468
+ {
469
+ method: "GET",
470
+ origin: false,
471
+ },
472
+ );
473
+ expect(start.status).toBe(302);
474
+ expect(start.location).toBe("https://github.example/authorize");
475
+ expect(start.calls[0].relay).toBe(RELAY_SECRET);
476
+ expect(start.cookies[0].attributes).toBe(
477
+ "Path=/auth/oauth; HttpOnly; SameSite=Lax; Max-Age=600",
478
+ );
479
+ const flow = (base: string) =>
480
+ fetch(`${base}/auth/oauth/github/start?redirect=%2Fprojects`, {
481
+ redirect: "manual",
482
+ }).then(
483
+ (response) => response.headers.getSetCookie()[0].split(";", 1)[0],
484
+ );
485
+ const callback = await askDms(
486
+ "/auth/oauth/github/callback?code=c&state=state-1",
487
+ async (base) => ({
488
+ method: "GET",
489
+ origin: false,
490
+ cookie: await flow(base),
491
+ }),
492
+ );
493
+ expect(callback.location).toBe("/auth/oauth/complete?redirect=%2Fprojects");
494
+ expect(callback.calls[0].relay).toBe(RELAY_SECRET);
495
+ const forged = await askDms(
496
+ "/auth/oauth/github/callback?code=c&state=other",
497
+ async (base) => ({
498
+ method: "GET",
499
+ origin: false,
500
+ cookie: await flow(base),
501
+ }),
502
+ );
503
+ expect(forged.location).toBe("/auth?oauth_error=error.oauth.invalid_state");
504
+ const cancelled = await askDms(
505
+ "/auth/oauth/github/callback?error=access_denied",
506
+ {
507
+ method: "GET",
508
+ origin: false,
509
+ },
510
+ );
511
+ expect(cancelled.location).toBe("/auth?oauth_error=error.oauth.cancelled");
512
+ const handoff = await askDms("/auth/oauth/handoff", {});
513
+ expect(handoff.body).toEqual({ kind: "none" });
514
+ });
515
+
516
+ it("renders e-mails for the backend's service token only", async () => {
517
+ const refused = await askDms("/api/html/render", {
518
+ origin: false,
519
+ body: { templateName: "EmailLayout", props: {} },
520
+ });
521
+ expect(refused.status).toBe(401);
522
+ expect(refused.body).toEqual({ message: "Invalid service token" });
523
+ // No e-mail bundle is built here: the render fails once the token passes.
524
+ const missing = await askDms("/api/html/render", () =>
525
+ Promise.resolve({
526
+ origin: false,
527
+ headers: { "x-dms-service-token": renderToken() },
528
+ body: { templateName: "EmailLayout", props: {} },
529
+ }),
530
+ );
531
+ expect(missing.status).toBe(404);
532
+ });
533
+
534
+ it("prefers the manifest's secrets to the environment's", async () => {
535
+ const environmentSecrets = {
536
+ render: process.env.DMS_HTML_RENDER_SECRET,
537
+ relay: process.env.DMS_OAUTH_RELAY_SECRET,
538
+ };
539
+ process.env.DMS_HTML_RENDER_SECRET = "a-stale-render-secret";
540
+ process.env.DMS_OAUTH_RELAY_SECRET = "a-stale-relay-secret";
541
+ try {
542
+ const accepted = await askDms("/api/html/render", {
543
+ origin: false,
544
+ headers: { "x-dms-service-token": renderToken() },
545
+ body: { templateName: "EmailLayout", props: {} },
546
+ });
547
+ // Accepted: the render itself fails, no e-mail bundle is built here.
548
+ expect(accepted.status).toBe(404);
549
+ const start = await askDms("/auth/oauth/github/start", {
550
+ method: "GET",
551
+ origin: false,
552
+ });
553
+ expect(start.calls[0].relay).toBe(RELAY_SECRET);
554
+ } finally {
555
+ process.env.DMS_HTML_RENDER_SECRET = environmentSecrets.render;
556
+ process.env.DMS_OAUTH_RELAY_SECRET = environmentSecrets.relay;
557
+ }
558
+ });
559
+
560
+ it("takes the secrets from the environment when the manifest withheld the private options", async () => {
561
+ const withheld = await serveModuleRoutes([{ id: "dms", setup: dmsServer }]);
562
+ try {
563
+ const response = await fetch(`${withheld.url}/api/html/render`, {
564
+ method: "POST",
565
+ headers: {
566
+ "content-type": "application/json",
567
+ "x-dms-service-token": renderToken(),
568
+ },
569
+ body: JSON.stringify({ templateName: "EmailLayout", props: {} }),
570
+ });
571
+ // Accepted: the render itself fails, no e-mail bundle is built here.
572
+ expect(response.status).toBe(404);
573
+ calls.length = 0;
574
+ await fetch(`${withheld.url}/auth/oauth/github/start`, {
575
+ redirect: "manual",
576
+ });
577
+ expect(calls[0].relay).toBe(RELAY_SECRET);
578
+ } finally {
579
+ await withheld.close();
580
+ }
581
+ });
582
+ });
@@ -18,7 +18,7 @@ beforeEach(() => {
18
18
  fetchCalls.push([url, options]);
19
19
  return {};
20
20
  });
21
- vi.stubGlobal("useUserSession", () => ({
21
+ vi.stubGlobal("useDmsUserSession", () => ({
22
22
  session: ref({ accountId: "account-before" }),
23
23
  fetch: refreshUser,
24
24
  }));