@antelopejs/dms 0.15.0 → 0.16.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 (354) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +36 -26
  3. package/bin/control/base.mjs +9 -4
  4. package/bin/control/browser-client.mjs +24 -5
  5. package/bin/control/browser-wire.mjs +41 -0
  6. package/bin/control/browser-wire.test.mjs +96 -0
  7. package/bin/control/browser.mjs +78 -27
  8. package/bin/control/commands-stack.mjs +58 -8
  9. package/bin/control/commands-verify.mjs +47 -1
  10. package/bin/control/doctor.mjs +49 -4
  11. package/bin/control/failure.mjs +9 -0
  12. package/bin/control/journey.mjs +163 -30
  13. package/bin/control/journey.test.mjs +61 -0
  14. package/bin/control/json.mjs +229 -0
  15. package/bin/control/json.test.mjs +130 -0
  16. package/bin/control/mail.mjs +57 -7
  17. package/bin/control/mail.test.mjs +55 -0
  18. package/bin/control/mongo.mjs +205 -27
  19. package/bin/control/mongo.test.mjs +298 -0
  20. package/bin/control/options.mjs +33 -0
  21. package/bin/control/processes.mjs +24 -2
  22. package/bin/control/renderer.mjs +39 -0
  23. package/bin/control/renderer.test.mjs +324 -0
  24. package/bin/control/run-state.mjs +119 -6
  25. package/bin/control/run-state.test.mjs +171 -0
  26. package/bin/control/session.mjs +31 -14
  27. package/bin/control/stack.mjs +96 -24
  28. package/bin/control/tab.mjs +117 -18
  29. package/bin/control/verify/behaviour.mjs +44 -6
  30. package/bin/control/verify/behaviour.test.mjs +130 -22
  31. package/bin/control/verify/decode.mjs +220 -0
  32. package/bin/control/verify/decode.test.mjs +245 -0
  33. package/bin/control/verify/doors.mjs +1 -1
  34. package/bin/control/verify/doors.test.mjs +40 -0
  35. package/bin/control/verify/emit.mjs +135 -9
  36. package/bin/control/verify/formal.mjs +61 -10
  37. package/bin/control/verify/formal.test.mjs +19 -3
  38. package/bin/control/verify/judgement.mjs +22 -3
  39. package/bin/control/verify/map.mjs +25 -5
  40. package/bin/control/verify/publish.mjs +20 -14
  41. package/bin/control/verify/seeded.mjs +65 -16
  42. package/bin/control/verify/skill.mjs +27 -6
  43. package/bin/control/verify/types.mjs +29 -0
  44. package/bin/control/verify/verdict.mjs +72 -7
  45. package/bin/dms-control.mjs +8 -28
  46. package/checks/api-error-presentation.d.mts +61 -0
  47. package/dist/attachments/access.js +1 -1
  48. package/dist/attachments/access.js.map +1 -1
  49. package/dist/attachments/fields.d.ts +2 -2
  50. package/dist/attachments/fields.js +26 -13
  51. package/dist/attachments/fields.js.map +1 -1
  52. package/dist/attachments/index.d.ts +1 -0
  53. package/dist/attachments/index.js +1 -0
  54. package/dist/attachments/index.js.map +1 -1
  55. package/dist/attachments/read.js.map +1 -1
  56. package/dist/attachments/save.d.ts +1 -1
  57. package/dist/attachments/save.js +49 -17
  58. package/dist/attachments/save.js.map +1 -1
  59. package/dist/attachments/table-view.d.ts +1 -2
  60. package/dist/attachments/table-view.js +23 -13
  61. package/dist/attachments/table-view.js.map +1 -1
  62. package/dist/automation/events.d.ts +10 -10
  63. package/dist/automation/index.js +3 -4
  64. package/dist/automation/index.js.map +1 -1
  65. package/dist/automation/send-notification.d.ts +2 -2
  66. package/dist/automation/send-notification.js +4 -6
  67. package/dist/automation/send-notification.js.map +1 -1
  68. package/dist/config/internal/state.d.ts +7 -0
  69. package/dist/config/internal/state.js +9 -0
  70. package/dist/config/internal/state.js.map +1 -1
  71. package/dist/config/startup-config.d.ts +1 -0
  72. package/dist/config/startup-config.js +11 -0
  73. package/dist/config/startup-config.js.map +1 -1
  74. package/dist/crons/cleanup-user-invites.js.map +1 -1
  75. package/dist/crons/sweep-expired-sessions.js.map +1 -1
  76. package/dist/crons/sweep-stale-exports.js.map +1 -1
  77. package/dist/db/guard-row.d.ts +2 -3
  78. package/dist/db/guard-row.js +1 -2
  79. package/dist/db/guard-row.js.map +1 -1
  80. package/dist/db/index.d.ts +1 -0
  81. package/dist/db/index.js +1 -0
  82. package/dist/db/index.js.map +1 -1
  83. package/dist/db/models/attachments.model.d.ts +7 -5
  84. package/dist/db/models/index.d.ts +1 -0
  85. package/dist/db/models/index.js +1 -0
  86. package/dist/db/models/index.js.map +1 -1
  87. package/dist/db/models/setupToken.model.d.ts +31 -0
  88. package/dist/db/models/setupToken.model.js +62 -0
  89. package/dist/db/models/setupToken.model.js.map +1 -0
  90. package/dist/db/models/signInAttempts.model.d.ts +7 -5
  91. package/dist/db/models/systemState.model.d.ts +7 -5
  92. package/dist/db/models/userEmailChanges.model.d.ts +7 -5
  93. package/dist/db/models/userKnownDevices.model.d.ts +7 -5
  94. package/dist/db/models/userNotificationPreferences.model.d.ts +7 -5
  95. package/dist/db/models/userNotifications.model.d.ts +9 -6
  96. package/dist/db/models/userNotifications.model.js.map +1 -1
  97. package/dist/db/setup-token.d.ts +2 -0
  98. package/dist/db/setup-token.js +28 -0
  99. package/dist/db/setup-token.js.map +1 -0
  100. package/dist/db/tables/index.d.ts +1 -0
  101. package/dist/db/tables/index.js +1 -0
  102. package/dist/db/tables/index.js.map +1 -1
  103. package/dist/db/tables/setupToken.table.d.ts +7 -0
  104. package/dist/db/tables/setupToken.table.js +34 -0
  105. package/dist/db/tables/setupToken.table.js.map +1 -0
  106. package/dist/dev/module-update-notifications.js.map +1 -1
  107. package/dist/hooks/internal/sign-in-monitor.js +6 -6
  108. package/dist/hooks/internal/sign-in-monitor.js.map +1 -1
  109. package/dist/implementations/dms/page/controller.d.ts +2 -2
  110. package/dist/implementations/dms/page/controller.js +3 -1
  111. package/dist/implementations/dms/page/controller.js.map +1 -1
  112. package/dist/implementations/dms/page/dynamic-menu.js +3 -2
  113. package/dist/implementations/dms/page/dynamic-menu.js.map +1 -1
  114. package/dist/implementations/dms/page/notifications.d.ts +6 -2
  115. package/dist/implementations/dms/page/notifications.js +1 -0
  116. package/dist/implementations/dms/page/notifications.js.map +1 -1
  117. package/dist/implementations/dms/page/payloads.js +6 -10
  118. package/dist/implementations/dms/page/payloads.js.map +1 -1
  119. package/dist/implementations/dms/page/preview-pages.js +6 -3
  120. package/dist/implementations/dms/page/preview-pages.js.map +1 -1
  121. package/dist/implementations/dms/page/public-layout.d.ts +2 -0
  122. package/dist/implementations/dms/page/public-layout.js +45 -0
  123. package/dist/implementations/dms/page/public-layout.js.map +1 -0
  124. package/dist/implementations/dms/page/registry.js +10 -4
  125. package/dist/implementations/dms/page/registry.js.map +1 -1
  126. package/dist/implementations/dms/permissions.js +12 -12
  127. package/dist/implementations/dms/permissions.js.map +1 -1
  128. package/dist/implementations/dms-base/bulk-selection.d.ts +3 -4
  129. package/dist/implementations/dms-base/bulk-selection.js +18 -6
  130. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  131. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  132. package/dist/implementations/dms-base/footer-summary.js +44 -27
  133. package/dist/implementations/dms-base/footer-summary.js.map +1 -1
  134. package/dist/implementations/dms-base/row-rule-operand.d.ts +6 -0
  135. package/dist/implementations/dms-base/row-rule-operand.js +41 -0
  136. package/dist/implementations/dms-base/row-rule-operand.js.map +1 -0
  137. package/dist/implementations/dms-base/search-route.d.ts +9 -13
  138. package/dist/implementations/dms-base/search-route.js +7 -6
  139. package/dist/implementations/dms-base/search-route.js.map +1 -1
  140. package/dist/implementations/dms-base/services.d.ts +1 -0
  141. package/dist/implementations/dms-base/services.js +1 -0
  142. package/dist/implementations/dms-base/services.js.map +1 -1
  143. package/dist/implementations/dms-base/table-view.d.ts +5 -6
  144. package/dist/implementations/dms-base/table-view.js +17 -6
  145. package/dist/implementations/dms-base/table-view.js.map +1 -1
  146. package/dist/implementations/dms-html-render/index.js +16 -6
  147. package/dist/implementations/dms-html-render/index.js.map +1 -1
  148. package/dist/implementations/dms-notifications/delivery-guard.d.ts +3 -2
  149. package/dist/implementations/dms-notifications/delivery-guard.js.map +1 -1
  150. package/dist/implementations/dms-notifications/index.js.map +1 -1
  151. package/dist/index.d.ts +0 -1
  152. package/dist/index.js +6 -2
  153. package/dist/index.js.map +1 -1
  154. package/dist/pages/public/preview/block-types.d.ts +1 -2
  155. package/dist/pages/public/preview/block-types.js.map +1 -1
  156. package/dist/pages/settings/overview/account-summary.js +4 -1
  157. package/dist/pages/settings/overview/account-summary.js.map +1 -1
  158. package/dist/pages/settings/users/account-activity-feed.js +3 -2
  159. package/dist/pages/settings/users/account-activity-feed.js.map +1 -1
  160. package/dist/pages/settings/users/account-credentials.d.ts +1 -1
  161. package/dist/pages/settings/users/account-credentials.js +1 -2
  162. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  163. package/dist/pages/settings/users/account-data-store.d.ts +1 -1
  164. package/dist/pages/settings/users/account-data-store.js +1 -3
  165. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  166. package/dist/pages/settings/users/current-password.js +1 -1
  167. package/dist/pages/settings/users/current-password.js.map +1 -1
  168. package/dist/pages/settings/users/email-change.d.ts +2 -2
  169. package/dist/pages/settings/users/email-change.js +2 -3
  170. package/dist/pages/settings/users/email-change.js.map +1 -1
  171. package/dist/pages/settings/users/invite-extension-routes.d.ts +2 -2
  172. package/dist/pages/settings/users/invite-extension-routes.js +11 -4
  173. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  174. package/dist/pages/settings/users/invites.d.ts +2 -2
  175. package/dist/pages/settings/users/member-invite-batch.d.ts +2 -2
  176. package/dist/pages/settings/users/member-invite-batch.js +7 -3
  177. package/dist/pages/settings/users/member-invite-batch.js.map +1 -1
  178. package/dist/pages/settings/users/member-roles-field.js +1 -4
  179. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  180. package/dist/pages/settings/users/members.d.ts +2 -3
  181. package/dist/pages/settings/users/members.js.map +1 -1
  182. package/dist/pages/settings/users/notifications.d.ts +2 -3
  183. package/dist/pages/settings/users/notifications.js +2 -1
  184. package/dist/pages/settings/users/notifications.js.map +1 -1
  185. package/dist/pages/settings/users/profile-helpers.d.ts +0 -1
  186. package/dist/pages/settings/users/profile-helpers.js +0 -17
  187. package/dist/pages/settings/users/profile-helpers.js.map +1 -1
  188. package/dist/pages/settings/users/profile.d.ts +3 -4
  189. package/dist/pages/settings/users/profile.js +12 -12
  190. package/dist/pages/settings/users/profile.js.map +1 -1
  191. package/dist/pages/settings/users/region.d.ts +3 -4
  192. package/dist/pages/settings/users/region.js.map +1 -1
  193. package/dist/pages/settings/users/regional-preferences.d.ts +8 -1
  194. package/dist/pages/settings/users/regional-preferences.js +10 -6
  195. package/dist/pages/settings/users/regional-preferences.js.map +1 -1
  196. package/dist/pages/settings/users/role-preview.js +3 -2
  197. package/dist/pages/settings/users/role-preview.js.map +1 -1
  198. package/dist/pages/settings/users/roles.d.ts +4 -5
  199. package/dist/pages/settings/users/roles.js.map +1 -1
  200. package/dist/pages/settings/users/security.d.ts +9 -12
  201. package/dist/pages/settings/users/security.js +11 -9
  202. package/dist/pages/settings/users/security.js.map +1 -1
  203. package/dist/pages/settings/users/two-factor-operations.d.ts +7 -7
  204. package/dist/pages/settings/users/two-factor-operations.js +5 -7
  205. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  206. package/dist/realtime/broker.js.map +1 -1
  207. package/dist/realtime/heartbeat.js.map +1 -1
  208. package/dist/realtime/redis-broker.js.map +1 -1
  209. package/dist/realtime/sessions.js.map +1 -1
  210. package/dist/realtime/sse.js +1 -2
  211. package/dist/realtime/sse.js.map +1 -1
  212. package/dist/routes/auth/forgot-password.d.ts +1 -1
  213. package/dist/routes/auth/forgot-password.js +1 -3
  214. package/dist/routes/auth/forgot-password.js.map +1 -1
  215. package/dist/routes/auth/login.d.ts +2 -2
  216. package/dist/routes/auth/login.js +8 -4
  217. package/dist/routes/auth/login.js.map +1 -1
  218. package/dist/routes/auth/logout.d.ts +1 -1
  219. package/dist/routes/auth/logout.js +1 -2
  220. package/dist/routes/auth/logout.js.map +1 -1
  221. package/dist/routes/auth/oauth/providers.js +30 -6
  222. package/dist/routes/auth/oauth/providers.js.map +1 -1
  223. package/dist/routes/auth/oauth/state.js +8 -5
  224. package/dist/routes/auth/oauth/state.js.map +1 -1
  225. package/dist/routes/auth/oauth/token-exchange.js +8 -1
  226. package/dist/routes/auth/oauth/token-exchange.js.map +1 -1
  227. package/dist/routes/auth/password-hash-upgrade.d.ts +2 -0
  228. package/dist/routes/auth/password-hash-upgrade.js +22 -0
  229. package/dist/routes/auth/password-hash-upgrade.js.map +1 -0
  230. package/dist/routes/auth/refresh.d.ts +1 -1
  231. package/dist/routes/auth/refresh.js +1 -2
  232. package/dist/routes/auth/refresh.js.map +1 -1
  233. package/dist/routes/auth/request-2fa-email.d.ts +1 -1
  234. package/dist/routes/auth/request-2fa-email.js +1 -2
  235. package/dist/routes/auth/request-2fa-email.js.map +1 -1
  236. package/dist/routes/auth/reset-password.d.ts +1 -1
  237. package/dist/routes/auth/reset-password.js +1 -3
  238. package/dist/routes/auth/reset-password.js.map +1 -1
  239. package/dist/routes/auth/session-handoff.d.ts +1 -1
  240. package/dist/routes/auth/session-handoff.js +1 -2
  241. package/dist/routes/auth/session-handoff.js.map +1 -1
  242. package/dist/routes/auth/signup.d.ts +1 -1
  243. package/dist/routes/auth/signup.js +1 -2
  244. package/dist/routes/auth/signup.js.map +1 -1
  245. package/dist/routes/auth/switch-tenant.d.ts +2 -3
  246. package/dist/routes/auth/switch-tenant.js +3 -2
  247. package/dist/routes/auth/switch-tenant.js.map +1 -1
  248. package/dist/routes/auth/validate-forgot-password-token.d.ts +1 -1
  249. package/dist/routes/auth/validate-forgot-password-token.js +1 -3
  250. package/dist/routes/auth/validate-forgot-password-token.js.map +1 -1
  251. package/dist/routes/auth/validate-invite-token.d.ts +1 -1
  252. package/dist/routes/auth/validate-invite-token.js +1 -3
  253. package/dist/routes/auth/validate-invite-token.js.map +1 -1
  254. package/dist/routes/auth/verify-2fa.d.ts +1 -1
  255. package/dist/routes/auth/verify-2fa.js +1 -2
  256. package/dist/routes/auth/verify-2fa.js.map +1 -1
  257. package/dist/routes/auth/verify-email.d.ts +1 -1
  258. package/dist/routes/auth/verify-email.js +1 -2
  259. package/dist/routes/auth/verify-email.js.map +1 -1
  260. package/dist/routes/auth.d.ts +14 -15
  261. package/dist/routes/auth.js +15 -13
  262. package/dist/routes/auth.js.map +1 -1
  263. package/dist/routes/authentication.d.ts +2 -0
  264. package/dist/routes/authentication.js +2 -0
  265. package/dist/routes/authentication.js.map +1 -1
  266. package/dist/routes/files.d.ts +1 -2
  267. package/dist/routes/files.js.map +1 -1
  268. package/dist/routes/onboarding.d.ts +4 -3
  269. package/dist/routes/onboarding.js +20 -3
  270. package/dist/routes/onboarding.js.map +1 -1
  271. package/dist/routes/realtime.js +5 -6
  272. package/dist/routes/realtime.js.map +1 -1
  273. package/dist/routes/store.d.ts +6 -7
  274. package/dist/routes/store.js +19 -13
  275. package/dist/routes/store.js.map +1 -1
  276. package/dist/store/catalog.d.ts +13 -5
  277. package/dist/store/catalog.js +19 -9
  278. package/dist/store/catalog.js.map +1 -1
  279. package/dist/store/compatibility.d.ts +1 -0
  280. package/dist/store/compatibility.js +24 -8
  281. package/dist/store/compatibility.js.map +1 -1
  282. package/dist/store/frontend-compatibility.js +15 -3
  283. package/dist/store/frontend-compatibility.js.map +1 -1
  284. package/dist/store/interfaces.js +35 -13
  285. package/dist/store/interfaces.js.map +1 -1
  286. package/dist/store/internal/frontend/frontend-modules.js +7 -3
  287. package/dist/store/internal/frontend/frontend-modules.js.map +1 -1
  288. package/dist/store/internal/frontend/frontend-ready.d.ts +2 -1
  289. package/dist/store/internal/frontend/frontend-ready.js +6 -4
  290. package/dist/store/internal/frontend/frontend-ready.js.map +1 -1
  291. package/dist/store/internal/frontend/manifest.js +6 -7
  292. package/dist/store/internal/frontend/manifest.js.map +1 -1
  293. package/dist/store/operations.js.map +1 -1
  294. package/dist/store/registry.js.map +1 -1
  295. package/dist/store/runtime.d.ts +13 -0
  296. package/dist/store/runtime.js +9 -1
  297. package/dist/store/runtime.js.map +1 -1
  298. package/dist/utils/admin-invite-email.js +3 -1
  299. package/dist/utils/admin-invite-email.js.map +1 -1
  300. package/dist/utils/email-subjects.js +3 -1
  301. package/dist/utils/email-subjects.js.map +1 -1
  302. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  303. package/dist/utils/ensure-default-tenant.js +126 -8
  304. package/dist/utils/ensure-default-tenant.js.map +1 -1
  305. package/dist/utils/export-jobs-run.js.map +1 -1
  306. package/dist/utils/export-jobs.js +3 -9
  307. package/dist/utils/export-jobs.js.map +1 -1
  308. package/dist/utils/index.d.ts +1 -0
  309. package/dist/utils/index.js +1 -0
  310. package/dist/utils/index.js.map +1 -1
  311. package/dist/utils/own-value.d.ts +1 -0
  312. package/dist/utils/own-value.js +7 -0
  313. package/dist/utils/own-value.js.map +1 -0
  314. package/dist/utils/row-action-rule-evaluator.js +31 -35
  315. package/dist/utils/row-action-rule-evaluator.js.map +1 -1
  316. package/dist/utils/sign-in-country.js +4 -2
  317. package/dist/utils/sign-in-country.js.map +1 -1
  318. package/dist/utils/tenant-export-archive.js +9 -14
  319. package/dist/utils/tenant-export-archive.js.map +1 -1
  320. package/dist/utils/user-agent.js +2 -1
  321. package/dist/utils/user-agent.js.map +1 -1
  322. package/dist/validation/auth.schema.d.ts +10 -10
  323. package/dist/validation/onboarding/register-admin.schema.d.ts +5 -0
  324. package/dist/validation/onboarding/register-admin.schema.js +3 -0
  325. package/dist/validation/onboarding/register-admin.schema.js.map +1 -1
  326. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  327. package/dist/validation/role-editor.schema.d.ts +1 -1
  328. package/frontend-vue/build/backend-boundary.ts +49 -18
  329. package/frontend-vue/dms.frontend.ts +8 -12
  330. package/frontend-vue/layers/dms-auth/server/accounts.ts +44 -3
  331. package/frontend-vue/layers/dms-auth/server/actions.ts +17 -4
  332. package/frontend-vue/layers/dms-auth/server/oauth.ts +42 -4
  333. package/frontend-vue/layers/dms-core/app/types/watch.ts +1 -2
  334. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +1 -0
  335. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +1 -0
  336. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +5 -0
  337. package/frontend-vue/layers/dms-layout/app/app.config.ts +1 -0
  338. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +9 -9
  339. package/frontend-vue/layers/dms-layout/app/components/layout/AppLogo.vue +1 -0
  340. package/frontend-vue/layers/dms-layout/app/composables/page/useInstalledModules.ts +78 -0
  341. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +5 -24
  342. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +2 -1
  343. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -1
  344. package/frontend-vue/layers/dms-onboarding/app/components/onboarding/steps/register.vue +21 -1
  345. package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +14 -0
  346. package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-en-GB.json +2 -0
  347. package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-fr-FR.json +2 -0
  348. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +57 -6
  349. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +7 -9
  350. package/frontend-vue/layers/dms-ui/app/entries/composables/form/registerFormFunctions.ts +1 -0
  351. package/frontend-vue/lazy-plugin.ts +18 -0
  352. package/frontend-vue/package.json +2 -2
  353. package/package.json +19 -13
  354. package/skills/dms-project/SKILL.md +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.16.0
4
+
5
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.15.1...v0.16.0)
6
+
7
+ ### 📖 Documentation
8
+
9
+ - **readme:** Start the demos with npx @antelopejs/core ([#753](https://github.com/AntelopeJS/dms/pull/753))
10
+ - **readme:** Show the Meet and DMS films, with new GIFs ([1e3f7694](https://github.com/AntelopeJS/dms/commit/1e3f7694))
11
+ - **readme:** Link the read-only live demo ([#665](https://github.com/AntelopeJS/dms/pull/665))
12
+
13
+ ### 🏡 Chore
14
+
15
+ - Land the cleanup campaign on main ([#761](https://github.com/AntelopeJS/dms/pull/761))
16
+ - **release:** @antelopejs/interface-dms v0.12.0 ([0e6b850d](https://github.com/AntelopeJS/dms/commit/0e6b850d))
17
+
18
+ ### ❤️ Contributors
19
+
20
+ - Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting))
21
+
22
+ ## v0.15.1
23
+
24
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.15.0...v0.15.1)
25
+
26
+ ### 🔥 Performance
27
+
28
+ - **page:** Send anonymous visitors only the public pages of the site layout ([#675](https://github.com/AntelopeJS/dms/pull/675))
29
+
30
+ ### 🩹 Fixes
31
+
32
+ - **auth:** Upgrade an outdated password hash on sign-in ([#691](https://github.com/AntelopeJS/dms/pull/691))
33
+ - **dashboard:** Name the brand logo for screen readers ([#652](https://github.com/AntelopeJS/dms/pull/652))
34
+ - **onboarding:** Require a setup token to register the first owner ([#654](https://github.com/AntelopeJS/dms/pull/654))
35
+ - **tenants:** Elect one replica to provision the default tenant through a renewed lease ([#657](https://github.com/AntelopeJS/dms/pull/657))
36
+ - **layout:** Count the sidebar's installed modules like the Modules page ([#666](https://github.com/AntelopeJS/dms/pull/666))
37
+
38
+ ### 📖 Documentation
39
+
40
+ - **github:** Send questions and ideas to Discord ([#672](https://github.com/AntelopeJS/dms/pull/672))
41
+
42
+ ### 🏡 Chore
43
+
44
+ - **release:** @antelopejs/interface-dms v0.10.4 ([3c856c69](https://github.com/AntelopeJS/dms/commit/3c856c69))
45
+
46
+ ### ❤️ Contributors
47
+
48
+ - Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting))
49
+
3
50
  ## v0.15.0
4
51
 
5
52
  [compare changes](https://github.com/AntelopeJS/dms/compare/v0.14.0...v0.15.0)
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  <picture>
4
4
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/frontend-vue/layers/dms-layout/public/images/antelope-logo/dark.svg">
5
- <img alt="Antelope DMS" src="https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/frontend-vue/layers/dms-layout/public/images/antelope-logo/light.svg" height="72">
5
+ <img alt="Antelope DMS" src="https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/frontend-vue/layers/dms-layout/public/images/antelope-logo/light.svg" data-illustration height="72">
6
6
  </picture>
7
7
 
8
- # @antelopejs/dms
8
+ # `@antelopejs/dms`
9
9
 
10
- <a href="https://www.npmjs.com/package/@antelopejs/dms"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/dms?style=for-the-badge&labelColor=000000&color=000000"></a>
11
- <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/dms?style=for-the-badge&labelColor=000000&color=000000"></a>
12
- <a href="https://discord.gg/g4pFt6eXqu"><img alt="Discord" src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"></a>
13
- <a href="https://dms.antelopejs.com/docs"><img alt="Documentation" src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000"></a>
10
+ <a href="https://www.npmjs.com/package/@antelopejs/dms"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/dms?style=for-the-badge&labelColor=000000&color=000000" data-illustration></a>
11
+ <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/dms?style=for-the-badge&labelColor=000000&color=000000" data-illustration></a>
12
+ <a href="https://discord.gg/g4pFt6eXqu"><img alt="Discord" src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000" data-illustration></a>
13
+ <a href="https://dms.antelopejs.com/docs"><img alt="Documentation" src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000" data-illustration></a>
14
14
 
15
15
  **Admin dashboards for AntelopeJS, declared in TypeScript on the backend.**
16
16
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  </div>
20
20
 
21
- ![A DMS dashboard: a period selector, KPI cards, a sales chart and ranked products](https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/docs/images/template-dashboard.png)
21
+ ![The Helio dashboard of the demo template: KPI cards, a revenue chart and the period selector](./docs/images/captures/helio-dashboard.webp)
22
22
 
23
23
  ## What it does
24
24
 
@@ -43,23 +43,21 @@ The DMS also needs an API module, a module implementing the database interface,
43
43
 
44
44
  ## Quick start
45
45
 
46
- Clone [template-dms-demo](https://github.com/AntelopeJS/template-dms-demo), which loads the DMS, its companions and a seeded demo application. It needs Node.js 22, pnpm and a MongoDB on `localhost:27017`.
46
+ Create a project from the `template-dms-demo` template, which loads the DMS, its companion modules and Helio, a demo back office for a solar installer. It needs the Node.js and pnpm releases the template pins, and a MongoDB on `localhost:27017`.
47
47
 
48
48
  ```bash
49
- git clone https://github.com/AntelopeJS/template-dms-demo.git my-app
49
+ npx @antelopejs/core project init my-app --template dms-demo
50
50
  cd my-app
51
- pnpm install
52
- pnpm --dir frontend-vue install
53
- cat > .env <<EOF
54
- DMS_API_BASE_URL=http://localhost:5010
55
- DMS_SESSION_SECRET=$(openssl rand -hex 32)
56
- EOF
57
- pnpm exec ajs project modules install
58
51
  pnpm dev # backend on http://localhost:5010
59
- pnpm frontend:dev # dashboard on http://localhost:3001, in a second terminal
60
52
  ```
61
53
 
62
- Open `http://localhost:3001` and create the first administrator. A page is a class whose static fields are blocks:
54
+ When the backend answers, start the dashboard in a second terminal, from the same folder:
55
+
56
+ ```bash
57
+ pnpm frontend:dev # dashboard on http://localhost:3001
58
+ ```
59
+
60
+ Open the address that the backend logs after `No owner exists yet`, and create the first administrator. The [Quickstart](https://dms.antelopejs.com/docs/getting-started/quickstart) walks through each step. A page is a class whose static fields are blocks:
63
61
 
64
62
  ```ts
65
63
  import { PageController, RegisterPage, pagesCategory } from "@antelopejs/interface-dms/page";
@@ -77,14 +75,26 @@ export class SalesPage extends PageController("sales", {
77
75
 
78
76
  ## Compatibility
79
77
 
80
- | `@antelopejs/dms` | `@antelopejs/interface-dms` | `@antelopejs/dms-frontend` (renderer) |
81
- | --- | --- | --- |
82
- | 0.9 | 0.7 | `>=0.6.1 <0.7.0` |
83
- | 0.8 | 0.6 | `>=0.5.2 <0.6.0` |
84
-
85
- **Embedded / AdonisJS 6**: tested with `@antelopejs/core` 1.15.2, `@antelopejs/dms` 0.9.2 and `@antelopejs/dms-frontend` 0.6.1 in an AdonisJS 6 application (see [Run the DMS inside AdonisJS](https://dms.antelopejs.com/docs/getting-started/adonisjs)).
86
-
87
- `@antelopejs/dms` 0.9.2 needs `@antelopejs/core` `>=1.15.2 <2` (its peer dependency). Every renderer release is listed in the frontend's [compatibility table](https://dms.antelopejs.com/docs/frontend/introduction#compatibility).
78
+ <!-- antelopejs-docs-compat:start -->
79
+
80
+ | Package | Supported | Tested with |
81
+ | ------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
82
+ | `@antelopejs/core` | `>=1.17.0 <2` | `1.17.0` |
83
+ | `@antelopejs/interface-api` | `>=0.2.0 <1.0.0` | `0.2.0` |
84
+ | `@antelopejs/interface-api-util` | `>=0.3.0 <1.0.0` | `0.3.0` |
85
+ | `@antelopejs/interface-core` | `>=0.4.0 <1.0.0` | `0.4.0` |
86
+ | `@antelopejs/interface-data-api` | `>=0.6.0 <1.0.0` | `0.6.0` |
87
+ | `@antelopejs/interface-database` | `>=0.3.0 <1.0.0` | `0.3.0` |
88
+ | `@antelopejs/interface-database-decorators` | `>=0.2.0 <1.0.0` | `0.2.0` |
89
+ | `@antelopejs/interface-dms` | `>=0.12.0 <0.13.0` | `0.12.0` |
90
+ | `@antelopejs/interface-dms-automation` | `file:../../.campaign/tgz/antelopejs-interface-dms-automation-b4bc5d2.tgz` | `file:.campaign/tgz/antelopejs-interface-dms-automation-b4bc5d2.tgz` |
91
+ | `@antelopejs/interface-email` | `>=0.0.6 <1.0.0` | `0.0.6` |
92
+ | `@antelopejs/interface-file-storage` | `>=0.1.4 <1.0.0` | `0.1.4` |
93
+ | `@antelopejs/interface-redis` | `>=0.0.9 <1.0.0` | `0.0.9` |
94
+
95
+ <!-- antelopejs-docs-compat:end -->
96
+
97
+ Each DMS release declares the renderer releases it runs on in `engines["@antelopejs/dms-frontend"]` of its frontend module, and the frontend's [compatibility table](https://dms.antelopejs.com/docs/frontend/introduction#compatibility) lists the renderer release line of each DMS release it covers. The DMS also runs embedded in an AdonisJS application: see [Run the DMS inside AdonisJS](https://dms.antelopejs.com/docs/getting-started/adonisjs).
88
98
 
89
99
  ## Documentation
90
100
 
@@ -24,8 +24,8 @@ function baseCache() {
24
24
  * A prepared worktree of the run's base ref, built once per commit under
25
25
  * `~/.cache/dms-control/base/<sha>` with the skill's `prepare` commands.
26
26
  *
27
- * @param {Record<string, any>} state The run
28
- * @param {Record<string, any>} skill The skill
27
+ * @param {import("./run-state.mjs").RunState} state The run
28
+ * @param {import("./verify/skill.mjs").Skill} skill The skill
29
29
  * @returns {{ sha: string, dir: string }} The base commit and its worktree
30
30
  */
31
31
  function baseCheckout(state, skill) {
@@ -47,12 +47,17 @@ function baseCheckout(state, skill) {
47
47
  * containment journey writes data with the base version inside `action`
48
48
  * and reads it back with the head version after it.
49
49
  *
50
- * @param {Record<string, any>} state The run
51
- * @param {Record<string, any>} skill The skill, whose `prepare` builds the base
50
+ * @param {import("./run-state.mjs").RunState} state The run
51
+ * @param {import("./verify/skill.mjs").Skill} skill The skill, whose `prepare` builds the base
52
52
  * @param {(sha: string) => Promise<void>} action What to do against the base backend
53
53
  * @returns {Promise<string>} The base commit
54
54
  */
55
55
  export async function withBaseBackend(state, skill, action) {
56
+ if (state.renderer) {
57
+ throw new Error(
58
+ "A run with --renderer has no base backend: the skill's prepare builds the renderer's repository, not the playground's.",
59
+ );
60
+ }
56
61
  const base = baseCheckout(state, skill);
57
62
  const playground = join(base.dir, relative(state.repo, state.playground));
58
63
  state.baseRoot = join(state.dir, "base-root");
@@ -1,3 +1,5 @@
1
+ import { isOutcome } from "./browser-wire.mjs";
2
+ import { decode } from "./json.mjs";
1
3
  import { CONTROL_BIN } from "./stack.mjs";
2
4
  import { freePort, isAlive, startDetached, waitForOk } from "./processes.mjs";
3
5
  import { saveRun } from "./run-state.mjs";
@@ -5,6 +7,10 @@ import { nextFolder } from "./session.mjs";
5
7
 
6
8
  const READY_TIMEOUT_MS = 60_000;
7
9
 
10
+ /**
11
+ * @param {import("./run-state.mjs").RunState} state The run
12
+ * @returns {Promise<import("./run-state.mjs").ServerRecord>} Its running browser server, started if it was not
13
+ */
8
14
  async function browserServer(state) {
9
15
  if (state.browser && isAlive(state.browser.pid)) return state.browser;
10
16
  const port = await freePort();
@@ -28,17 +34,30 @@ async function browserServer(state) {
28
34
  * page, both kept between commands. The action, the URL, the ARIA snapshot
29
35
  * and a screenshot go to `<run>/evidence/browser/NN-<label>/`.
30
36
  *
31
- * @param {Record<string, any>} state The run
32
- * @param {Record<string, any>} request The action: `do`, `account`, `tab`, and the action's target, value, key or path
37
+ * @param {import("./run-state.mjs").RunState} state The run
38
+ * @param {Omit<import("./browser.mjs").BrowserRequest, "folder">} request The action: `do`, `account`, `tab`, and the action's target, value, key or path
33
39
  * @param {string} label What the step does
34
40
  * @returns {Promise<{ ok: boolean, url: string, error: string | null, aria: string, folder: string }>} The outcome
35
41
  */
36
42
  export async function browserCommand(state, request, label) {
37
43
  const server = await browserServer(state);
38
44
  const folder = nextFolder(state, "browser", label);
39
- const response = await fetch(`http://127.0.0.1:${server.port}/`, {
45
+ const outcome = await postAction(server.port, { ...request, folder });
46
+ return { ...outcome, folder };
47
+ }
48
+
49
+ /**
50
+ * @param {number} port The browser server's port
51
+ * @param {import("./browser.mjs").BrowserRequest} request The action
52
+ * @returns {Promise<import("./browser.mjs").Outcome>} The server's answer
53
+ * @throws When the answer is not an outcome
54
+ */
55
+ export async function postAction(port, request) {
56
+ const response = await fetch(`http://127.0.0.1:${port}/`, {
40
57
  method: "POST",
41
- body: JSON.stringify({ ...request, folder }),
58
+ body: JSON.stringify(request),
42
59
  });
43
- return { ...(await response.json()), folder };
60
+ /** @type {unknown} */
61
+ const answer = await response.json();
62
+ return decode(isOutcome, answer, "the browser server's answer", "an outcome");
44
63
  }
@@ -0,0 +1,41 @@
1
+ import {
2
+ isBoolean,
3
+ isNumber,
4
+ isString,
5
+ nullable,
6
+ objectOf,
7
+ oneOf,
8
+ } from "./json.mjs";
9
+
10
+ /**
11
+ * What the `dms-control browser` client and the browser server post to each
12
+ * other, checked on both sides.
13
+ */
14
+
15
+ const isRoleName = { role: isString };
16
+ const isRoleOptions = { name: isString, exact: isBoolean };
17
+
18
+ /** @type {import("./json.mjs").Guard<import("./browser.mjs").BrowserRequest>} One browser action, as the client posts it */
19
+ export const isBrowserRequest = objectOf(
20
+ { do: isString, account: isString, tab: isString, folder: isString },
21
+ {
22
+ target: objectOf(isRoleName, {
23
+ ...isRoleOptions,
24
+ nth: isNumber,
25
+ within: objectOf(isRoleName, isRoleOptions),
26
+ }),
27
+ value: isString,
28
+ key: isString,
29
+ path: isString,
30
+ state: oneOf(["attached", "detached", "visible", "hidden"]),
31
+ timeoutMs: isNumber,
32
+ },
33
+ );
34
+
35
+ /** @type {import("./json.mjs").Guard<import("./browser.mjs").Outcome>} What the server answers for one action */
36
+ export const isOutcome = objectOf({
37
+ ok: isBoolean,
38
+ url: isString,
39
+ error: nullable(isString),
40
+ aria: isString,
41
+ });
@@ -0,0 +1,96 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import test from "node:test";
4
+ import { answerRequest } from "./browser.mjs";
5
+ import { postAction } from "./browser-client.mjs";
6
+ import { isOutcome } from "./browser-wire.mjs";
7
+ import { decode } from "./json.mjs";
8
+ import { freePort } from "./processes.mjs";
9
+
10
+ const REQUEST = {
11
+ do: "click",
12
+ account: "admin",
13
+ tab: "main",
14
+ folder: "/run/evidence/browser/01-click",
15
+ target: { role: "button", name: "Invite", nth: 1 },
16
+ };
17
+ const OUTCOME = { ok: true, url: "/", error: null, aria: "- button" };
18
+
19
+ /**
20
+ * @param {string} body What a client posted
21
+ * @returns {Promise<{ answer: import("./browser.mjs").Outcome, handled: unknown[] }>} The answer and the requests that reached the handler
22
+ */
23
+ async function answered(body) {
24
+ /** @type {unknown[]} */
25
+ const handled = [];
26
+ const answer = await answerRequest(body, (request) => {
27
+ handled.push(request);
28
+ return Promise.resolve(OUTCOME);
29
+ });
30
+ /** @type {unknown} */
31
+ const posted = JSON.parse(answer);
32
+ return {
33
+ answer: decode(isOutcome, posted, "the answer", "an outcome"),
34
+ handled,
35
+ };
36
+ }
37
+
38
+ await test("a posted request reaches the handler as written and its outcome is the answer", async () => {
39
+ const { answer, handled } = await answered(JSON.stringify(REQUEST));
40
+ assert.deepEqual(handled, [REQUEST]);
41
+ assert.deepEqual(answer, OUTCOME);
42
+ });
43
+
44
+ await test("a request with a field of the wrong type is answered as a failed action, not run", async () => {
45
+ const { answer, handled } = await answered(
46
+ JSON.stringify({ ...REQUEST, tab: 5 }),
47
+ );
48
+ assert.deepEqual(handled, []);
49
+ assert.equal(answer.ok, false);
50
+ assert.match(
51
+ answer.error ?? "",
52
+ /browser request is malformed: expected a browser action/,
53
+ );
54
+ });
55
+
56
+ await test("a request without an action is answered as a failed action, not run", async () => {
57
+ const { answer, handled } = await answered(
58
+ JSON.stringify({ account: "admin" }),
59
+ );
60
+ assert.deepEqual(handled, []);
61
+ assert.match(answer.error ?? "", /malformed: expected a browser action/);
62
+ });
63
+
64
+ await test("a body that is no JSON is answered as a failed action", async () => {
65
+ const { answer, handled } = await answered("{");
66
+ assert.deepEqual(handled, []);
67
+ assert.equal(answer.ok, false);
68
+ });
69
+
70
+ /**
71
+ * @param {import("node:test").TestContext} t The test
72
+ * @param {unknown} answer What the server answers
73
+ * @returns {Promise<number>} The server's port
74
+ */
75
+ async function serving(t, answer) {
76
+ const port = await freePort();
77
+ const server = createServer((_incoming, outgoing) => {
78
+ outgoing.end(JSON.stringify(answer));
79
+ });
80
+ await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
81
+ t.after(() => server.close());
82
+ return port;
83
+ }
84
+
85
+ await test("the client returns the server's outcome", async (t) => {
86
+ const port = await serving(t, OUTCOME);
87
+ assert.deepEqual(await postAction(port, REQUEST), OUTCOME);
88
+ });
89
+
90
+ await test("the client refuses an answer that is no outcome", async (t) => {
91
+ const port = await serving(t, { ...OUTCOME, ok: "yes" });
92
+ await assert.rejects(
93
+ postAction(port, REQUEST),
94
+ /the browser server's answer is malformed: expected an outcome/,
95
+ );
96
+ });
@@ -1,9 +1,22 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { createServer } from "node:http";
3
3
  import { join } from "node:path";
4
+ import { isBrowserRequest } from "./browser-wire.mjs";
5
+ import { describeFailure } from "./failure.mjs";
6
+ import { decode } from "./json.mjs";
4
7
  import { importFrom } from "./mongo.mjs";
5
8
  import { loadRun } from "./run-state.mjs";
6
9
 
10
+ /**
11
+ * @typedef {import("playwright-core").Browser} Browser
12
+ * @typedef {import("playwright-core").BrowserContext} BrowserContext
13
+ * @typedef {import("playwright-core").Locator} Locator
14
+ * @typedef {import("playwright-core").Page} Page
15
+ * @typedef {{ ok: boolean, url: string, error: string | null, aria: string }} Outcome
16
+ * @typedef {{ do: string, baseUrl: string, target?: Target, value?: string, key?: string, path?: string, state?: "attached" | "detached" | "visible" | "hidden", timeoutMs?: number }} Action
17
+ * @typedef {Omit<Action, "baseUrl"> & { account: string, tab: string, folder: string }} BrowserRequest
18
+ */
19
+
7
20
  const ACTION_TIMEOUT_MS = 15_000;
8
21
  const NAVIGATION_TIMEOUT_MS = 60_000;
9
22
  const SETTLE_MS = 400;
@@ -13,9 +26,11 @@ const VIEWPORT = { width: 1280, height: 900 };
13
26
  * Launches the headless Chromium that playwright-core pins.
14
27
  *
15
28
  * @param {string[]} from Folders to resolve playwright-core from
16
- * @returns {Promise<any>} The Playwright browser
29
+ * @returns {Promise<Browser>} The Playwright browser
17
30
  */
18
31
  export async function launchBrowser(from) {
32
+ /** @type {typeof import("playwright-core")} */
33
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- importFrom resolves playwright-core by path from the project under test at run time and answers its namespace as any, which the JSDoc type binds to Playwright's own declarations.
19
34
  const { chromium } = await importFrom("playwright-core", from);
20
35
  return chromium.launch({ headless: true });
21
36
  }
@@ -24,8 +39,8 @@ export async function launchBrowser(from) {
24
39
  * A new browser context: one per account, so cookies never leak between
25
40
  * the users a journey plays.
26
41
  *
27
- * @param {any} browser The Playwright browser
28
- * @returns {Promise<any>} The context
42
+ * @param {Browser} browser The Playwright browser
43
+ * @returns {Promise<BrowserContext>} The context
29
44
  */
30
45
  export function newContext(browser) {
31
46
  return browser.newContext({
@@ -39,15 +54,15 @@ export function newContext(browser) {
39
54
  * What an action targets: an ARIA role and accessible name, optionally
40
55
  * inside another one (`within`), or the n-th of several matches.
41
56
  *
42
- * @typedef {{ role: string, name?: string, exact?: boolean, nth?: number, within?: { role: string, name?: string } }} Target
57
+ * @typedef {{ role: string, name?: string | RegExp, exact?: boolean, nth?: number, within?: { role: string, name?: string | RegExp, exact?: boolean } }} Target
43
58
  */
44
59
 
45
60
  /**
46
61
  * The locator of a target, by role and accessible name, as a user finds it.
47
62
  *
48
- * @param {any} page The Playwright page
63
+ * @param {Page} page The Playwright page
49
64
  * @param {Target} target What to find
50
- * @returns {any} The locator
65
+ * @returns {Locator} The locator
51
66
  */
52
67
  export function locate(page, target) {
53
68
  const scope = target.within
@@ -76,6 +91,7 @@ export function describeTarget(target) {
76
91
  return target.within ? `${own} in ${describeTarget(target.within)}` : own;
77
92
  }
78
93
 
94
+ /** @type {Record<string, (page: Page, action: Action) => Promise<unknown> | undefined>} */
79
95
  const ACTIONS = {
80
96
  goto: (page, action) =>
81
97
  page.goto(new URL(action.path, action.baseUrl).href, {
@@ -123,7 +139,7 @@ export const ACTION_NAMES = Object.keys(ACTIONS);
123
139
  * Writes what the page shows into `folder`: `url.txt`, `aria.yml` (the ARIA
124
140
  * snapshot of the body) and `screenshot.png`.
125
141
  *
126
- * @param {any} page The Playwright page
142
+ * @param {Page} page The Playwright page
127
143
  * @param {string} folder The evidence folder
128
144
  * @returns {Promise<string>} The ARIA snapshot
129
145
  */
@@ -132,7 +148,10 @@ export async function capture(page, folder) {
132
148
  const aria = await page
133
149
  .locator("body")
134
150
  .ariaSnapshot({ timeout: ACTION_TIMEOUT_MS })
135
- .catch((error) => `snapshot failed: ${error.message}`);
151
+ .catch(
152
+ (/** @type {unknown} */ error) =>
153
+ `snapshot failed: ${describeFailure(error)}`,
154
+ );
136
155
  writeFileSync(join(folder, "aria.yml"), `${aria}\n`);
137
156
  await page
138
157
  .screenshot({
@@ -148,10 +167,10 @@ export async function capture(page, folder) {
148
167
  * Runs one action on a page, then captures the page into `folder`. A target
149
168
  * that is missing, hidden or disabled past the timeout fails the action.
150
169
  *
151
- * @param {any} page The Playwright page
152
- * @param {{ do: string, baseUrl: string, target?: Target, value?: string, key?: string, path?: string, state?: string, timeoutMs?: number }} action What to do
170
+ * @param {Page} page The Playwright page
171
+ * @param {Action} action What to do
153
172
  * @param {string} folder The evidence folder
154
- * @returns {Promise<{ ok: boolean, url: string, error: string | null, aria: string }>} The outcome
173
+ * @returns {Promise<Outcome>} The outcome
155
174
  */
156
175
  export async function perform(page, action, folder) {
157
176
  const run = ACTIONS[action.do];
@@ -171,7 +190,7 @@ export async function perform(page, action, folder) {
171
190
  .catch(() => undefined);
172
191
  await page.waitForTimeout(SETTLE_MS);
173
192
  } catch (failure) {
174
- error = String(failure?.message ?? failure).split("\n")[0];
193
+ error = describeFailure(failure).split("\n")[0];
175
194
  }
176
195
  const aria = await capture(page, folder);
177
196
  return { ok: error === null, url: page.url(), error, aria };
@@ -180,10 +199,10 @@ export async function perform(page, action, folder) {
180
199
  /**
181
200
  * Signs an account in through the sign-in form, the way a user does.
182
201
  *
183
- * @param {any} page The Playwright page
202
+ * @param {Page} page The Playwright page
184
203
  * @param {{ email: string, password: string }} credentials The account
185
204
  * @param {{ baseUrl: string, folder: (label: string) => string }} options Where the frontend is and where evidence goes
186
- * @returns {Promise<{ ok: boolean, url: string, error: string | null, aria: string }>} The outcome of the last step
205
+ * @returns {Promise<Outcome>} The outcome of the last step
187
206
  */
188
207
  export async function signInThroughForm(page, credentials, options) {
189
208
  const steps = [
@@ -221,6 +240,35 @@ export async function signInThroughForm(page, credentials, options) {
221
240
  };
222
241
  }
223
242
 
243
+ /**
244
+ * @param {unknown} error What the action threw
245
+ * @returns {Outcome} The failed outcome
246
+ */
247
+ function failedOutcome(error) {
248
+ return { ok: false, url: "", error: describeFailure(error), aria: "" };
249
+ }
250
+
251
+ /**
252
+ * Decodes a posted request and answers it. A body that is no valid request
253
+ * is answered as a failed action, like any action that throws.
254
+ *
255
+ * @param {string} body What the client posted
256
+ * @param {(request: BrowserRequest) => Promise<Outcome>} handle Runs one action
257
+ * @returns {Promise<string>} The outcome as JSON
258
+ */
259
+ export function answerRequest(body, handle) {
260
+ return Promise.resolve()
261
+ .then(() => {
262
+ /** @type {unknown} */
263
+ const posted = JSON.parse(body);
264
+ return handle(
265
+ decode(isBrowserRequest, posted, "browser request", "a browser action"),
266
+ );
267
+ })
268
+ .catch(failedOutcome)
269
+ .then((outcome) => JSON.stringify(outcome));
270
+ }
271
+
224
272
  /**
225
273
  * Serves the run's browser for `dms-control browser`: one context per
226
274
  * account and one page per tab name, kept between commands, driven by JSON
@@ -235,8 +283,15 @@ export async function signInThroughForm(page, credentials, options) {
235
283
  export async function serveBrowser(dir, from, port) {
236
284
  const browser = await launchBrowser(from);
237
285
  browser.on("disconnected", () => process.exit(0));
286
+ /** @type {Map<string, BrowserContext>} */
238
287
  const contexts = new Map();
288
+ /** @type {Map<string, Page>} */
239
289
  const pages = new Map();
290
+ /**
291
+ * @param {string} account The account
292
+ * @param {string} tab The tab name
293
+ * @returns {Promise<Page>} The tab's page
294
+ */
240
295
  const pageFor = async (account, tab) => {
241
296
  const key = `${account}/${tab}`;
242
297
  if (!pages.has(key)) {
@@ -246,6 +301,10 @@ export async function serveBrowser(dir, from, port) {
246
301
  }
247
302
  return pages.get(key);
248
303
  };
304
+ /**
305
+ * @param {BrowserRequest} request What the client posted
306
+ * @returns {Promise<Outcome>} The outcome
307
+ */
249
308
  const handle = async (request) => {
250
309
  const state = loadRun(dir);
251
310
  const page = await pageFor(request.account, request.tab);
@@ -270,21 +329,13 @@ export async function serveBrowser(dir, from, port) {
270
329
  outgoing.end("ok");
271
330
  return;
272
331
  }
332
+ /** @type {Buffer[]} */
273
333
  const chunks = [];
274
- incoming.on("data", (chunk) => chunks.push(chunk));
334
+ incoming.on("data", (/** @type {Buffer} */ chunk) => chunks.push(chunk));
275
335
  incoming.on("end", () => {
276
- handle(JSON.parse(Buffer.concat(chunks).toString("utf8")))
277
- .then((result) => outgoing.end(JSON.stringify(result)))
278
- .catch((error) =>
279
- outgoing.end(
280
- JSON.stringify({
281
- ok: false,
282
- url: "",
283
- error: String(error?.message ?? error),
284
- aria: "",
285
- }),
286
- ),
287
- );
336
+ answerRequest(Buffer.concat(chunks).toString("utf8"), handle)
337
+ .then((answer) => outgoing.end(answer))
338
+ .catch(() => outgoing.destroy());
288
339
  });
289
340
  });
290
341
  await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));